mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 07:26:30 +00:00
782e61350615461a49ba0f86a4cc795a24794de6
171 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
782e613506 |
Tours: day GPX import, wanderer trails and planning over an import
- GPX files drop onto the map or a day; the import lands on that day - wanderer connection per user in Integrations; the workspace offers the user's own trails only, behind an admin switch under the Tours addon - wanderer difficulty is stored on the SAC scale (T1 to T3), backfilled - a GPX or wanderer import can be planned as an editable tour, either in place (the recorded track is kept and can be restored) or as a new tour that remembers the import it came from - rails, rows and detail dialog follow the plan sidebar's cards and badges - admin addon shelves with more than one row fold, collapsed by default - reverse-geocoded waypoint names are cached on the device |
||
|
|
9e475f447a | feat(tours): add non-motorized day tour planning | ||
|
|
8c37597bd7 | feat(dashboard): search every trip by title, date or a place on it (#2190) | ||
|
|
f4626c9f3e |
feat(notifications): admin defaults for users, and channels blocked for everyone
Admin > Notifications gets a Defaults for users matrix: each event and channel starts On, Off (a user may still turn it on) or Blocked (off for everyone). A user's own choice is kept against the default, and a value equal to it is stored as no row, so a later change of the default reaches everyone who never chose. Blocked cells show as locked in the user settings on desktop and phone, and the server ignores any attempt to turn one on. (#1536) |
||
|
|
7acf7f5655 |
feat(admin): a daily limit on Google API calls
Admin > Settings > Google options gets a daily limit with today's count beside it. Every call through the Places and Routes clients is counted per UTC day; once the limit is reached the Google key reads as absent until the next day, so search, details and photos fall back to OpenStreetMap the way a keyless install does, and the admin sees that the day is used up. (#1582) |
||
|
|
8fd46c4c18 |
feat: quality of life sweep from the feature discussions
Small requests from the discussions, each built end to end: planner and map (cycling mode, date first headings, clear a day, stay times per day, off-route stops, map lock, compact unplanned markers, pending stays, cable car, transit quick picks), places (location filter, category from OSM, open in maps, geo: link, image from an attachment, enrichment on file import, places near a pin, place name language, Amap first in China), lists and costs (packed counter, inherited sharing, Markdown to-dos, payment notes, bookings CSV, my plan PDF), sharing (unplanned places, travel only, without photos), journey (photo zoom, drafts, photo order, trip suggestion, day jump), atlas (region counter, region bucket list, places per country), vacay half company holidays, studio copy and paste plus the book's own layouts, and RESTORE_FROM_BACKUP for a first start restore. |
||
|
|
b1c05bca83 |
feat(llm-parse): read a photo, as a booking or as a receipt in Costs
Whether the AI Parsing model reads images is an admin setting now, and it lands with the two features that send a photo, so it has something to control. The setting is `vision: 'auto' | 'on' | 'off'` on the addon config. On Automatic a local Ollama is asked what the model can do (`/api/show`), and a cloud provider means no; Yes and No are applied as set. The old `multimodal` flag is not read: the admin screens wrote it false on every save, so a stored false meant nothing. The per-user toggle keeps its on/off meaning and is read by the extraction now. The lookup is server-side only: capped read, validated body, never throws, cached per server and model. GET /api/llm/capabilities answers it for the signed-in caller, and both photo pickers below ask it. The booking import takes JPG, PNG and WEBP when the model reads images. The photo goes as an image to every provider (image_url, an Anthropic image block, Ollama's native chat with `images`), capped at 1600px, and skips KDE Itinerary. A scanned PDF, which used to reach the model as empty text, has its first two pages drawn by pdf-parse and sent the same way. The rest is the import as it was: background job, review, the file attached to the booking, the linked cost. On a real qwen3.5:4b a train ticket came back as bare clock times until the router gave the model every type's field meanings, and its price as an object inside a string, which read as 4900; both are handled. Costs gets Scan receipt: a dialog laid out like the booking import's takes one photo, read on the import job queue, then reviewed in the existing expense editor pre-filled with merchant, total, currency, day and lines, the photo waiting in Receipts & Invoices to be uploaded on save. No table, no migration, no category guess, no document-type detection. |
||
|
|
d1dc8542ac |
fix(costs): leave out an expense no rate is known for instead of counting it 1:1
A foreign expense without a frozen rate is converted with today's rate. When the server could not fetch rates itself, it counted the amount 1:1 in the trip currency, although the browser had the rates all along. A VND bill on an AUD trip was settled as millions of AUD. Such a row is now left out of the balances, flows, final budgets, trip totals and per-person summaries. The settlement reports it under `unconverted` and says which currency its figures are in; the trip summary lists the ids and the budget prompt says how many are missing. The browser lends the rates it already holds (`fallback_fx`) when it saves an expense or a settle-up transfer, and to heal existing rows through the new POST .../budget/freeze-rates route, which an editor's Costs view calls for the rows the settlement left out. The server still prefers its own rate, uses a lent one only while it is quoted against the trip currency, and never replaces a frozen or explicit rate. The settlement also takes the browser's display rate (`base_rate`) in place of a display quote the server cannot fetch; the browser leaves out a figure the query would refuse. The browser's own fetch gives up after 10 s. Changing a row's currency while no rate is available no longer keeps the old currency's rate; the row goes back to unfrozen. MCP gets the same through the new freeze_budget_rates tool, which only ever pins the server's own rate. |
||
|
|
2a8071f642 |
feat(plugins): search plugins can answer the place search as you type
A searchProvider plugin can now implement an optional suggest() beside search(). Its places then show up in the place search's typed-ahead list, after the core suggestions and marked with the plugin's name, on desktop and phone. An index that cannot take a request per keystroke leaves it out and keeps answering the full search only. The child reports which functions each hook object carries at load, so GET /api/plugin-search/suggest only asks the plugins that implement suggest: from the second character, three rows at most across all of them, 800 ms to answer. The feed marks plugins holding hook:search-provider and the client only asks the route while one is installed. A plugin row carries its whole place, so picking it skips the details lookup, which knows no plugin id. The phone's search results now name their source as the desktop ones do, and the desktop dropdown scrolls instead of running past the dialog. Addresses discussion #2221. |
||
|
|
c7c9ba24a3 |
fix(journey): select all takes every photo in the range and imports keep their order
The photo picker's Select all took only the pages already loaded, 50 at a time, and replaced whatever had been picked on another tab. It now loads the rest of a day, trip or date-range search before selecting (cancelled by closing the picker or switching tab), adds to earlier picks, and Deselect all only clears what is in view. Add stays disabled until that loading is done. The whole-library tab keeps selecting what is loaded and shows the count as "N+". A date-filtered Immich search stopped at 1,000 photos without a word, because its page budget was absolute; it is now relative to the page asked for, with an absolute ceiling of 270 raw pages sized so the picker's deepest page still gets a full answer. Adds go out in batches of 500 ids, under the JSON body limit, and each batch is sent oldest first, so photos land in an entry in the order they were taken. When a later batch fails, the photos already saved are counted and the journey reloads while the error still shows, through one hook for desktop and phone. After a provider add, the capture-time lookup runs at most four at a time per user, shared by all of that user's batches. When it fills a row, a journey:photos:updated event reloads open journeys once per batch, from the REST routes and the MCP add tool alike. Device uploads, which arrive one file per request, get the same lookup without the event. Addresses discussion #1587. |
||
|
|
b7f777024a |
feat(i18n): bring the Thai translation up to date with dev
Translates the keys dev gained since the branch was cut, the help center above all, and drops the six keys dev no longer has. Registers th everywhere a language is listed: notification emails, the rate-limit message, the placeholder check and the wiki. The two tests that counted the languages by hand now derive the count from SUPPORTED_LANGUAGES. |
||
|
|
3c036ea25d |
feat(i18n): bring the Slovak translation up to date with dev
Translates the keys dev gained since the branch was cut, the help center above all, and drops the six keys dev no longer has. Registers sk everywhere a language is listed: notification emails, the rate-limit message, the placeholder check and the wiki. The two tests that counted the languages by hand now derive the count from SUPPORTED_LANGUAGES. |
||
|
|
9263421c5e | feat(i18n): add Estonian (et) translation | ||
|
|
33aa1cd52e |
feat(i18n): bring the Azerbaijani translation up to date with dev
Translates the keys dev gained since the branch was cut, the help center above all, and drops the six keys dev no longer has. Registers az everywhere a language is listed: notification emails, the rate-limit message, the placeholder check and the wiki. The two tests that counted the languages by hand now derive the count from SUPPORTED_LANGUAGES. |
||
|
|
b56f335978 |
feat(dayplan): a day can be added with the next date, which extends the trip by one
On a trip with dates the reorder dialog and the days sheet on a phone now offer two ways to add a day: a day without a date at the end as before, or the calendar day after the last date, named on its button, which extends the trip by one day. The new day goes right behind the last dated day, so the days without a date move one place back with their plans, and no existing day or booking changes its date. The server does it in one transaction behind a new dated flag on the day create contract, used by REST, the create_day tool and the plugin RPC alike. It moves the road trip boundaries of the days behind the new one, and announces the day with day:reordered and the grown trip with trip:updated; an insert at a position announces its trip now as well. The next date and the day limit come from one shared rule, so the button never promises a date the server would not give. On the client the add is online only and runs one at a time, so a double click cannot grow the trip by two days. A wiring test that picked the stay question by its position now finds it by its handler, since the delete question sits in front of it. |
||
|
|
2f6f921aef |
feat(dayplan): a day can be deleted from the reorder dialog, after a warning that says what goes with it
Every row of the reorder dialog, and of the days sheet on a phone, now has a delete button. It first asks, and the question lists what goes with the day: planned places stay in the place list, notes and day texts are deleted, bookings stay without a day, a stay that checks in or out on the day is cancelled with its booking and expense (shown in the danger tone), and on a dated trip the later days and their bookings move one date earlier or the trip ends a day earlier. The server side moved from a bare DELETE into DayRemovalService, which REST, the delete_day tool and the plugin RPC all use. It closes the numbering, keeps the dates on their positions like a reorder, re-stamps the moved bookings, shifts the road trip boundaries, cancels touching stays through the accommodations domain, refuses the last day of a trip, and fans out day:deleted, day:reordered and trip:updated so collaborators renumber and see the new day count and end date. The trip query moved into a leaf file so the days domain can read it without a module cycle. On the client the delete is online only, runs optimistically through the days slice with the same reducer collaborators apply, and the ConfirmDialog gained a slot for extra content, an Escape that only closes itself, and the danger and accent tokens. |
||
|
|
85b5756e89 |
fix(client): the audit round over the 4.3.1 release
Ride minutes and late marks follow an edited timetable instead of the routing that fetched them; a landing day's join band reads as the ride it is rather than a nine hour drive of no length; the refuel offer is there on the road out of a terminal or a pick-up desk; the phone's stage map draws the ride; the share page numbers its stops the way the planner does; the PDF lists a day the way the day plan lists it, from both shells; a poster-less clip draws its play badge everywhere, not only in the editor; the hero title has room for its descenders and the list card keeps its badge clear; the release notice keeps its close control in reach when it stacks; a link chip's actions are thumb-sized and its delete asks first; the Google retry line and the Google-only switch appear only where Google answers. |
||
|
|
e12ae950df |
feat(places): send a search to Google, once or always
Since 4.3.0 the TREK index and OpenStreetMap answer every search first and Google is only asked when they find nothing, so a list with the wrong place on it never reached Google, whatever key the instance had, and the provider choice only decided who fills the empty case. Two ways to change that, both only where Google holds the key slot. Under a result list that did not come from Google, one quiet line sends the same query to Google Places alone (`provider: 'google'` on the search contract, the MCP search tool takes it too). And beside the key, an admin switch "Search with Google only" hands every search and every suggestion to Google and asks nothing of the index or OpenStreetMap; off by default, read by the search after the keyed provider so an install without a key, or one on Amap or OpenStreetMap, is untouched. The panel says so beside the switch. Both shells share the one rule for when the line is offered (placeSource), and the wiki names both switches. |
||
|
|
ed18c37903 |
fix: the findings of the final 4.3.0 audit
Every file the release touches was read once more, and what survived an independent check against the code is fixed here, with a test for each. Data and credentials - A document sync connection edited to a new address no longer carries the stored token along: a blank form under another origin is refused, the same rule the probe route already had. Dawarich and AirTrail testConnection send the stored key only to the host it was stored for. - The key rotation script now covers document_connections.secrets and trip_document_links.webhook_secret, which it had skipped. - Turning a booked night into a pause in the road trip popup asks first and says that the booking and its expense go with it. Deleting a place says the same when a night is booked there, on the desktop and on the phone. - A moved version marker over unchanged bytes no longer replaces the TREK file and its attachments; a real revision carries the attachments over. Planner - A drop in the day plan names the row it lands ahead of in the full day, so a hidden hotel stop no longer shifts it one slot early; the store sorts before it splices. Via re-anchoring, the booked-night guard and the sidebar's day membership read the stored day, the index space the server uses. - A hotel stop placed mid-day re-pins the road trip vias of that day, and the booking session receives the new stop order too. The Public API and the iCal feed list a booked hotel once. A plugin can no longer write a stay against another trip's day. Road trip and map - The route overview paces its requests to the public routers and retries a rate limit, and it says which legs it could not route instead of quietly reporting a shorter trip. A click on a via handle no longer drops a second via; handles hide when zooming out. Read-only members cannot edit the driving limits. The phone shows a way back when no day is picked and prints the night drive departure in the chosen clock format. Integrations and search - Google transit keeps conventional trains when the train chip is on. TREK_PLACES_ENABLED=false stops the details and enrichment calls too, and accepts the whole boolean family. A caller-supplied description no longer lands in the instance-wide enrichment cache. Nominatim searches carry a timeout. An OSM place id is checked for shape before it reaches Overpass. - Dawarich puts a stay on the trip whose dates hold it, the Atlas hand-off knows what the Atlas already shows, and a host change with a new key clears the old instance's state. - Document sync: a webhook address pasted by hand works on the token alone, deleting a connection unregisters its subscriptions, Nextcloud under a sub-path connects, Synology answers a name collision before uploading and verifies a move into the bin, Paperless titles are capped at 128 characters, a DNS failure reads as unreachable rather than as a policy verdict, videos from the store are accepted like uploads, and remote_missing has a way out. Conflict buttons only for the owner, a refused choice is reported, a refused credential gets a Reconnect button, disconnecting asks first on the desktop as it does on the phone, and the folder picker no longer spins forever. Release - The server holds the release notice back from a bundle that cannot draw it, so the previous shell served from the service worker cache after an update no longer shows bare keys and no longer lets a click use the notice up. - Import into an existing list gets the same body limit as a new list. Chat images say when the limit is reached and the new controls are translated. The transit backend switch reports a failed save. The Helm chart carries the Amap credentials in its Secret; the shipped templates document the Places API and Amap variables and the Overpass default. The wiki follows. |
||
|
|
1e35a99c11 |
fix(memories): search the photo provider by the local day, not the UTC one
"This day" sent the entry's calendar date to the provider as both bounds, and both providers turned that date into a UTC window — Immich by concatenating a Z suffix, Synology by relying on a date-only string parsing as UTC midnight. For a reader at UTC+10 the searched window was 10:00 on the day to 09:59 the next morning, so every breakfast photo was missing while the next morning's turned up in its place, and nothing said the day was incomplete. The date headings had the mirror defect, filing a 07:32 photo under the previous day. Immich publishes the right anchor already: localDateTime is required on every search result and is what its own timeline groups by. The window is padded by a day on each side and the page narrowed again by each photo's own local date, carried through as localTakenAt; a server that does not send it falls back to the timestamp used until now, so nothing changes there. Because the padding means raw rows and answered rows are no longer the same thing, the page is filled server-side until it holds what the caller asked for, bounded so a pathological album cannot spin. Synology has no local stamp, so its anchor comes from the caller as a UTC offset, resolved for the day being searched rather than for today — a day looked up across a DST boundary would otherwise start an hour out. The MCP tool takes the same optional input, defaulting to the behaviour it has now. The picker's pagination sentinel moved out of the non-empty branch: a page the new filter empties has to keep paging, and it now says so instead of claiming there are no photos. Closes #2336 |
||
|
|
9de8490176 |
style(docsync): drop dash punctuation from the added comments and test titles
The lines this branch adds used em and en dashes as sentence punctuation across comments, JSDoc, CSS comments and test titles. Each one is rewritten with a comma, a colon, parentheses or a second sentence, keeping what the sentence said. Only lines this branch adds are touched; everything that already exists on dev stays as it is. No logic, error code or logged message changes. The only string literals affected are describe and it titles, which now read "X: topic" like the rest of the provider tests. |
||
|
|
b88b92add7 |
fix(docsync): a paused binding no longer resets its own settings
PATCH links validated its body with a schema derived from the create contract through .partial(). In Zod 4 a .default() still fires inside an optional field, so a patch naming one setting arrived with every other one filled in: pausing a pull-only binding switched it to two-way, reset both policies and blanked its label. The update contract is now written out without defaults, and an e2e test drives a patch through the real pipe, which the controller unit tests never see. Also, the cosmetic leftovers from the review: - POST connections/:connectionId/scopes no longer wants the connection in the body as well. It was required, never read, and let one request name two connections; a client that still sends it has the key stripped rather than refused. - createLink loses the scope argument every caller passed as null. - The controller's owner check is now assertCanManage, because it lets instance admins through just as canManageDocSync does on the client. Who may manage a binding is unchanged. - document_provider_fields drops settings_key and payload_key. Nothing read them: the connect form takes its fields from /docsync/providers and the admin shelf sends none for document providers. - The e2e test imports the provider classes under their new names. |
||
|
|
2d7cb124b3 |
feat(documents): two-way sync with Paperless, Papra, Nextcloud, OpenCloud and Synology
A trip can be bound to a folder, tag or space in a self-hosted document store, and documents then move in both directions: what someone uploads in TREK appears there, and what someone files there appears in TREK. Three decisions shape the rest. The connection belongs to the TRIP, not to a person. Every other integration here is per user, and photos are shared only through an opt-in flag; neither can deliver "everyone on the trip sees the same documents", because visibility would depend on whose credentials fetched a file, and a member without their own Paperless account would see nothing. So the trip owner binds the trip once, the server talks to the provider under that one identity, and TREK's own membership decides who sees what. The provider never learns that TREK has members. When the credential holder leaves the trip the binding goes to `orphaned` rather than quietly carrying on with an ex-member's token. TREK keeps the bytes. A synced document is an ordinary trip_files row, so download, trash, offline cache, PDF export and backup keep working untouched, and a provider outage does not blank out a trip that starts tomorrow. Deletion never crosses the boundary on its own. A document that disappears upstream is recorded as missing and waits for a human, and a run that sees more than 40 percent of its known documents vanish at once refuses the whole listing rather than acting on it: an unmounted share answers exactly like a mass delete. The core reconciles by enumerating both sides, because the weakest provider sets the model. A changed-since query cannot see a deletion, Papra does not move updatedAt when a tag changes, a Paperless bulk edit leaves `modified` alone, and Synology FileStation has no change feed at all. A webhook is therefore only ever "look now", never a source of truth. Echo is caught by comparing content, not timestamps: pushed_sha256 records what TREK itself wrote, so a provider reporting TREK's own upload is not mistaken for a stranger's edit. Provider differences are answered by capability data rather than by branching on an id: stable ids where they exist (Paperless document id, oc:fileid, driveId) and a normalised path where they do not, replace-in-place where the API offers it, and a content hash from the listing only where the provider really supplies one. Admins switch providers on under Admin, Addons, Documents, mirroring how photo providers hang off Journey, including the cascade and the 409 when the parent addon is off. The per-trip setup lives in the file manager, where the person has the context that makes the folder choice obvious. |
||
|
|
15aefa449e |
feat(journey): the marker card, and what testing it turned up
Everything here came out of putting the first commit in front of somebody and watching them use it. **The marker card is one card now, and it carries the entry's photos.** Leaflet showed a one-line label and the GL map a two-line card, so the same marker said different things depending on a renderer setting nobody connects to journeys. Both now build from one module: a strip of up to three thumbnails bleeding to the card's edges, the name, and the place and date as chips. On the GL map the thumbnails open the entry's photos; the rest of the card still never takes the pointer, so the map drags from anywhere on it. **A pin that would not sit still.** Dragging the map left the marker trailing behind and snapping back at the end. The app's press-feedback rule puts a 180ms transition on `[role="button"]`, MapLibre and Mapbox give their marker element exactly that role, and both rewrite its `transform` on every frame of a movement. Markers are excluded now — every map in TREK, not just this one. **The phone card, after four rounds of looking at it.** The day is the card's own edge rather than a bar across its foot, and the ring is inset because the carousel clips. A card without a photo is opaque: it was a wash of the day's colour over the map. The country is a real flag rather than two letters — Windows has no flag glyphs, so the emoji drew "DE" where a flag was meant, and the image falls back to the emoji when it cannot be fetched. Mood, weather and the date share the top edge; the flag sits opposite them. **The day bar answers to a finger.** Press the date and slide sideways to run through the days. The bar is still a row of buttons underneath, because that is the only version of it a keyboard can reach, and each one is 20px tall now rather than 3. Also here: - Videos upload to an entry (issue #2341). The gallery has taken them since #823 and an entry could not: everything went through the images-only filter and came back 400, reported as "1 of 1 photos failed". Same two-part upload, same absence of transcoding, and the editor now previews a clip in a `<video>` instead of a broken-image glyph. - The search box takes the gap the toolbar already had instead of clipping its own placeholder. - Entry cards line up with the day header again: the reorder arrows moved out of a column of their own into the feed's padding and appear on hover, and the day-coloured ring on the active card is a layer over it — an outline tight enough to sit on the card's edge was painting behind it. - Map attribution is collapsed to its ⓘ. It cannot go away — the OSM licence asks for it — but it does not have to lie under the carousel. - The suggestion dismiss and the day-header plus use TREK's own tooltip rather than the browser's. |
||
|
|
c2707b7eff |
feat(journey): the enhancements from discussion #2299
roel-de-vries pitched seventeen changes to Journey. Four were already built or rest on a misunderstanding, one cannot work as described, and three would take away more than they give; the rest are here, plus two things the list did not ask for. **The entry you write on the move now has a name.** Quick capture hid the title field outright and offered a note instead, so every entry caught on a phone arrived nameless and a day's timeline read as a column of identical placeholders. One line of input; it is the single change in this branch that users will feel most. **The map stops yanking.** Stepping through the timeline called flyTo with a zoom floor of 12, so reading a journey from a country view snapped to street level on the first scroll and stayed there — every marker filling the screen, and no sense of where anything was. Both renderers pan now and leave the frame the reader chose. The initial fitBounds still decides how close a journey opens. **The phone timeline is photos.** A 64px thumbnail beside three lines of text spent the card on a story nobody reads at that size and truncated the title after a few characters. The picture is the card now, with the name and place on it, the country's flag and the date in the corners, and the day carried as a colour rather than as a number nobody was reading. Above the carousel, a segment per day: it says how long the journey is, how far in you are, and takes you to day nine without swiping past days three to eight. Everything else, briefly: - A plus in each day header, starting a draft on that day instead of on today. - A journey opens on today when today is part of it, and at the beginning when it is not — a journey that ended last spring is something you read. - Suggestions can be waved away one at a time. The row survives so the trip sync does not offer the same place again; journey settings brings them all back. - Mood, weather and the pros/cons list can be switched off per journey. Nothing written is erased — the form stops asking, the values stay. - The weather fills itself in at the desk the way it already did on the phone. The date picks the source, so writing up last Tuesday gets last Tuesday's. - Enter opens the next pro or con, below the one you are in. - The verdict moved behind the story's fold, and the fold itself came in from nine lines to four. - The external picker lost the count it was printing twice and some padding it could not afford inside the editor. - The phone can reorder entries within a day at last; the desktop arrows finally have translated labels. - Marker labels are off where a carousel already names the entry, and on where they are the only name a marker has. - Search over a journey's titles, stories, places and tags, accent-blind. - An entry knows which country it happened in, resolved once from its coordinates against the bundled boundaries. Not built, and why: the photos-for-markers idea needs clustering first or thirty markers become soup, and the active entry's photo is already on screen in the card below. Removing mood, weather and the verdict outright would take them from everyone who uses them, so they became switches. `capture` on the file picker takes the photo library away on a phone, which is why the camera is a second input; it is hidden on desktop now instead. The suggestion strip in the new-entry flow would be a third surface over the same data — tapping a suggestion already opens the editor filled in. The one gap the list missed and this branch does not close: Journey is the only domain with no offline path. That belongs in its own change. |
||
|
|
edf0538260 |
fix(roadtrip): keep a booked night out of the day list, and off the traveller's stops
Three things the first pass got wrong. A stop a booking puts on the day is for road trip mode, which needs the hotel in the driving chain. The day planner already shows that booking as its own overnight block, so the row there would be the same hotel twice. It is hidden in Days whatever the service-stop switch says, and the switch goes back to meaning what it says: the petrol stations the traveller added. Cancelling a night from the road trip popup took the stop with it. That control means "this is a pause now, not a night", and the stop sits mid-drive where re-adding it would land it at the end of the day. The booking now hands the stop over instead of deleting it. Moving a booking whose stop belongs to the traveller put a second one on the new day and left the first behind, which is the duplicate this change exists to remove. Only a booking's own stop moves now. Stays booked before this shipped get theirs from the migration, not from the next edit they happen to receive. |
||
|
|
dad1316172 |
feat(api): let public API keys be scoped to single areas
A key could only ever read everything a user owns. When a second service reads TREK on that user's behalf, that is more than it needs: Dawarich wants trips, days and places, not notes, bookings or the wish list. Keys can now be minted with an explicit list of areas, checked in the guard and shown when the key is listed. Keys that existed before this keep full read access, so nothing that works today stops working. A key marked as limited whose list cannot be read grants nothing rather than everything. |
||
|
|
bd46089450 |
feat(budget): make a settle-up payment's date editable
The settle-up form only exposed from/to/amount and silently dated the payment by created_at, unlike a regular expense which has an editable date. Add a settled_at column (independent of created_at, same split as expense_date) through the schema, migration, service, REST and MCP surfaces, and the desktop + mobile forms; the ledger groups a payment by settled_at, falling back to created_at for rows recorded before the column existed. |
||
|
|
a9907e2f10 |
feat(plugins): let a plugin answer place searches from its own index
TREK searches its own place index and OpenStreetMap. Neither carries a rating, so "the best rated hotel around here" has never been answerable, and an operator with a better index for their region had no way to reach the search box at all. The searchProvider hook is that way in: a plugin is handed the query, a limit, the language and the coordinate the search is biased toward, and answers with places in the same shape the core search returns, plus a rating. It sits beside the core search rather than inside it. The plugin runtime already imports PlacesModule, which imports MapsModule, so asking the providers from the maps service would close a module cycle — the same reason the other fourteen contribution controllers stand next to what they contribute to. The client asks both at once and merges, at the single point every caller already goes through, so all nine search surfaces get plugin hits without knowing the hook exists. A plugin index may never hold up the list: the client stops waiting at two and a half seconds and the host gives a provider two, which is also why the budget is the shortest of any hook here. With no such plugin installed the route answers without reaching the runtime at all. Every hit is normalized host-side and shared by the REST route and the MCP tool, so a cap that holds for the browser holds for the assistant: coordinates are range-checked, strings capped, the rating clamped to 0..5, a website must be http or https, and ids are namespaced so they can never collide with an OSM one. search_places_via_plugins is the MCP half, because what the user can see the assistant has to be able to see; search_place now points at it. |
||
|
|
9543fdc3f2 |
feat(collab): add shared links, chat images, and plugin fullscreen
Adds a Collab Links panel (add/pin/delete) with translations in every locale so tab and button labels are not raw i18n keys, chat image attachments (up to 4), and native fullscreen on plugin iframes. Includes Zod request DTOs and CollabPanel coverage for the Links tab. |
||
|
|
4a88dbcbcf |
fix(places): say where a result came from, and stop asking questions in the form
Six changes to the place picker and the card behind it, all of them about a panel saying less and meaning more. The TREK place index has no switch in the admin panel any more. It decides whether a search leaves the instance at all, which is a property of the deployment rather than a taste: an operator pins TREK_PLACES_ENABLED=false in their compose file, and it cannot be turned off from a browser by whoever holds an admin account that day. The switch never shipped, so nothing to migrate. The standing "Using OpenStreetMap" notice above the search box is gone. It was a sentence about configuration in the middle of a form about a place, and it answered a question nobody asked while leaving the real one open. Each result now carries a small mark instead: TREK, OpenStreetMap or Google. It sits on the row rather than above the list, because a list can be two indexes interleaved and a heading could only name one of them. The saved-places filters open across the full width of the row. Two of them sit side by side, only one can be open at a time, and list names were being cut at about ten characters for no reason. Three empty states use the mascot the rest of TREK uses. The place details panel no longer tells the reader to go and ask an administrator for a Google key, the saved-places list and the road trip rail no longer explain themselves in two lines of grey text. That needed one new mood, `sad`: `sleepy` reads as falling asleep, which is a different thing from finding nothing. The Kind of stop field is out of the place form. It is a road trip concept, and the road trip view already sets it from the stop's own number, where the choice is next to what it changes. |
||
|
|
1e1dd9b814 |
fix(roadtrip): keep the shaping a traveller did, through every path that moves a stop
A via records which stop it follows by POSITION, so anything that renumbers a day's stops moves the ground under it. The road-trip rail corrects for that; almost nothing else did. reanchor() never renumbered `sequence`. It is allocated per leg and starts at 0 in each, and re-anchoring MERGES two legs whenever the stop between them goes away — so leg A's 0,1 ended up beside leg B's 0,1, and everything that draws the route orders by sequence. The drive ran A0, B0, A1, B1: forward, back, and forward again. Persisted, surviving a reload, and unrepairable except by deleting the vias. The old anchor is read before the updates and used as the primary sort key, so the vias of the earlier leg stay ahead of the ones that merged into it. Reordering the only two stops of a day deleted every via on it. A reorder is modelled as remove-then-insert, and the removal half refuses to treat a two-stop day as still having a drive — right when a stop really goes away, catastrophic when nothing was removed. Two stops is exactly the shape "follow this track" produces, up to nine vias on that one leg, gone to a single drag with no prompt and no undo. Removing a stop, reordering within a day, and assigning a place mid-day never re-anchored at all, though the helpers exist and three other paths use them. The detour then sat on a leg the traveller never chose, or on a leg that no longer existed and vanished — until a later stop brought it back bending a road nobody asked for. Reordering hands a whole new ordering rather than one move, so the anchors there follow the stop they were pinned behind instead of arithmetic on the index, which is correct for any permutation. Duplicating a trip dropped all of it: `stop_type` was missing from the place insert, and neither via nor day-track table was copied. The duplicate looked complete and quietly drove somewhere else. A failed read of the via list emptied it. That read runs after every write and feeds the routing key, so a 502, a dropped connection or a tab going offline re-routed the whole trip along the roads the traveller had steered away from, with nothing said and nothing retried — indistinguishable from having deleted them. Only a 404 or 403 empties now; anything else keeps the last known list and says it may be stale. |
||
|
|
676c82a308 |
feat(roadtrip): remember which track a day follows
The vias already made the day follow the road; nothing said which road. After a reload the badge could only say "shaped by hand", and re-fitting after the stops changed meant finding the track again by eye. One row per day, written in the same transaction as the chain it describes: a day claiming to follow a road whose vias never landed would be worse than a day claiming nothing. Three states rather than two, which is why the field is an object and not an id — absent leaves whatever the day followed alone, null says it follows nothing, an object records the road it now takes. A bare nullable id could not tell "do not touch this" apart from "forget it", and a hand-dragged via on the same day must not wipe the label. The foreign key is the point: delete the imported track and the label cascades with it, so no day is left claiming to follow a line nobody can see. The vias stay, because they are the drive. Both write paths check that the place is a track of this trip before it becomes a label — permission alone would let another trip's place id, or an ordinary place, become a name that can never be drawn. Also clears three reliability findings the scanner raised on this branch: two default sorts given an explicit comparator, and a trailing-slash regex the scanner is right to call backtracking, walked instead. |
||
|
|
67163e7224 |
feat(roadtrip): lay a whole chain of via points in one write
The single-via route is right for a detour dragged by hand and wrong for anything that derives its anchors from a line. Pinning a day onto a recorded track or a signed scenic road means a couple of dozen points, and one request each re-routes the entire trip once per point, spaced by the routing host's own rate limit — twenty anchors would spend half a minute drawing routes nobody asked to see. replace_legs names the legs to clear rather than taking a flag for the day, and that is the whole difference: a traveller who bent two legs by hand and then adopts a scenic road for a third has not asked to lose the two. The tools follow in the same change, as parity requires. Undoing a detour picks this up too, where the vias of one leg were deleted one request at a time. |
||
|
|
e11ae6f056 |
feat(roadtrip): shape the drive — vias, alternatives and stops along the way
The road-trip addon's second half: the parts that let a planned drive be argued with rather than only read. Reshaping. A via records which stop it follows by position, and that position is resolved fresh every time the day is routed — so inserting, reordering or removing a stop moved the ground under every via after it, and the drive silently snapped back to the road it had been steered away from while the handle still sat on the map where it was left. The correction is computed against the day as it stands and written with the same change, in one transaction: a half-applied re-anchoring mixes two numbering schemes in one day. The one judgement that needs geometry — which half of a split leg a via belongs to when a stop lands in the middle of it — is measured on the road actually driven, so a fuel stop added to a bent leg keeps the bend on the side it belongs to. Alternatives. OSRM answers most long legs with exactly one route, which read as "there is no other way" when a perfectly good slower road existed. When it offers nothing, the same drive is asked for again without the motorway, and failing that without the tolls — one question at a time, stopping as soon as one lands. The offer says what it is rather than how much slower. Stops along the way. Everything the corridor search can find is a pause, not a destination: fuel, charging, rest areas, campsites, food and sights all sit inside the leg they were found on, carry their own colour and icon, and are left out of every stop count. That colour is defined once and read by the rail, the corridor list, the map pin and the picker — and it replaced the chains' logos, which turned a corridor full of petrol stations into a row of advertisements and left the brands with no logo on file looking like something else entirely. How long a stop takes was readable in the rail and writable nowhere; new places were given an invented hour that nobody had entered. It is now set from the rail, cleared as a zero because the update statement folds a null into "leave it alone", and new places start with nothing. Both columns are rebuilt on one type scale, anchored to the day plan's own sizes so switching tabs does not change the size of the text beside it. The rail reads as trip totals, day cards and a chain of stops with the drives between them; the corridor column as what to look for, how the search is getting on, and what it found. Six category pills over three lines became one dropdown, which is the space the results wanted. Also: the offered routes are framed on the map when the picker opens (`fitKey` carries no coordinates and each renderer reads it as "the selected day"), road trip mode does not cluster at all so the whole drive stays readable zoomed out, and the drive-time labels finally render in the app's own typeface instead of Leaflet's Helvetica. All of it in both renderers. |
||
|
|
ed5769fc89 |
fix(maps): name the index as the source, and stop it being asked twice
Attribution first, because this branch argues the point itself: the card names the sources with the comment that naming them is a licence obligation rather than decoration, and the copy beside it promises that every field says where it came from. The POI path then stamped Overture rows as OpenStreetMap. Overture is not OpenStreetMap — it carries OSM among other sources under other licences — so that is a wrong attribution rather than a rounded one. The wire contract keeps `source` an open string and nothing on the client switches on it, so saying `trek-places` costs nothing. The MCP tool description no longer calls itself an OpenStreetMap tool either, and says which source answered. Adding one place cost two round trips to the index. The details lookup fetches the place and its answer carries the description; toPlaceRecord dropped that field, so the enrichment fetched the same place again a moment later to read it. It is carried through now and read from what the caller already has, with the second lookup left as the fallback for when there is nothing to read. That description's source URL becomes an href on the client and was taken on `typeof === 'string'` alone. It now goes through the same allow-list a place's website goes through — the file already does this for menu_url, with the comment explaining why — so anything but http(s) loses the link and keeps the text. The instance token was hashed from raw APP_URL, which the shipped compose file leaves commented out. Every default install therefore sent the same token, and a token every caller shares is not a caller: one instance running a large import would rate-limit all the others, who would fall back to Nominatim with nothing to point at. getAppUrl() falls through to ALLOWED_ORIGINS and then to the port, so unconfigured instances still differ. Two client-side leaks. A pick made out of the offline cache was recorded into the shadow corpus, where it moved the two figures the corpus exists to measure — and offline is a setting, so the network is usually reachable and the row landed. And the prefetch never re-checked the session after its request came back: a logout in that window repoints the handle at the anonymous database, so one person's trip area was written where the next person's offline search reads. The cache path also ignored the abort signal, so an answer for a keystroke two keystrokes old could overwrite a newer suggestion list. It rejects the way both surfaces already know to swallow. Left alone, and said in a comment rather than hidden: the index cannot localise. The Overpass path picks name:<lang> and falls back to int_name; the service has no language parameter at all, so a German reader exploring Tokyo gets the Japanese primary names on this path. |
||
|
|
48f0f29273 |
feat(admin): let an operator switch the place index off
trek_places_enabled was read by six call paths — search, autocomplete, the POI pills, booking-import geocoding, the offline area and details resolution — and written by nothing. No admin route, no MCP tool, no switch anywhere in the UI, and the key is not in the settings allow-lists either, so the only way to set it was an INSERT against app_settings by hand on the server. That matters more than the four switches beside it, which trade away quota. This one decides whether every search and every keystroke in the place box leaves the instance at all. The flag reads fail-open, so an install that upgrades without visiting the admin panel starts sending them, which is the right default and exactly why the operator needs somewhere to say no. Adds the pair symmetrically to place-shadow from the same branch: getTrekPlaces /updateTrekPlaces on AddonsService, GET/PUT /api/admin/trek-places with an audit row, and a real switch on the card. The read stays fail-open so it keeps agreeing with MapsService.trekPlacesEnabled(), and a test pins that agreement across every stored value — if those two ever diverge the panel shows "off" while the queries keep leaving, which is worse than either default. The card said this itself: `available` was a display-only prop whose comment read "False until the client is wired to the service; the card then says so instead of offering a switch that would do nothing." It is wired now, so the prop is a switch. Its two badge strings had been shipped to all 23 locales and rendered nowhere; they make way for the switch label and a line naming what answers while the index is off. |
||
|
|
5aa36aaf3d |
fix(maps): stop the index costing a second search, a duplicate and a lost cache
Six things the merged search and the offline cache got wrong. An index failure took the OpenStreetMap answer down with it. Only the OSM half of the Promise.all had a catch, so a rejection on the index side discarded ten results that had already arrived and sent the same query to Nominatim a second time, behind its 1.1 s process-wide throttle. The index refusing a query is ordinary traffic rather than an outage — a common single word without coordinates is turned down upstream as too expensive — so this fired in normal use. Each side now catches its own failure, and the keyless fallback reuses the answer OpenStreetMap already gave instead of asking again for a list that cannot have changed. Dedup never fired for a non-Latin name. namesOverlap splits on [^a-z0-9], so 長谷寺, Кремль and مسجد الشيخ زايد all tokenise to nothing and the function returned false — the same place came back once from each source, one row under the other, and picking either gave it a different id. That is the corpus this whole feature was measured on. Two names with no Latin words are now compared strictly, folded through NFKC so the width variants the sources disagree on still match; sharing a character is deliberately not enough, because deduping 東京タワー against 東京タワー駅前郵便局 would swallow a real result. expand=1 answered nothing for an index place while expand=0 answered in full. The expanded lookup degrades OSM ids to the plain one and had no branch for the new family, so the richer request was the poorer one, over REST and MCP alike. The bias circle could exceed Google's 50 km ceiling. A trip spread over more than that produced a radius Google rejects outright, turning a search into an error toast. Clamped on both sides — the client because it draws the box, the server because the radius arrives over the wire. The offline cache was rebuilt on every sync and never deleted. syncTrip wrote its meta row with put(), which replaced the whole record and dropped the fingerprint saying the cached area was current, so every login, reconnect and manual sync re-downloaded megabytes that had not changed. And clearTripData never touched areaPlaces, so a trip switched off for offline use kept answering offline searches and its rows stayed for the life of the install. clearCachedPlaces, which claimed to do that job, was called from nowhere; its only test asserted with .every() on an empty table and passed on nothing. Picking an offline suggestion ended in an error toast. The details lookup had no cache path, so it failed and the callers fell back to searching for "name, address" — which the cache matches on the folded name alone, so it found nothing for a place that was sitting right there. A miss stays a miss. |
||
|
|
c0eb1c6e38 |
fix(maps): send the location bias the search route already accepted
MapsSearchDto validates a locationBias and MapsService passes it through, but no caller ever sent one. That gap is the whole difference between the numbers in this PR and what the software does. Measured against a live instance over the 127 places of a real trip, searching each by name: without a hint 50.0% -> 54.8% top-5 with a hint 50.0% -> 73.0% top-5 (this PR reports 72.7%) mess/eval_merge.mjs sends lat/lng on every index call and a viewbox to Nominatim; the client sent neither. Without coordinates the index refuses common queries as too expensive and contributes to 41% of searches instead of 100%, so the merge degenerates to Nominatim alone. The hint has to be NARROW, which is the part that is easy to get wrong. Same corpus, same instance, varying only how the box is derived: no hint 54.8% top-5, 38.1% at rank 1 box around the whole trip 57.1% top-5, 17.5% at rank 1 box around the nearest few places 70.6% top-5, 34.9% at rank 1 A box spanning a round trip has its centre in open country between the cities. It points at nothing and costs more than half the rank-1 hits. useLocationBias therefore prefers the day being planned, falls back to the trip only while it still fits inside one region, and drops the hint past a 60km radius rather than sending a misleading one. This also replaces the two hand-rolled copies of the bounding box in the planner dialog and the phone sheet, which shared a defect: Number(null) is 0, so a place without coordinates entered the box as the Gulf of Guinea and stretched it across the planet. |
||
|
|
87a765b763 |
feat(offline): cache the trip's area so place search works with no network
An offline instance had the trip's own places and nothing else, so "find a pharmacy near the hotel" had nothing to answer from: every lookup went to a server. The index can hand over a bounding box in one request, so it does, once, next to the map tiles. A trip to Rostock is roughly a megabyte, refreshed only when the trip's area actually moves. Search and autocomplete fall back to it at the API layer, which is why all ten call sites got the offline path without any of them learning about it, and the answer says `source: 'offline-cache'` rather than pretending to be live. Country packages stay what they were: a SQLite file for anyone who would rather run their own copy of the service. A browser cannot query one without shipping a WASM SQLite, and Germany alone would not fit in IndexedDB, so that door stays where it belongs. /v1/packages lists them, because "you can leave" only means something if the door is visible. |
||
|
|
b3d69aa760 |
feat(places): record which search result was picked, on this instance only
The corpus behind "is our own index good enough to replace Nominatim". When somebody searches and then picks a result, the client posts the query, the rank and the pick. Nothing else: no user, no trip, no session, and coordinates only to three decimals. Deliberately decoupled from the search endpoint. Correlating a response with a later pick would mean handing out a ticket on POST /api/maps/search and threading it through all fifteen call sites; the client already holds everything the row needs at the moment of the pick. Evaluation happens offline afterwards: export the corpus, run it against a candidate index, count how often the pick came back in the top five. That is why collection can start before such an index exists, which is the whole point, because the corpus needs weeks of real use and the index needs a server, and those two waits should not be serial. Off unless an admin switches it on, and it reads === 'true' rather than !== 'false'. The three older places switches fail open because a migration backfilled a row for installs already using them; nothing uses this one, so an instance that upgrades and never visits the admin panel logs nothing. Reading is admin only, retention is 180 days, and the switch state is on the unauthenticated app-config endpoint on purpose: whether an instance records what its users search for is something a visitor may know before logging in. |
||
|
|
2a0ceb2687 |
feat(transit): let an admin route transit search through Google
Transitous has no GTFS coverage for much of Asia, so a search between two Osaka subway stops finds nothing — the stops are not in the feed at all. Add Google as an optional backend behind an admin setting, reusing the install's existing Places key, and fall back to Transitous whenever no key resolves. Uses the Routes API rather than the legacy Directions endpoint the request named: every other Google call here already speaks the v1/X-Goog-Api-Key family, and Google closed the legacy endpoint to Cloud projects created after March 2025. Google returns walking as turn-by-turn navigation steps where MOTIS returns one leg per walking segment, so consecutive walk steps are coalesced into a single leg — otherwise a finely-sliced walk pushed a real journey past the 20-leg cap and the whole itinerary was dropped. Both responses now name the backend that answered. The fallback is silent by design, which left an empty result ambiguous between "no data here" and "the provider you picked never ran", and the admin panel warns when Google is selected but no key resolves — or when only the admin's own key is set, which serves them and nobody else. Cost shaped the implementation, since Google bills where Transitous does not: both field masks are cut to the cheapest SKU tier that still answers the question, cache TTLs are longer than the Transitous path's, and route alternatives ride along inside the already-billed request. Addresses #1699 |
||
|
|
3e532aab99 |
fix(planner): never link an offline temp place id to a hotel booking
The envelope fix routes the created place through the store, which offline
mints a NEGATIVE temp id: reservations have no offline write path, the
mutation queue rewrites temp ids in a URL but never inside another entity's
body, and day_accommodations.place_id carries a foreign key. So a
forced-offline save (the Settings toggle sets the flag while the network is
still live) reached the server with place_id: -1758..., failed the FK inside
the insert transaction and rolled the whole booking back into a 500 — where
dev merely saved it unlinked. Worse, the temp place is in `places` by then,
so every retry matched the same negative id and the booking stayed unsavable
for the rest of the session.
Offline the function now links nothing at all and skips the geocode too: the
booking cannot be written either way, so minting a place would only leave an
orphan behind on the next flush. The name-match arm rejects temp ids for the
same reason. `as never` is gone from the addPlace argument, and
placesApi.create finally declares its `{ place }` return type — an untyped
`r.data` is what let the envelope be misread in the first place.
|
||
|
|
55f14c6a70 |
fix(offline): the PWA starts offline again and the download does what it says
Four separate defects sat behind "offline mode is broken". The app shell. On the first online launch after a version bump, App.tsx deleted EVERY Cache Storage bucket and unregistered EVERY service worker, then reloaded. That is exactly wrong for a Workbox autoUpdate build, which swaps its precache atomically on install and never leaves an empty one. Between the purge and the completion of a fresh 22 MB precache the origin had no shell at all; anyone who closed the app or lost signal inside that window could no longer start the PWA offline. It also threw away the map tiles and documents the user had downloaded on purpose. It now asks the worker to fetch the new build and reloads once it has taken over. The version marker also moves inside the guarded path, because it was written after the purge and outside its try: a throwing setItem left the caches gone, the marker unwritten and the reload unreached, repeating on every launch. The reported version is validated before it reaches this device's storage and before it is compared against the stored marker, so a value that is not a release tag can neither be persisted nor start an update on every launch. The session. loadUser's offline branch kept the persisted session but never called onAuthSuccess, the only caller of setAuthed(true), and the mutation queue's flush, syncAll and prepareForOffline are all gated on it. A session that launched without network could therefore never sync again for the rest of its life, even after the signal returned: the offline settings buttons spun and returned instantly having done nothing, and queued edits never uploaded. It also left reads pointed at the anonymous database instead of the user's own. The date rule. A trip is only cached when it has not ended, and one that ended over a week ago is actively evicted. The per-trip switch was an opt-out list, so ticking a finished trip changed nothing at all. For anyone whose trips are all in the past, "Download for offline use" stored none of them, reported success, and the cache stayed at zero. Switching a trip on now pins it, which overrides both the skip and the eviction, and the switch shows the honest position rather than promising storage that never happens. What the screen says. A sync that never started (already running, offline, signed out) was indistinguishable from one that stored everything: the progress bar painted full and a green "Ready for offline use" appeared over a run of nothing. Both syncs now report why they stopped and the screen says so. A rejected Dexie read no longer renders as "No trips cached yet", and the clear button stays reachable when the database holds data that no bookkeeping row points at. A run that rejects outright says so too: the trip list it starts from is a plain fetch, so a network blip took the whole thing down and left the last progress frame on screen looking like a finished download. The 401-with-HTML branch in the API client now confirms a reachable proxy before tearing the worker down, since several of TREK's own routes answer 401 with a text/html body. Both settings shells move onto one shared hook rather than growing the same logic twice, and the tile prefetch caps its depth when the browser refused persistent storage. Opaque tiles are billed at about 7 MB each, and an origin that is not exempt from eviction loses its whole bucket, precached shell included, which Workbox then never refills because sw.js has not changed. Closes #2228 |
||
|
|
b82962f95b |
Feat/plugin instance actions (#2209)
* feat(shared): scope plugin action descriptors * feat(plugin-sdk): instance-scoped settings actions * feat(plugins): run instance-scoped actions from the admin settings dialog * feat(admin): instance-scoped plugin actions in the settings dialog * fix(plugins): surface instance actions without instance fields and harden the action gate * docs(plugin-sdk): note that hosts before 4.2.0 ignore action scope * fix(admin): open the dialog for instance actions, explain inactive 404s, guard the save window * docs(wiki): plugin pages for instance actions and the admin panel changes |
||
|
|
a5d87d295a |
fix(settings): the offline language mirror does not outlive the account (#2146)
The mirror of the account language stayed in localStorage past logout, so the next account on the device booted in the previous user's language forever. It is cleared on logout and rewritten on login, and the rate-limit message follows the same order the boot language does. |
||
|
|
b6a86275f6 |
fix(notifications): the smtp test names its failure and the log carries it (#2196)
testSmtp logged nothing and the transport ran on nodemailer's two-minute defaults while the client gave up after eight seconds, so every failure was a generic toast over an empty log. The transport gets real timeouts, every outcome is logged without credentials, and a classified, scrubbed cause (auth, DNS, refused, filtered port, TLS mismatch, certificate) goes into the response the admin sees; the channel test calls wait long enough to receive it. The password field also rendered its mask as value, so replacing a stored password silently saved the bullets as part of the new one; it is a placeholder now, on desktop and the phone admin. |
||
|
|
03bd5c77af | fix(weather): day-local named anchor, cache ttl and past-days forecast window (#2167) | ||
|
|
c21e86e3bd | feat(planner): surface and edit day-assignment notes across desktop, mobile and pdf (#2163) | ||
|
|
394201cd32 |
feat(plugins): instance-wide settings editor in the admin UI (#2170)
* chore: bump version to 4.1.0 [skip ci] * fix(collections): stop findDuplicateCollectionPlace merging places by coordinates alone Three places answered "is this a place the trip already has?", not two: PlacesService.findDuplicatePlace and isPlaceDuplicate got the shared strategy list in #2130; CollectionsService.findDuplicateCollectionPlace was missed and kept its own hand-rolled copy of the pre-#2130 order. Unlike findDuplicatePlace, this one had no isPlaceDuplicate() guard in front of it — savePlace calls it directly — so the coordinate fallback for a NAMED candidate was live, not latent: two distinct places sharing an address (a restaurant and the bar upstairs) would report each other as duplicates the moment their coordinates round to the same ~11 m box, regardless of name. It also never read google_place_id/google_ftid/osm_id at all, even though every collection_places row stores them, so a place renamed since it was saved could be saved again instead of being recognised by its provider id. Now walks placeMatchStrategies() from @trek/shared, same as findMatchingPlaceId: provider id, then name, then coordinates only when there is no name. savePlace passes the provider id fields through since they are already on the request body; the two bulk call sites (importablePlaces, saveFromTripPlaces) are unchanged, since their source rows don't carry provider ids to pass. Closes #2137 * fix(collections): ask the same question at all three call sites savePlace was not the only caller of findDuplicateCollectionPlace. The bulk copy already carries google_place_id/google_ftid/osm_id into the row it writes but asked without them, and the import picker never selected them at all, so a renamed place was recognised on one path and not on the other two. The picker answering differently from the import is the drift the method exists to prevent: a row shown as new would come back refused. COLLECTIONS-SVC-102 and -103 cover the bulk copy and the picker. * fix(map): give the Leaflet map its own zoom ceiling, so a vector basemap cannot take the planner down Opening a trip on 4.1.0 could land on the error boundary with "Map has no maxZoom specified", and so could the map settings tab, which left no screen from which to pick a different basemap. Leaflet answers getMaxZoom() from the map options first and otherwise from a layer that brought one, and only a GridLayer ever contributes via its beforeAdd hook. A vector basemap is a GL canvas on a plain L.Layer, so it contributes nothing, and MapContainer set no ceiling of its own. MarkerClusterGroup.onAdd refuses an infinite one by throwing, before any clustering, so an empty trip hit it too. That path opened when the default basemap became a MapLibre style. It reaches further than an operator choosing OpenFreeMap: resolveTileUrl sends a keyless CARTO template to the app default, which is now a vector style, so every instance still carrying the old CARTO basemap fell into it on upgrade. The ceiling belongs on the map rather than on the base layer, because it has to hold whichever of the three branches renders. 19 matches what the raster and satellite layers already carried, so nothing changes for the maps that worked. SharedTripPage gets the same treatment: it has no cluster to throw, but it draws the same vector basemap and its fitBounds asks for a ceiling. mapZoomCeiling.test.ts pins Leaflet's own semantics against the real library rather than a mock, since a mocked react-leaflet would have kept passing through all of this. * fix(map): put the CARTO key back on the template the settings preview draws The map settings fields hold what the user is editing rather than what useTileUrl already resolved, so the preview received a bare template with no key on it. resolveTileUrl then read it as a keyless CARTO url and did what it is meant to do with one: fall back to the app default. The preview drew OpenFreeMap while the fields said CARTO, whether or not a key was saved. Same reason the tab crashed for anyone on a CARTO basemap even after entering a key, which is the loop that made this hard to escape: the key is entered on the page the fallback had taken down. FE-COMP-MAP-033b pins the template the preview is handed, with and without a key. * fix(admin): put the CARTO key back on the default-settings map preview too Same shape as the user-facing map tab, and reported alongside it: the field holds what the admin is editing, so the preview resolved a keyless CARTO template and drew the app default instead of the basemap being set. * fix(mobile): put the CARTO key back on the phone map settings preview too The third preview with the same shape, and the one that matters most while the crash is live: the phone layout is where someone would go to change the basemap when the desktop tab will not open. * fix(calendar): a fully timed stay is its two hand-overs, not a week-long bar Closes #2136 A stay was always exported as an all-day block across its whole range, and the check-in and check-out markers came on top of it. When the stay records both ends of its clock the markers already say everything the block does, and the block is the half nobody can act on: it takes a week of calendar to repeat what two one-hour events state precisely. Same shape as the parking and rental windows in #2068. Only where the markers actually stand in for the booking, though. They are emitted once per stay and titled from its lowest-id reservation, so a second room on the same stay would otherwise be left with nothing at all. A stay that knows only one end keeps its block too, since nothing else carries the other end's date. CAL-025b covers the drop, -025c the one-sided stay, -025d the second room. * fix(mobile): the phone admin map preview needs the CARTO key too The fourth preview with the same shape, missed when the other three were fixed. Sonar pointed at it: the desktop and phone admin pages are near-identical, so the duplication report named the file the change had not reached. * chore: bump version to 4.1.1 [skip ci] * feat(plugins): admin editor for instance-wide plugin settings The scope:'instance' settings a plugin declares had API endpoints but no UI. - shared: plugins.schema.ts — the settings-field descriptor and the admin instance-config GET/PUT wire contracts, imported by both sides - server: GET /api/admin/plugins/:id/config now returns the declared fields alongside the (masked) values; PUT re-spawns an ACTIVE plugin (its child reads config once, at init) and reports it via 'restarted'; the admin list carries instanceSettingsCount so the UI gates its menu item without a fetch - client: 'Instance settings' row action opening a form modal (desktop) / MSheet (phone) in both admin shells — checkbox/select/secret rendering as in the user settings form; an untouched secret mask is never sent back - i18n: three new admin.plugins.* keys in every locale * refactor(plugins): move the admin body contracts to @trek/shared The remaining plugin DTO schemas (install, link, activate, uninstall, retrust, update, egress-hosts, user-settings update, route) now live in shared/src/plugins/plugins.schema.ts with their inferred types, alongside the instance-config contracts. The journey-contract doctrine notes (#1842) moved with them — each body stays deliberately loose so the handlers keep the rejections and fallbacks they already own, and new spec tests pin that looseness so a well-meant tightening breaks a test that explains itself. plugins.dto.ts is now thin createZodDto wrappers over the shared schemas (same class names, no controller changes), and the client's pluginInstall types its options and body against PluginInstallRequest. * fix(admin): offer Allowed hosts only to plugins that declared operatorEgress The row's egress chip was already gated on the declaration, but the kebab menu (desktop) and row action sheet (phone) offered Allowed hosts on every installed plugin — inviting the admin to widen egress for plugins that never asked for it. Both entries now render only when the plugin declares operatorEgress; the dialog's unsupported-notice fallback stays as the truth-teller for stale list data. * test(admin): close the Sonar gaps on the instance-settings form The PR gate flagged new-code coverage (76.4%) and duplication (10.9%). - The two identical 39-line handler blocks were logic, not markup — they move into one shared useInstanceSettings hook; each shell keeps its own markup (the deliberate desktop/phone divergence stays) - New tests per shell cover the untested branches: every field type renders and round-trips into the save payload (select, checkbox/MToggle, number, hint, no-label key fallback), a failed fetch is a toast, a rejected save shows the server reason and keeps the edits, and the dialog/sheet closes without saving - INS-009 covers the controller's no-body '|| {}' fallback --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: michael-bohr <263459669+michael-bohr@users.noreply.github.com> Co-authored-by: Maurice <mauriceboe@icloud.com> |