Commit Graph

165 Commits

Author SHA1 Message Date
Maurice dbb98a9bcd Merge branch 'main' into dev 2026-09-20 15:29:35 +02:00
Maurice 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.
2026-09-20 13:54:31 +02:00
Maurice 4b3fb7dec9 fix(oidc): never take a role away over a claim the provider did not send
Role mapping read the configured OIDC_ADMIN_CLAIM out of the userinfo payload
and collapsed two very different answers into one: the claim arrived and did
not name an admin, and the claim was not there at all. The second is not a
verdict about the user — it is what an operator sees when the scope carrying
that claim was never requested, or the claim name has a typo. TREK wrote
role='user' for it anyway, with nothing in the log, nothing in the audit trail
and nothing in the UI, so a misconfiguration read as an unexplained loss of
admin. The only guard was the last-admin rule, which protects an instance with
exactly one admin row and therefore almost nobody.

An absent claim now leaves the stored role untouched. A claim that does arrive
still promotes and demotes exactly as before. To keep the silence from moving
somewhere worse, an absent claim on an account that is already an admin warns
on every login, naming the account: providers that omit a claim rather than
send it empty — Okta's filtered groups, Entra ID — cannot take admin away
through the mapping at all, and that has to be done in TREK's own admin panel.
The ordinary case still warns once per claim name, listing the claim names that
did arrive (never their values) and pointing at OIDC_SCOPE. A real transition
writes an audit row from the controller, with the client IP.

Closes #2364
2026-09-20 12:48:44 +02:00
Maurice 5d04f9c400 fix(docker): move start-up into a script so an edited container still boots
The image carried its whole start-up sequence as one inline `sh -c` string
containing four single-quoted echo messages. Container management UIs render
Config.Cmd back into a text field and re-split it on submit with a quote-aware,
escape-unaware tokenizer, so editing a container in place — the usual way to
change an environment variable — shredded the quoting and left dash half an
if-block to parse. The container then died before Node started, with only
`TREK: 1: Syntax error: end of file unexpected (expecting "fi")` in the log:
the word TREK is the stranded fragment of the first echo, reported as $0.

The logic moves to server/scripts/entrypoint.sh unchanged, and CMD becomes a
single bare token, which has nothing left to mangle. A unit test pins that
shape — both exec-form lines parse, and every argv element stays one token
free of quotes and spaces — and the Dockerfile joins the test workflow's path
filter so that guard actually runs when someone edits it.

Closes #2374
2026-09-20 12:48:44 +02:00
Maurice c8d4ed896e fix(trips): let a trip run past 365 days and refuse a longer range instead of clipping it
A trip longer than a year stopped at day 365: generateDays capped the day
rows at MAX_TRIP_DAYS while the trip kept its full end date, so the last
days had a date but no day to plan on, in the day list, the transport and
booking dialogs and the phone's date pickers (#2403).

The limit is MAX_TRIP_DAYS in @trek/shared now, 999 days, shared by the
server, both trip forms and the MCP tools, and a range past it is refused
rather than cut short: 400 on REST, an error result on MCP, bad params on
the plugin RPC, and a message in the forms before anything is sent. The
check runs before the trip row and the budget are touched, also catches a
start date moved past the stored end on its own, and holds a day_count
rebuild to the same bound. A trip that already carries a longer range can
still be renamed. The MCP-only 90-day ceiling on create_trip goes with it;
it clipped dated trips the same way.

Closes #2403
2026-09-20 12:34:34 +02:00
Maurice 44c92052a3 docs(wiki): bring the pages in line with 4.3.0
Every page was read against the code on dev. What changed:

- Pages that described 4.2 behaviour: satellite view on the vector maps,
  the mobile journal timeline, Collab with its fifth tab, the Review badge,
  clustered pins on the share page, the Leaflet preset list, the strict
  guard on fe80::/10, the invite link tools and their scopes, the Public
  API scope rules, the permission count in the plugin SDK.
- Features of this release that no page mentioned: videos in a journal
  entry, receipts on an expense, the Google transit backend, routing
  counters, the rating on a place, the navigation menu, car stops, the
  manual school holiday catalogue and its MCP tools, the Dawarich panel in
  the Atlas, the release notes modal, plugin frames going fullscreen, the
  corridor call into the search provider hook.
- Statements that sent an admin the wrong way: DEFAULT_LANGUAGE is not in
  the Helm ConfigMap, env.PORT cannot be changed on the chart alone, the
  rotation script does not cover the document sync credentials, event
  handlers cannot broadcast, there is no per user MFA reset in the admin
  panel, the demo user can edit, an unparsable APP_URL stops the server.
- Updating gets a section for 4.3.0: the TREK Places API as the new
  default, the new variables, the Overpass timeout, SSO sessions on an
  OIDC only instance, the migration that puts booked nights on the drive.
2026-09-20 12:11:41 +02:00
Maurice d803efd39d feat(collections): import a file into a list that already exists
The import dialog always made a new list. It now asks where the places go:
a new list, as before and still the default, or a list that is already there.
The picker offers the lists the person may edit, which is their own plus a
shared one where they are an editor or an admin, and starts on the list they
have open.

Adding to a list only ever adds. A place the list already holds is left
exactly as it is, with its status, notes, rating and labels, and counted as
already there in what the import reports; it is recognised the way a single
save recognises one, by provider id, then name, then position. The list keeps
its own name, colour, icon and description whatever the file says, a label
the file brings is created only when the list has none of that name, and new
places are appended after the ones already there so a manual order survives.

The route is POST /api/addons/collections/:id/import, 200 because it adds to
a list rather than making one, and both imports now write their places through
the same helper.
2026-09-19 23:12:25 +02:00
Maurice 7cfec6452d fix(ssrf): let a listed link-local address through, such as a rootless Podman host gateway
A rootless Podman container reaches its host through 169.254.1.2, and an
identity provider behind that address failed the OIDC login with "Requests
to link-local / cloud-metadata addresses are not allowed", with no setting
to change it.

ALLOW_LINK_LOCAL_IPS names single link-local IPv4 addresses that may be
reached after all. The admin-configured lane (OIDC, AI parsing, plugin
OAuth, routing engines) reaches a listed address directly; the guard on
user-supplied URLs treats it as internal, so it also needs
ALLOW_INTERNAL_NETWORK there. 169.254.169.0/24 and 169.254.170.0/24, where
clouds serve instance metadata and credentials, cannot be listed, and an
entry that is not one link-local IPv4 aborts startup. Unset, nothing
changes.

Closes #2400
2026-09-19 22:39:12 +02:00
Maurice 3ad40e056d fix(docsync): an orphaned binding stays stopped on every path that starts a run 2026-09-19 21:57:04 +02:00
Maurice 83c9cebddc docs(wiki): a booked check-out no longer drives the road trip schedule 2026-09-19 21:48:08 +02:00
Maurice 58f6a49f32 docs(wiki): document road trip, document sync, the TREK Places API and Dawarich
The 4.3.0 features had no pages, or pages that described an earlier
state. Three new pages and a rewrite:

- Road-Trip: the addon from enabling it to the phone tab, stays and a
  stop's End as the time you leave, pinned times and the backward
  schedule, daily travel times, the three driving limits, search along
  the route, via points, other ways and what avoiding tolls needs,
  following a track, the warning layer, MCP tools and troubleshooting.
- Document-Sync: switching stores on, binding a trip, what each store
  asks for and binds, direction and delete rules, conflicts, the
  mass-delete guard, renames and restores, what needs a look, limits,
  private addresses, and the MCP tools.
- TREK-Places-API: what the index is, where TREK asks it, what leaves
  the instance, switching it off or pointing it at a copy of your own,
  and what happens when it is down.
- Dawarich: brought up to date with suggestions, the journal fold,
  recorded routes, Atlas proposals and the wish list.

Places and search, documents, the addon and admin overviews, MCP
tools, environment variables, internal network access, the sidebar and
home link the new pages, and the in-app help picks them up from the
sidebar.
2026-09-19 21:45:16 +02:00
Maurice 80bb06c84f feat(collections): export and import a list as GPX
A list could leave the instance as a TREK file, which only another TREK
reads. Export now asks for a format, and GPX is the second one: the
places as waypoints, for OsmAnd, Organic Maps, a Garmin or gpx.studio.
Import takes a GPX in the same dialog as a list file. Asked for in
discussion #2301.

The GPX is written from the very list file the JSON export returns, so
what may leave the instance is still decided in one place. Name,
description and address, notes, website and category go into GPX's own
elements, where every app shows them. Labels, status, phone and the
rest travel in a TREK extension namespace, so a GPX made by TREK comes
back as the same list. A place without coordinates cannot be a
waypoint; it is left out and the export says how many were.

A GPX is read on the server, sent as text, with the XML library the
trip importer already uses. A document with a DOCTYPE is refused before
parsing, which closes both external entities and entity expansion. Size
is capped like the list file, and the element count is capped before a
tree is built. The reader only turns the GPX into a list file: the
browser previews it and imports it through the existing route, so one
contract, one transaction and one skipped counter cover both formats.
Waypoints and named route points become places; tracks and unnamed
route points are counted in the preview and not imported.

The two list-file routes get a body ceiling of twice the file limit.
The global 100 kB cap refused most real favourites files, and it
already refused list files well under the megabyte the contract allows.
2026-09-19 21:01:45 +02:00
jubnl ac47066a93 change oidc behavior with remember me on by default when oidc only enabled (#2397) 2026-09-19 09:12:15 +02:00
Maurice 185f6c5335 Merge pull request #2373 from liketrek/fix/journey-skeleton-per-day
fix(journey): a place kept across two days is two days of the journal
2026-09-17 15:31:37 +02:00
Konstantinos Thermos bd44197974 Merge pull request #2330 from liketrek/feat/pdf-trip-route-map
Feat/pdf trip route map
2026-09-16 16:10:31 +03:00
Maurice 61a8a2765b fix(geo): read the Nominatim override the way the schema validates it
Follow-up to #2379.

NOMINATIM_URL arrives from a compose file or a ConfigMap with whatever
whitespace was typed around it. The schema validates the trimmed string and
calls a blank one unset, the derivation did neither, so " " passed startup as
"use the default" and then became the base URL that cannot be fetched, and a
padded value lost every geocoding call the same way. It trims now, which is
also what the places client next door does with its own override.

The chart could not pass the variable at all: the ConfigMap is an allowlist and
an unknown key is dropped without a word, which is exactly the locked-down
cluster the override exists for.

The docs said what to set and not what it costs. They now say that it replaces
the public service with no fallback, that the endpoint has to speak the
Nominatim API, and that the one-per-second spacing stays either way, so nobody
stands up their own instance expecting it to run faster.

Test IDs follow the file again.
2026-09-16 11:08:07 +02:00
Luffy c0195557d4 feat(geo): support overriding Nominatim endpoint via env 2026-09-16 14:30:31 +08:00
Konstantinos Thermos 48cd46f818 feat(pdf): put the trip's route map in the export 2026-09-15 15:58:36 +03:00
Konstantinos Thermos 4eaf90fd1b fix(journey): a place kept across two days is two days of the journal
The sync engine keyed a skeleton by source_place_id alone, so a stop standing
on two days — the city you land in at dusk and walk through the next morning —
got one entry on the first of them and nothing on the rest.

Key on the day assignment instead. The assignment row rather than the day,
because moving a stop updates day_assignments.day_id in place: keyed this way
an entry follows the move instead of reading as one stop leaving the plan and
another arriving. Existing rows are backfilled to the place's earliest
assignment, and reconciliation adopts an unlinked row rather than annotating
it out from under its author.

Fixes #2329
2026-09-15 15:31:59 +03:00
Maurice 54b94cf9e3 docs(wiki): the manual stop, clearing a search, and charging before the add
Three panel changes and one new MCP tool that the pages did not describe
yet. Also records the two rules the code enforces and a reader would
otherwise have to discover: the charging panel is fetched once per opened
dialog rather than per row of a result list, and the lookup tool carries no
power rating or socket count because those are OpenStreetMap tag data that
travels with the corridor result instead.

The wiki syncs on a push to main, so this rides along until the next
release rather than appearing now.
2026-09-14 13:22:26 +02:00
Maurice a2585beb13 docs(wiki): the SSO exchange takes a cookie as well as the code 2026-09-13 22:05:54 +02:00
Maurice e3471e2dc8 fix(reservations): a night booked on the form belongs on the drive too
The booking form wrote the stay row itself, so it was the one door that left the
day stop out. A hotel entered there stayed invisible to road trip mode, which is
the same complaint as before with a different way in: enter the place twice or it
is not on the route.

It writes the stop now, through AccommodationsService rather than a second copy of
its SQL. The permission argument for not doing so does not hold: the night already
shows in the day header the moment the booking is saved, and the stop is that same
night in the other view, not a second edit to the plan.

Two holes go with it. Moving the booking left the stop on a day nobody sleeps there
any more, and deleting it left the stop behind entirely — in both cases still
carrying the booking's id, which is what the day list hides on, so nobody could
reach it to remove it. The stop now follows the stay and goes with it, and only
once the trip_id guard has confirmed the stay was this trip's to delete.
2026-09-13 12:38:21 +02:00
Maurice f0e1da2d81 feat(accommodations): put a booked night on its check-in day
A road-trip stop is a day assignment: the rail, the server-side plan and the
map all build their stops from day_assignments and only look the stay up
afterwards, to hang check-in and check-out on one. A stay written without an
assignment was therefore invisible to every routing surface, so a hotel entered
in the day planner never reached the road-trip view and had to be entered a
second time as an ordinary place. The road-trip side has always written both
halves in one go; this is the day planner catching up, in the domain that
writes the stay.

Booking a night now also assigns its place to the check-in day, inside the
transaction that already writes the stay and its partner hotel reservation, and
types the place as a hotel stop when it carries no type of its own, so it reads
exactly like a night booked in the road trip. Only the check-in day gets a
stop; the later nights ride on the stay row. A place already planned for that
day gets no second stop and the booking claims none, so cancelling it cannot
delete a stop somebody placed by hand. Moving the booking moves its own stop,
deleting it takes its own stop back, and a trip re-date carries it along with
the stay. All four write surfaces (REST, plugin RPC and both MCP tools) run the
same write and announce the same events; the answers carry the stop too,
because the broadcast skips the socket that sent the request.

New nullable day_assignments.accommodation_id marks a stop as a booking's own.
It stays NULL for every existing row on purpose: everything planned before this
was planned by hand, and no backfill rewrites a finished trip. A stay booked
earlier picks up its stop the next time somebody saves it.

The booking form keeps writing stays on its own: that surface is gated on
reservation_edit and the day stop is day_edit work.
2026-09-13 01:13:25 +02:00
Maurice 105bba2b95 docs(wiki): describe the Dawarich integration
What it reads, what it never writes, what happens to the recorded
positions, and how the connection, the suggestions, the Atlas marks and
the wish list behave from the reader's side.
2026-09-12 23:37:13 +02:00
Maurice 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.
2026-09-12 23:36:09 +02:00
Maurice f2bd116044 docs(maps): an Amap link needs a coordinate
A bare amap.com/place page carries an id and nothing else, and turning that
id into a location would need a keyed detail lookup the URL resolver has no
user for. It answers 400, which AMAP-084 pins. The wiki offered the link as
paste-able and the parseAmapUrl comment still promised the lookup, so both
now say what the code does.

The provider header also claimed photoRefs/photoBytes were deliberately
left out, naming two methods the places-provider interface no longer has.
The reason is unchanged: an Amap picture comes without a licence statement
we could show, so an Amap place gets its picture from Wikimedia.
2026-09-12 18:03:11 +02:00
Maurice 3e8ab22836 docs(maps): Amap beside the TREK index
The index and OpenStreetMap answer every search; the provider choice
only decides who fills the keyed slot next to them. Rows carry a source
mark, a place keeps its Amap id, and the journey map shares the shifted
projection.
2026-09-12 18:03:11 +02:00
A1phaN 57eac32f9f feat(map): amap as map api provider and display template 2026-09-12 18:02:57 +02:00
Maurice 9307025ed0 fix(costs): derive the final budget breakdown on the server, in the same cents
The three figures under a traveler's final budget came from the server, netted
in integer trip cents at the rate frozen on each expense. The rows listed under
them did not: both shells converted the expense list again with whatever live
rate they had, and with none loaded yet (first render, offline) the raw foreign
amount went out under the display currency's symbol. On any trip with a
foreign-currency expense the rows could not add up to the line above them.

The settlement response now carries the rows with the figure: `sources` on each
finalBudgets entry lists what the person fronted per expense, the recorded
transfers on their side and the open flows, in display cents. They are filled
by the same two loops that fill the totals, so an expense with no split members
stays out of them as it stays out of the ledger, and each list is spread over its
figure's already allocated cents with the same largest-remainder split, so the
sum is exact in every display currency. The shells print those cents and only
resolve names; the rows carry the sign of the line they sit under.
2026-09-12 17:33:15 +02:00
RemEx e0c9a8d1b6 feat(costs): show what the trip finally costs each participant
The settle-up view says who owes whom, but nothing on the Costs screen
answers "what did the trip cost me": the balances move every time a
payment is recorded, and the tiles only describe the signed-in user.

The settlement response now carries finalBudgets, one row per participant
in the ledger: expenses (what they fronted on split expenses), reimbursed
(recorded transfers, received minus sent), pending (their current
balance) and final = expenses - reimbursed - pending.

- Computed in calculateSettlement from the same integer trip cents as the
  balances, filled by the same loops, so the breakdown can never
  contradict the balance printed next to it. Each component is converted
  to the display currency as its own set and the final is subtracted in
  display cents, so the three lines add up to the total in any currency.
- A "Final budget" card on desktop, the narrow desktop layout and the
  mobile Costs tab: one amount per traveler, and a tap opens the three
  lines plus the expenses, transfers and open flows behind them. The
  filtering lives in CostsPanel.helpers so both shells share it.
- Contract in shared (budgetParticipantFinalSchema); the MCP
  get_settlement_summary description mentions the new field it returns.
- Four new costs.* keys, translated in every locale.
2026-09-12 17:33:15 +02:00
Maurice bc6d804af6 docs(budget): say what settled_at does when it is left out 2026-09-12 17:33:04 +02:00
Maurice ac57321840 merge dev and reconcile roadtrip planning 2026-09-12 15:45:34 +02:00
Maurice b10b02c685 finish overnight stops and cross-day range warnings 2026-09-12 15:37:09 +02:00
Konstantinos Thermos 359551653f Merge pull request #2319 from liketrek/feat/trip-route-overview
feat(maps): draw every travel day's route at once, with the trip total
2026-09-12 14:49:44 +03:00
Maurice f56e7a161e add open charging data and polish roadtrip warnings 2026-09-12 13:25:15 +02:00
Maurice 228e6a13a9 fix(admin): say what the local model actually gets 2026-09-12 13:07:08 +02:00
RemEx b58771fd4c fix(admin): recommend the local model that also reads images
The one model the addon offers to pull was Qwen3 8B, which is text-only, so a
scanned page or a photographed document reached it as no text at all and came
back empty. Qwen3.5 4B is multimodal at a third of the size: one download
covers every kind of document instead of leaving the operator to work out that
the recommended model cannot read half of them.

The list stays one entry long. A self-hosted host needs a model, not a menu,
and any other id can still be typed in or picked from what Ollama already has.
2026-09-12 13:07:08 +02:00
Maurice b90d258b6c docs(mcp): say whose settings the OAuth client lives in 2026-09-12 12:55:34 +02:00
Maurice 41b18f970c feat(vacay): add manual school holiday calendars 2026-09-12 12:51:27 +02:00
Maurice ff5fa55fad add Google Maps route import and plugin corridor search 2026-09-12 12:24:33 +02:00
Maurice af45b0ca74 add roadtrip warning areas and tidy driving settings 2026-09-12 11:23:09 +02:00
amo c0e3fde6f7 Clarify settings path for creating machine client
Updated instructions to specify 'user' in settings path. Without it, minor confusion to be still operating in main 'Admin' settings where you enabled MCP integration
2026-09-12 13:08:18 +10:00
Maurice b773faff70 feat(roadtrip): share trip preferences and improve service stops 2026-09-12 00:24:45 +02:00
Maurice fccc55732b feat(roadtrip): plan and edit trips through MCP without a browser 2026-09-11 23:25:06 +02:00
Maurice 491cfccc56 feat(roadtrip): add daily scheduling and configurable day endings 2026-09-11 22:28:03 +02:00
Konstantinos Thermos 3c735c80a4 Merge remote-tracking branch 'origin/dev' into feat/trip-route-overview
# Conflicts:
#	client/src/components/Map/MapView.tsx
#	client/src/components/Map/MapViewGL.tsx
#	client/src/pages/TripPlannerPage.tsx
#	client/src/pages/tripPlanner/useTripPlanner.ts
2026-09-11 16:22:21 +03:00
Maurice 2c06c9904b feat(collections): a list as a file, so it can leave the instance (#2198)
Sharing a list works between people on the same TREK. This is the other
case: giving one to somebody who runs their own, or keeping a copy. Export
sits beside Edit and Share in the list header and downloads the open list;
Import sits beside New list in the rail, reads a file, shows what is in it
and makes a new list of it. Desktop only, which the route already decides.

What travels is what somebody wrote down: the list's name, colour and
description, its labels, and each place with its address, coordinates,
notes, status, contact details, category name and labels. A file is dull
JSON on purpose, so it can be read and edited by a person.

What does not travel is everything that only means something on the instance
it came from. Ids of every kind, since the receiver issues its own and a
category crosses over by name. Star ratings and members, because a rating is
an opinion somebody gave the people on that list. Provenance into trips,
which points at rows the receiver cannot see. And an image path under
/uploads, which resolves on the sender's server: only an absolute https URL
survives.

A file is a stranger's JSON and is read as one. It is parsed in the browser
that chose it and never uploaded; the parsed list goes through the same
contract on both sides; and a link that is not http(s) arrives as nothing
rather than as an anchor. Strict where it must refuse, forgiving where it
can drop: a file that does not say what it is never reaches the importer,
while one unreadable place inside it is skipped and counted, and one bad
field on a good place costs that field alone.

An import is always a new list. Importing into an existing one would mean
deciding what happens to a place that is already there, and every answer to
that is a merge nobody asked for.
2026-09-11 14:58:34 +02:00
Maurice 87c73e50fb feat(studio): give a book its first and its last page (#2317)
A new book was a cover, a spread and a back cover, so it opened straight
onto pages 2 and 3. A bound book does not: the inside of the cover is
blank, and the first thing facing the reader is a single right-hand page,
page 1. The same at the end, where a single left-hand page sits before the
back cover. Both are laid down now, empty, in a new book and in a laid-out
one, and they are fixed the way the covers are: an inner spread only ever
goes between them.

The page numbers follow. They were counted from a spread's position in the
array, which assumed the spread after the cover was the first, and three
places did that sum on their own. One function now counts along the book,
one page for a single leaf and two for a spread, so the sheet on screen and
the label in the print view and the folio on the page all say the same
number. A book with a first page starts at 1; a book from before the first
page existed has none and keeps the numbers it always had.
2026-09-11 11:01:15 +02:00
Maurice 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.
2026-09-10 21:40:51 +02:00
Konstantinos Thermos bf1900c3a0 feat(maps): draw every travel day's route at once, with the trip total
Extract the day route builder out of useRouteCalculation into a pure module and
aggregate it over the whole trip: one map with every day's route, each day in its
own colour, the mode per stage and the trip's total distance. Straight lines first,
roads as they answer; a refused leg keeps its line and adds no distance.

Give the floating map controls one shared drop shadow while adding the toggle, so
the new pill and the base-layer switcher stand off the tiles the way the locate
button always has. Document the control in wiki/Map-Features.md.

Addresses #1736
2026-09-10 15:57:48 +03:00