Commit Graph

1482 Commits

Author SHA1 Message Date
Maurice 71cc97822a fix(ci): the second round of the quick-win checks on the server
oidc_auto_redirect is managed-locked in the registry too, costs_mode is read
as the mode it is, the days of a dated trip are read through the factories,
and the dashboard's country order moves out of the atlas service into a
module of its own, so the service keeps its size.
2026-11-13 12:57:56 +01:00
Maurice 721a610a70 fix(ci): green the quick-win round on the server, the shared contracts and the plugin facts
The trips entity takes costs_mode as the generator writes it, the plugin
output contract publishes it, and the hand-built test schemas carry it. The
new MCP fields come from @trek/shared, the link tool tests join the collab
file that already mocks the database, oidc_auto_redirect is the operator's
on a managed install, and the tests follow the KML service, the dated trip
days and the zod pipe.
2026-11-13 12:40:15 +01:00
Maurice 74af74fb5e feat(atlas): show the most recently visited countries first on the dashboard (#2648)
The place and endpoint reads behind the passport tile now carry the last day
of each started trip, and getTravelStats orders the countries by their latest
visit, with countries only marked by hand after them. The country set is
unchanged; repository, parity and service tests pin the order.
2026-11-13 10:17:26 +01:00
Maurice 924b4e0787 feat(auth): let admins keep the login page on OIDC-only instances (#2925)
A new app setting oidc_auto_redirect (default on) is part of the admin form
and the public app config. Off, an instance without password login no longer
sends visitors straight to the IdP: the login page shows its SSO button,
also while the cached config is read. Admin toggle on desktop and phone.
2026-11-13 10:17:26 +01:00
Maurice d5d9428cf2 feat(calendar): let a feed leave out the per-day summary events (#2602)
Both feed routes and the one-off export.ics take ?days=0, validated by a
shared query contract, and then skip the all-day "Day N" event per day; the
default is unchanged. The subscribe dialog gets a Day overview events switch
that adds the parameter to its links, export_trip_ics takes dayEvents and the
feed tools explain the parameter.
2026-11-13 10:17:25 +01:00
Maurice b9231ffb53 feat(mcp): manage the trip's shared links over MCP (#3076)
list_collab_links, create_collab_link, update_collab_link and
delete_collab_link run the Links panel's REST routes as tools: the shared
body contracts, trip access, collab_edit on writes and the collab🔗*
broadcasts, behind the links sub-feature flag. Unit, e2e and broadcast
parity cases cover them; the wiki lists the tools.
2026-11-13 10:17:15 +01:00
Maurice 489ba93c0c feat(mcp): let update_display_settings set the dashboard world clocks (#3044)
dashboard_timezones joins the display-preference allow-list as a list of
IANA zone ids, each checked with Intl like the widget does, without repeats
and capped at 24. The write goes through saveMany, so open tabs refresh, and
both tool descriptions name the widget's fallback zones.
2026-11-13 10:17:15 +01:00
Maurice 3e98938c95 feat(export): places as KML for Google My Maps (#1866)
Google Maps takes no list from outside, but My Maps imports a KML layer.
The trip export dialog and the phone export sheet get "Places for Google
My Maps" (GET places/export.kml, MCP export_trip_kml), and a list's export
menu gets KML next to GPX. The KML codec gains a pin writer; the list file
mapping moves to collections.helpers to keep the service within its size.
2026-11-13 10:17:12 +01:00
Maurice 7283b75cac feat(mcp): list_collection_places pages a long list compactly (#2647)
A read tool that answers one list a page at a time (100 by default, up to
500) as compact one-line JSON rows, filtered by status, label or "no label",
with the list's labels, the matching total and a cursor. The cursor is the
last place's position (sort_order, created_at, id), so labelling a page
while paging through unlabelled places skips none. getCollection orders by
the same keys, id last, so both tools agree on ties.
2026-11-13 10:17:11 +01:00
Maurice cae04e83dd feat(collections): edit a saved place's provider ids, website, phone and price (#2646)
The update contract takes google_place_id, google_ftid, osm_id, website,
phone, price and currency like saving does, website with the same
normalisation and refusal. REST and update_collection_place share it, so an
agent can correct a place instead of re-saving it and losing its labels,
ratings, position and id. The assistant's change card names the new fields.
2026-11-13 10:17:11 +01:00
Maurice 72e8b1f8d9 feat(collections): "want to revisit" status for saved places (#2567)
A fourth status after visited, for a place already seen that the user wants
to go back to. It joins the one-tap cycle, the filters, the pickers, the
assistant cards and the MCP status tool, and counts as visited wherever
visits are counted (isVisitedCollectionStatus), so "visited everywhere"
keeps it. The status pickers go two by two where four would not fit.
2026-11-13 10:17:11 +01:00
Maurice a70eea5bf2 feat(costs): let a trip keep its costs as totals only (#1714)
Travellers who track what a trip costs but never split it saw an
Unfinished warning on every expense and a settle-up they had no use for.
A trip now has a costs mode (appended migration, trips.costs_mode, split
by default); totals leaves payers, shares and settling up out of Costs on
desktop and phone through the same gate as a trip of one, and saves new
expenses without a payer. PUT /api/trips/:id and update_trip take it on
budget_edit. The split chips move into their own component.
2026-11-13 10:17:03 +01:00
Maurice b6e3c9bf39 feat(native): the app needs TREK 5.1.4 on the server
nativeApi goes to 3 with 5.1.4, whose UI follows a widget or Live Activity
tap that starts the app. The apps refuse an older server with the hint to
update, and check a server they remember once more.
2026-11-12 20:21:16 +01:00
Maurice ae26a6c61c refactor(airtrail): save the settings in one transaction
The address and the key it may no longer carry now change together, as
they do for Dawarich, so the transaction ratchet drops the AirTrail entry.
The tests that pinned the old no-transaction shape now pin the new one.
2026-11-12 17:31:56 +01:00
Maurice 8979bd1fcd test(airtrail): one case per key field instead of a loop of awaits 2026-11-12 17:14:54 +01:00
Maurice 74d243d8e0 fix(airtrail): a new host does not inherit the stored API key
Saving the AirTrail settings with a different URL and the key field left
masked or blank kept the stored key, so the next sync sent the old
instance's key to the new host. The key now goes with the host, as it
already did for the connection test and for Dawarich: a new origin without
a new key clears it, a path edit on the same origin keeps it. The key is
cleared before the new address is written.
2026-11-12 16:58:24 +01:00
Maurice f2ac12ca4f fix: hold the ratchets after the SSO unlink and the day pager window
- The SSO unlink answers with a response contract (adminSsoUnlinkResponseSchema).
- It reuses linkOidcIdentity with nulls instead of a new repository method,
  so UsersRepository keeps its method count and size.
- The client calls the route through apiClient, so api/client.ts does not
  grow, and its confirm dialog takes no async callback.
- The pager's preview step and pagesFor keep their complexity.
2026-11-12 15:56:59 +01:00
Maurice b53130ca0d feat(admin): remove the SSO link of an account whose provider changed its subject
A sign-in with another subject of the same provider is refused, so an
account whose provider re-issued the subject (a migrated or rebuilt IdP, a
changed sub format) was locked out: editing the email, as the wiki advised,
never touched the stored link. The edit dialog now offers "Remove SSO link"
for other accounts while SSO is set up (DELETE /api/admin/users/:id/sso,
audit admin.user_sso_unlink). The next SSO sign-in links again under the
rules of a first one, so it still needs a verified, not self-set email.
The log line names the remedy, and the wiki tells the two refusals apart.

Fixes #3065
2026-11-12 15:39:21 +01:00
Maurice 1323183cce fix(assistant): Claude Sonnet 5.5 no longer ends a turn with 'came back without an answer'
Sonnet 5.5 hands back the words written before a tool call as a hidden thinking block. After the ask for the answer, an empty step got the general push with tools open again; the model answered, offered follow-ups once more, and the answer was lost a second time. That push now asks for the answer as text and stays text-only.
2026-11-12 08:42:16 +01:00
Maurice 855bf8610a refactor(planner): the day plan's drag state lives in a hook of its own; the holiday provider cases get a suite of their own
DayPlanSidebar grew past its size baseline with the Firefox drag fix. Its drag state (what is dragged, the drop line, the day under the drag, whether a drag happened) and the effect that ends a lost drag move into useDayPlanDragState, typed, which also lowers the file's strict-mode count; size and strict baselines are lowered with it.

vacay.service.test.ts went past the 2000-line limit once the Nager v4 cases were back in it. They move into vacay.holiday-provider.test.ts, which takes VacayService from a Nest testing module over a snapshot db, so neither the db mock nor a hand-built service is needed.
2026-11-11 22:38:59 +00:00
Maurice 742edc4a79 fix(ci): today's changes pass the release gates
- iOS: the navigation proxy reports an error page through a callback the controller sets; it called the controller's method on itself and did not build.
- keyboard: the done key reads the field's text in the capture phase, before the field's own handler, so a list that adds an item on Enter and clears itself keeps the keyboard; the tests wire it as the app does.
- Ireland migration: the queued writes use addSql instead of awaiting inside loops; only table constants and integer ids reach the SQL.
- tests: the Dawarich GL test types its two layer specs once (strict baseline lowered), the collab test reads the id off the listed message, the reasoning test runs its four servers in parallel, and a wasm copy covers the precompressed branch without a charset.
- en wording: the deviations agreement no longer calls TREK self-hosted.
2026-11-11 22:06:50 +00:00
Maurice 948c9ef57b fix(atlas): show Ireland's 26 counties instead of its four provinces
geoBoundaries has no county layer for Ireland: its ADM1 is the four
provinces and its ADM2 the 166 local electoral areas, so the Atlas showed
Ireland as four provinces next to a UK of counties.

The Atlas builder can now take a country's regions from another source.
Ireland's come from Tailte Éireann's 26 counties (CC BY 4.0), with Dublin,
Cork, Galway and Limerick each one county, under their ISO 3166-2:IE
codes. Those outlines come at full detail, so the builder simplifies them
itself and keeps the borders two counties share identical on both sides,
so no point between them falls into neither. ATLAS_ONLY rebuilds just the
named countries without downloading the whole world.

A migration clears Ireland's cached place regions so they are worked out
again as counties, moves visited rows that name a county onto its code,
and hides each county of a province a user had hidden. Provinces a user
marked as visited are kept as they are.

Fixes #2922
2026-11-11 21:26:31 +00:00
Maurice bac968c0bf fix(ai): retry chat requests with reasoning_effort none when a server refuses tools with reasoning
Azure AI Foundry reasoning deployments behind an "Own server" connection
reject function tools on /v1/chat/completions at their default reasoning
effort and answer HTTP 400 naming reasoning_effort. The Responses API
switch from #2792 only covers direct OpenAI keys, so the assistant could
not run on these deployments at all.

The chat client still never sends reasoning_effort, so servers that do not
know the field (Ollama, LM Studio, vLLM, OpenRouter) see unchanged
requests. When a request with tools is refused over reasoning_effort, it
goes out once more with reasoning_effort "none", and the rest of the turn
keeps sending it. The refused field is now carried on AiProviderError
instead of only being logged.

Fixes #3043
2026-11-11 21:26:20 +00:00
Maurice 5345b50d17 test(vacay): the holiday provider cases live in the existing suites again
The two new test files mocked the db module and built VacayService by hand,
which the test-mocks and new-service ratchets refuse for files without a
baseline entry. The cases move back into vacay.service.test.ts and
vacay.test.ts unchanged; VACAY-037 asks for a year no other case caches. The
unrelated admin addon test change is reverted and two comments name the new
provider.
2026-11-11 21:06:29 +00:00
Luffy a3622e91b6 feat(vacay): implement Nager.Holidays v4 integration and adapt holiday data structure 2026-11-11 21:04:07 +00:00
Luffy be305c3daf fix(vacay): move holiday lookups to Nager.Holidays v4 2026-11-11 21:04:07 +00:00
Sebastian Fischer ec78cfbdd0 feat(tours): find bakeries and groceries on the map and along a hike
A multi-day hike restocks on the way: bread in the morning, food for
lunch and supper every day or two. Neither could be searched for.

- Explore pill: new `bakery` and `supermarket` chips. `supermarket`
  already existed on the server (shop=supermarket|convenience) but no
  pill offered it; `bakery` is new (shop=bakery).
- Along the tour: `bakery` and `supermarket` are offered to a hike,
  after its existing kinds so a new hike still starts on the same three.
- TREK places index: `bakery` maps to food_and_drink>casual_eatery>bakery;
  `supermarket` is narrowed to grocery and convenience stores, since the
  whole food_and_beverage_store branch also brought tobacconists and
  wine shops. Both paths checked against a live answer of the index.
  As the more specific term, a bakery is no longer listed as a place to
  eat when only `food` is asked along a tour.
- Category guess: a bakery or grocer picked from the map was preselected
  as Bar/Cafe or Shopping even where the admin had made a Bakery or
  Groceries category. The guess now looks for such a category first, by
  name in the usual languages or by icon, and keeps the broad one when
  there is none. The category icon picker offers Croissant and
  ShoppingBasket, the icons the map chips use.
- Labels in every locale; MCP tool description names the new kinds.
2026-11-11 20:59:59 +00:00
Ibrahim Roshdy 8023be452a fix(atlas): give a point no polygon contains to the nearest border
A point just off a simplified coastline lies in no admin0 polygon, and the
fallback then took the candidate with the smallest bounding box. Nice and
Menton sit ~80 m outside France's coast and 20+ km from Italy, but Italy's
box is the smaller one, so they resolved to IT. The fallback now ranks the
candidates by distance to their border. A deferred loose box (PS/XK) keeps
its priority. The parity test's reference lookup mirrors the new rule.

Closes #3048
2026-11-11 20:59:53 +00:00
John Jahn 5112c96c05 perf(places): the country lookup skips polygons whose box misses the point and remembers its answers
The places list and the trip bundle resolve the country of every place
without a cached region on each read, and that lookup was about 90% of
their CPU time: it ray-cast every polygon of each candidate country, which
is thousands of rings for a country that is mostly islands.

A polygon whose outer ring's box does not hold the point is now skipped
without walking it, and an answer is kept by coordinate pair (20,000 pairs,
oldest out first), since the borders never change while the process runs.
The answers are the ones the ray-cast alone gives; ATLAS-SVC-051 and
ATLAS-SVC-055 compare every country against the reference lookup.

Measured on a production build with a 360-place trip: GET places
22.7 ms -> 3.5 ms, GET bundle 27.5 ms -> 6.2 ms. A first lookup of a
pair is about 1.8 times faster (90 us -> 50 us over 20 cities).
2026-11-11 20:59:47 +00:00
John Jahn 04b1fb72bb perf(static): serve the hashed assets from Brotli copies written at build time
Every file under assets/ was compressed again on each download, at the low
level a request path can afford. They carry their content hash in the name
and never change, so the client build now writes a Brotli copy beside each
compressible one (postbuild, scripts/precompress.mjs) and production points
a browser that accepts Brotli at that copy before express.static serves it.

Measured on a production build of 5.1.2, fetching the 652 precached files
the way a service worker install does: 16.3 MB -> 13.9 MB on the wire and
about 1.0 s -> 0.2 s of server CPU per install. A client without Brotli, a
range request and a build without copies are served exactly as before.
2026-11-11 20:59:40 +00:00
John Jahn a9d11ccb2e fix(plugins): grant plugin children the network on Node 25 and newer
From Node 25 the permission model covers the network, so a plugin child
forked with --permission and no --allow-net gets ERR_ACCESS_DENIED on every
lookup and connect, and no plugin can reach a host it was granted. Node 24
is unaffected and rejects the flag as a bad option, so it is passed only
where process.allowedNodeEnvironmentFlags knows it.

Which hosts a plugin may reach is still decided by the egress guard inside
the child.

Fixes #3045
2026-11-11 20:59:34 +00:00
Maurice 0a3d93b59b fix(collab): a deleted chat message keeps no text
Deleting a message set the flag and kept the text in the row and in every backup; only the client hid it. The delete now blanks the text with the flag, and a migration clears what earlier deletes left. The row stays for the placeholder.
2026-11-11 20:41:25 +00:00
John Jahn 8e8cff8105 fix(reservations): keep day_id on partial updates of time-only bookings
A partial update that left day_id out (a notes-only PUT, an MCP
update_reservation or update_transport call) re-derived the day from the
stored reservation_time whenever one was set. A time-only value such as
"12:00" resolves to no day, so the booking's day_id was written as null and
it vanished from its day in the planner. The end_day_id branch did the same
when a time-only reservation_end_time was sent.

The day is now derived only when the time carries a date that resolves to a
trip day; otherwise an omitted day_id or end_day_id keeps the stored value.
A dated time still re-pins the day, an explicit day_id null still clears it,
and a cleared end time still clears end_day_id. REST and both MCP update
tools share updateInTx, so the fix covers all three.

Tests: service unit cases (RESV-SVC-044..048), an e2e case per behaviour
on PUT /api/trips/:tripId/reservations/:id, and an update_reservation MCP
case, all seeded with the ORM factories.

Fixes #3025
2026-11-10 10:05:22 +01:00
Maurice 6ebb92af6c fix(bookings): a hotel booking's card shows check-in and check-out when only the stay has them
The edit form fell back to the linked stay's times, the cards read only the
booking's metadata, so a hotel showed 15:00 and 11:00 when edited and no times
in the list. The reservation list now carries the stay's check-in window and
check-out, and the desktop facts and the phone card fall back to them. On the
phone they come first, so a price or a seat cannot push them off the card.
2026-11-10 09:28:43 +01:00
Maurice 5cbe4e2fa3 feat(native): the app needs TREK 5.1.3 on the server and checks a remembered one once after the update
The fixes that keep the app's sign-in in the app live in the server's UI,
so an updated app on a 5.1.2 server still ended in the browser. The server now
reports nativeApi 2 and the app refuses anything below it, naming 5.1.3.

A server the app remembers from before is not loaded unchecked: the first
launch after the update shows the address screen filled in with it, says why
and checks it right away. A server that is new enough loads again with its
session; an older one gets the update hint.
2026-11-10 09:17:26 +01:00
Maurice 08034c4402 fix(map): the iOS app's tile store is allowed by the CSP, so its maps draw tiles again
The iOS app rewrites raster tiles to trek-tile:// and answers them from its
store. WebKit checks that scheme against img-src, which had only https:, so
every tile was refused and the app showed routes and markers on a blank map.
2026-11-10 09:17:25 +01:00
Maurice 28079197e7 fix(assistant): an answer that hands the talk on to a trip or journey chat ends the turn
The home assistant wrote its answer and offered the trip's own chat in the
same step. The loop asked the model once more after the card, and it wrote
the same answer a second time below it. A step that has its answer and only
hands the conversation on now finishes the turn; a hand-on without an answer,
or beside a read, still asks the model again.
2026-11-10 00:14:44 +01:00
Maurice 8dd2e9716d test(native): fix the sign-in handoff tests' quoting and formatting 2026-11-09 23:15:09 +01:00
Maurice 034cfb539a fix(native): the app's browser sign-in no longer lands on the dashboard when the browser still holds an older TREK
A browser that ran TREK before 5.1.1 keeps that version's app in its service
worker. When the app opened /native-handoff, the worker answered it from its
cache with an app that has no such page, which sent the user to /, and the
sign-in ended on the dashboard in the browser instead of in the app.

The app now opens /api/auth/native/start, which every TREK service worker
passes to the server. That page takes the workers off the browser and
forwards to the handoff page. Until the sign-in is handed back, the web app
registers no service worker and does not reload for a new version: a tab
still open on the older TREK keeps its worker alive, and a registration or a
reload in the middle of the sign-in brought it back to answer the next page.
2026-11-09 23:02:33 +01:00
Maurice d3bdc6de62 fix(memories): keep the Synology repair's progress through the settings domain
The repair job wrote its marker rows through the AppSettings repository,
which the settings domain owns. It now registers both keys and goes through
SettingsService, as the import boundary check asks.
2026-11-09 12:03:27 +01:00
Maurice a9bfcd0fd5 fix(memories): log capture reconfirm failures through the Nest logger
Keeps the no-console and test-new-service ratchets at their baselines: the
reconfirm tests build the service through the suite's existing helper.
2026-11-09 11:32:27 +01:00
Maurice 12caa7fbde fix(synology): an unreachable library is reported, not asked a second time for the next id 2026-11-09 11:08:54 +01:00
Maurice 1cbd4acf01 feat(synology): photos attached before the fix get their own capture time and place once
Until #3019 a Synology photo's capture time and GPS came from the item after
it, which on many libraries is the next photo, and no one can edit a photo's
capture time to put it right. On the first start after this, every Synology
photo is asked again through its owner's library, held to the backfill's few
lookups per NAS, and what Synology confirms for that very photo replaces what
was stored; a photo it cannot confirm keeps its values. While a library could
not answer, the pass runs again on the next start, at most three times.
2026-11-09 10:59:43 +01:00
Maurice bc2fb1ce2d fix(synology): journey photos keep their own capture date and place, not the next photo's
getSynologyAssetInfo asked Browse.Item for the unit id plus one, a fixed
offset that holds on some libraries and on others names the next photo, whose
date and GPS were then stored for the attached one. It now asks for the unit
id, then for the one after, and takes only an item whose own thumbnail belongs
to the requested unit; when neither does, it answers not found instead of a
neighbour's metadata.

Closes #3019
2026-11-09 10:47:22 +01:00
Maurice 286b0b09ec feat(managed): the managed attachment point declares the routes it serves without a session, and the boot gate allows exactly those
Empty in this repository, as the module itself. An install that replaces the
module adds its anonymous routes in the same file, and the gate still fails on a
route missing there or an entry that went stale.
2026-11-08 23:33:52 +00:00
Maurice 5e7d778740 fix(assistant): the step after the ask for the answer may only write text, so Claude Sonnet 5.5 can no longer offer its follow-ups a second time instead (#3016) 2026-11-08 21:16:09 +01:00
Maurice 4f1f48eec5 fix: watch sync reads the roster through trip membership, image viewer on theme tokens, retry flaky release downloads in CI 2026-11-08 11:30:45 +01:00
Maurice c6cb3dc64a feat(native): a silent push keeps every watch on a trip current
A change to a trip's packing lists or to-dos (any writer: REST, MCP, plugin RPC) is noted from the realtime broadcast and, a couple of seconds later, each member with the iPhone app gets a silent push through the app relay, marked as a background push. The push carries no content: the woken app reads both lists of the trip its watch shows from the server with its own session and hands them to the watch. The app gains the remote-notification background mode for it.
2026-11-08 11:05:06 +01:00
Maurice 99a8701bd4 fix(wallet): offer Google Wallet only where it works, and never post it to the Apple route
- GET /api/trips/:tripId/reservations/google-wallet answers whether this
  server can make Google Wallet passes: passes on, and the TREK services'
  signer with its Google route mounted (it is once liketrek has a Wallet
  issuer). The probe is kept for ten minutes.
- The Google route is derived from TREK_WALLET_SIGNER_URL by its path, the
  query kept; a signer URL that does not end in /pass has none, and the
  booking is refused with a 404 instead of going to the Apple signer.
- End-to-end tests for the save link: owner, stranger, another trip, no
  issuer yet, a link that is not Google Wallet, and the availability.
- Italian and Hungarian wording of the button.
2026-11-07 14:50:17 +01:00
Maurice d41a4f0827 feat(server): name the event in a push and offer a booking as a Google Wallet pass
The push payload carries the event it reports, so the Android app can file a
notification under a channel of its own (chat, trip updates, invitations).
A browser and the iOS extension ignore the field.

GET /api/trips/:tripId/reservations/:id/wallet-pass/google sends the same
request the Apple Wallet signer takes to the TREK services' Google route and
answers with the signed save-to-wallet link the Android app opens. Off with the
other TREK services, and a clear 404 while liketrek has no Wallet issuer yet.
2026-11-07 14:11:58 +01:00