Commit Graph

160 Commits

Author SHA1 Message Date
Maurice 178df0fbe3 fix(map): keep the world-copy fix inside the CI ratchets
Moves the Leaflet map's event bridges (click, context menu, viewport, POI drop
and the longitude-frame follower) out of MapView into leafletMapBridges, and
shares one POI drop target between the Leaflet and GL maps, so both map files
shrink instead of growing. The repair migration does its arithmetic in one
UPDATE per table, and the new tests drop their non-null assertions and loops.
2026-11-04 11:37:52 +01:00
Maurice 51af575c9a fix(map): keep places and routes on one world, and start new installs with Journey and Collections
A right-click, click or drag on a repeated world copy reported the longitude it
has on that copy (274.9 for -85.1). The place was saved there, the geocoder
found nothing, and a route to a place on the real world ran across the whole
map (#2976). Longitudes are now brought back onto [-180, 180] where the map
hands them out (Leaflet and GL), where they are stored (places, booking
endpoints, collection places, so REST, MCP, plugin RPC and importers alike)
and before reverse geocoding. A migration moves the ones already stored.

Leaflet draws a layer at its literal longitude only, so a trip from Los
Angeles to Auckland had its pins at opposite ends of the world while the
flight ran off the edge to the next copy (#2975). A trip that crosses the
antimeridian is now drawn in one longitude frame, seamed in the widest
stretch it leaves empty, so pins, routes, flights and every overlay lie on
one copy and the map opens across the Pacific. Every other trip has no frame
and draws exactly as before. The GL day fit also takes the short way.

A new install now starts with Journey and Collections switched on, next to
the trip addons (all but Road trip), Atlas, Vacay and the Naver import that
already were. Existing installs keep their addons as they are.

Closes #2975
Closes #2976
2026-11-04 10:56:36 +01:00
Maurice 65e5d4ab72 fix(settings): format the date format files prettier flagged 2026-11-03 23:39:06 +00:00
Maurice 44c6fbf372 fix(settings): let every date follow a date format of the user's choice
Every date in TREK was written in the interface language's format, and
English is en-US, so an English interface always showed and read
mm/dd/yyyy with no way to change it. A few native date inputs followed
the browser instead, so one screen could mix two formats.

date_format is now a user setting with an admin default, next to the
time format: automatic (the language's format, as before), YYYY-MM-DD,
YYYY/MM/DD, DD.MM.YYYY, DD/MM/YYYY or MM/DD/YYYY. The date picker shows,
pre-fills, hints and reads typed dates in it, and every date written in
numbers across the app (lists, cards, admin panels, CSV and PDF exports)
goes through one formatter. Dates written with a month name keep the
language's wording. The remaining native date inputs are the shared
picker now. The display settings MCP tool reads and writes the key.

Closes #2971
2026-11-03 23:38:37 +00:00
Maurice f8e51cb83d fix(pdf): hand the trip PDF to Safari from the iOS Home Screen app
An iPhone or iPad running TREK from the Home Screen ignores print(), in
the page and in an iframe alike, so "Save as PDF" in the preview did
nothing there. Mobile Safari prints the same preview fine.

In the installed iOS app the preview now shows "Open in Safari" in
place of the dead button, with a line saying why. The link opens the
trip with ?export=pdf, and both shells open their export dialog or
sheet for it, so the PDF is one tap away once Safari is up. The
parameter is dropped once read and survives the login redirect.
Everywhere else the preview prints as before.

The preview overlay moves out of TripPDF.tsx into pdfPreview.ts, which
TripPDF had no room left for under lint:size, and its backdrop and
stacking now use the --overlay and --z-modal tokens.
2026-11-03 09:14:55 +01:00
Maurice 04debce79e fix(planner): keep the lint and size ratchets where they were
The zone helpers of the day order take the merged row's own type instead
of an explicit any, and the day list's size baseline follows it down.
2026-11-01 11:41:56 +01:00
Maurice 4003769a5c fix(planner): keep the date whole and rename the title where it stands with the date first
With "date first in day headings" a long day title squeezed the date in
the day list to "Th…" (#2920); the name now gives way and the date stays
whole. The day panel and the mobile day sheet kept the rename pencil on
the heading, which then is the date, and the input replaced it (#2921);
pencil and input now go with the title line under the date.
2026-11-01 11:41:56 +01:00
Maurice 8692b8790c fix(planner): judge a move by the order a day is drawn in across time zones
The checks behind a drag and the up/down buttons read each booking's raw
clock, so on a day whose flights were put in the order they happen every
move of a timed stop asked to change its time (desktop) or was refused
(mobile). They now read the same key the day is ordered by.
2026-11-01 11:41:56 +01:00
Maurice 1b6a156969 fix(planner): order flights across time zones by when they actually leave
A booking's time is local to its own airport, so a flight leaving
Auckland at 20:20 and landing in Los Angeles at 12:25 the same date
was drawn below the 17:00 flight on from Los Angeles. Bookings whose
dated time has a known zone now take their clock slots in real order.
2026-11-01 11:41:55 +01:00
Maurice 6b6d971f07 fix: the CI findings on the 5.0 release notes and the demo fixes
The overview of a trip's tours decides for itself when it shows, which keeps the planner page hook inside its complexity and length limits, and adds nothing to the camera's key while there is no tour to frame. The elevation helper names its empty case for the strict type check, the self-host wording exemption for the retired Dawarich card goes, the day plan test reads the wrapping pills, and the new tests drop their non-null assertions and loops.
2026-10-25 11:03:03 +01:00
Maurice 26611b46e4 fix(tours): elevation model jitter no longer counts as ascent
Ascent and descent were the raw sum of every height difference, so a flat walk across Venice (highest point 16 m) read 121 m of climbing. Heights now count once they move 5 m from the last one that counted; the server's stored figures and the planner's live analysis share the one function.
2026-10-25 10:51:56 +01:00
Maurice 3681db6cd4 fix(collections): the collection map opens on all of its places
The map framed its first view for the browser window instead of its own box and then skipped the fit, so a list of cafes across Europe opened on northern France. It now fits the places in its own box with room for the label bar and the controls, fits again when the set of places changes, keeps a view the user moved, opens a single place at town level and keeps a list across the date line on its short arc.
2026-10-25 03:12:19 +00:00
Maurice e28bd2fc99 fix(maps): draw with Leaflet wherever WebGL2 is missing
MapLibre 6 and Mapbox GL 3 both throw out of their Map constructor without
WebGL2. Every place that builds a GL map now asks one helper first,
components/Map/glSupport.ts, and a browser without WebGL2 gets the Leaflet
map with the user's tiles instead of a map that fails to start:

- the trip map (MapViewAuto) and the journey maps (JourneyMapAuto) pick
  their renderer through useGlRenderer, which folds in the Mapbox token
  rule they each had;
- the settings preview, desktop and phone, shows the Leaflet preview
  through useMapSettingsForm and says why under it;
- the tour planner keeps the 2D map when 3D is asked for and says that
  3D needs WebGL2;
- the PDF route map and the Leaflet vector basemap already fell back and
  now report a GPU failure too.

Behind the probe, buildGlMap wraps each constructor (MapViewGL,
JourneyMapGL, the settings preview, the terrain map, the PDF map). A GPU
that claims WebGL2 and still refuses the context is reported through
utils/webgl, which turns the answer to no and tells every mounted map,
so they switch to Leaflet without a reload; any other error is still
thrown. Two new strings, map.webgl2.fallback and map.webgl2.terrain, in
all 27 locales.
2026-10-24 22:41:56 +00:00
Maurice 1696162d22 fix(deps): maplibre-gl 6.15.0 past the attribution sanitizer advisory
GHSA-jrc7-96c5-q579 covers every maplibre-gl up to 6.4.0 and has no 5.x
release, so npm audit kept flagging the patched 5.24.0. Moving to 6.15.0
retires the local backport: patch-maplibre.mjs and its postinstall,
predev and prebuild hooks go, along with the Dockerfile copy and its
format-baseline entry. 6.x sanitizes attributions against an allow list.

What the major changes for TREK:
- ESM only, no default export: the engine module imports the namespace
  and hands that out as its default, so every caller stays as it was.
- The worker is no longer inlined, and inside a bundle MapLibre cannot
  find it next to its own module. The engine sets it from Vite's
  ?worker&url build before any map, the Leaflet bridge's included.
- WebGL2 only: the probe asks for webgl2 alone, so a WebGL1-only browser
  gets the raster basemap instead of a GPUInitializationError.
- 6.9 shapes right-to-left text itself and deprecated setRTLTextPlugin;
  the old plugin would replace that and could only run through eval in
  the module worker, so MapLibre no longer registers it. Mapbox does.

The lockfile moves maplibre-gl and its own dependencies only; the
Leaflet bridge already allows ^6.0.0 as its peer.
2026-10-24 22:41:56 +00:00
Maurice 4b633d8cd2 feat(admin): release notes show a #### heading as the feature it names
The release notes in Admin read ## as the version and ### as a section,
and printed a #### line as plain text. A release that heads each feature
inside a section with #### (Tours, TREK Studio under Added) now shows it
as a feature heading between the section and its text, on desktop and on
the phone.
2026-10-24 18:28:44 +02:00
Maurice a0554c2346 fix: four reported bugs that dev still had
- Dashboard: a browser that names no time zone (Chrome on Windows for a zone
  it cannot map) crashed the world clocks and the whole dashboard. One helper
  now reads the browser zone and every reader picks its fallback; a stored
  zone the browser does not know is left out instead of throwing (#2694).
- Phone: a leg of a multi-leg train or flight showed only its route, and its
  sheet showed the first leg's seat. The day plan subtitle is one helper for
  desktop and phone now, so a leg names its own train or flight, platform and
  seat, translated instead of the hardcoded "Gl."/"Sitz"; the sheet opened
  from a leg shows that leg's number, seat, platform and times (#2701). The
  PDF export translates platform and seat too.
- Journey list: the year badge named the year a journey was written, not the
  years of its trips; a trip over New Year shows both (#2685).
- A place's description and notes are markdown in the planner but printed as
  raw syntax in the PDF export and on a shared trip. Both render it now through
  the app's sanitized markdown pipeline, raw HTML stays text (#2692).

Closes #2694
Closes #2701
Closes #2685
Closes #2692
2026-10-24 10:39:35 +02:00
Maurice c31b2162ad feat(assistant): passport, country and bucket list cards 2026-10-22 21:34:15 +00:00
Maurice 4641788fc9 fix(assistant): lint, types and tests for the CI round
- agent loop and decisions without await in loops: steps as recursion,
  calls and applied changes in order through one promise chain, the
  independent reads and claims in parallel; part mapping in its own helpers
- place card, header and weather strip without refs or components created
  in render; long hooks and components split; state reset during render
- trip notes only in a turn's tool session, never an undo's; map view
  reported only while the assistant can run
- place strip scrolls through the strip's own smooth scroll, no physical side
- tests follow the raised limits and the new card fields; toasts read through
  a mocked useToast instead of a window global; baselines lowered where the
  counts fell
2026-10-22 21:30:35 +00:00
Maurice b64da26651 feat(assistant): trip assistant chat with your own AI connection
Adds an in-app assistant that reads and plans the open trip. Users connect
their own provider (Sign in with ChatGPT, or an Anthropic, OpenAI, Gemini or
OpenRouter key) or use the one the admin set up, including a custom
OpenAI-compatible server. It is an addon and only shows while a connection
is usable.

The chat runs on the existing MCP tools, pinned to the trip. Every change is
held as a change card the user applies or discards, and applied changes can
be undone. On the desktop it is a floating window that can be moved and
resized; on the phone it is fullscreen. Search results show as place cards
that can go onto a day, weather lookups as a strip, proposed places as
ghost markers on the map with a before/after switch, and an answer can be
shared into the trip chat. Booking files can be dropped in and are read
into change cards.
2026-10-22 21:30:12 +00:00
Maurice 31859651d7 feat(help): rewrite the help center for v5 with a Tours section, new pictures in every language, and fix the tour planner bugs it surfaced
Every help screen checked against the v5 redesign. Tours is documented for
the first time, as a screen of its own with twelve screens under it, and
notifications get help of their own. All pictures are taken again on a
realistic demo trip, with short loops where a still cannot show the motion.
The English is plain and every language is written naturally rather than
word for word.

The switcher shows two levels at most, a screen's overview lists its
sub-screens as cards above its guides, each wiki page is linked once, and
the help sidebar is wider.

Fixes found along the way: Save to Collection keeps a tour's route, a track
dropped on the places column opens the Tours import, tour Share on desktop,
the unsaved tour draft lives on the account, a route click no longer adds
an end point, Trip places leaves the trip's tours out, Escape closes a
stage's day menu, confirms before destructive deletes, DWD warnings sharing
one id kept apart, a booking's date moves with it, and smaller fixes across
notifications, bookings, the day plan, stays, Atlas, Collections, Vacay and
the admin screens.
2026-10-22 22:29:35 +02:00
Maurice 94674e7572 fix(tours): open the GPX share sheet inside the tap
Sharing a tour fetched the GPX file first and only then called
navigator.share, so the tap that asked had often run out by then. The
retry path kept that file for one more tap only, and a tap while a sheet
was still open made the browser throw InvalidStateError, which read as a
failed share.

The share button now fetches the file ahead where sharing is offered
(not while the planner has unsaved changes), and every tap hands a fresh
File built from the kept bytes to the sheet before anything is awaited.
A file a tap had to fetch is kept for the next tap. A tap while a sheet
is open is let be, and InvalidStateError and a closed sheet stay quiet;
only a real failure still says the download is left.
2026-10-20 20:56:19 +02:00
Maurice b460cfd9a7 refactor(tours): move non-component exports out of component files and split long hooks
Clears the client lint ratchet: helpers move to sibling .ts files for react-refresh, complex render paths get small helpers, floating promises are voided, the stage rail lets a stale request go during render, and the collections page reads the active list's tours and rights in ActiveCollectionTours. StageNumber is no longer exported.
2026-10-20 17:51:28 +02:00
Maurice 3fe34061fd feat(tours): keep whole tours in collections and take them into trips
A saved tour can now be kept in a collection, apart from any trip, and
later made into a tour of another trip. The bookmark button in a tour's
footer and in the planner's head (waiting for the save while there are
unsaved changes) opens a dialog with every list of the user; a list they
may only read stays in it, dimmed with a lock and a tooltip. The list
keeps an independent copy: the line, the type, every measure and setting,
the waypoints in order with their roles, the origin, and the trip and tour
it was saved from as they were called then. Its days, its links to places
of the trip and its obstacle decisions stay in the trip, and later changes
to the tour do not reach the copy.

A collection shows its kept tours as cards above its places: the grade
tile with the source badge, measures and the waypoint count as badges,
the trip it came from, and who kept it. The owner or an admin of the list
can remove one after a question; tours made from it stay. "Add to a trip"
shows the kept tour on a map with its facts and numbered waypoints, lists
the user's trips (the ones they may not add tours to dimmed), and asks
outright whether the waypoints come along. "Line only" keeps the line as
saved and adds no waypoints; a tour kept without a line can only bring
them. The new tour has an id of its own, no days and no new places, and
names the trip and tour it was saved from; the dialog then offers to open
that trip. Neither the collection nor the original tour changes.

On the server the kept tours are a table of their own in the tours domain
(collection_tours, migration 046100), each row written whole once. Every
copy, the in-trip "save as new tour", the take-over from another trip and
both directions here, now goes through one copy model (tour-copy.helpers)
instead of a second one. Reading and removing follow the collection's
roles, keeping one also needs access to the source trip, and taking one
over needs place_edit on the target trip; the kept tour is read again
inside the write, so one removed meanwhile leaves no half-made tour. The
MCP tools list_collection_tours, get_collection_tour,
save_tour_to_collection, take_over_collection_tour and
delete_collection_tour run over the same service and checks. All 27
locales carry the new strings, and the wiki describes the flow.
2026-10-20 15:32:47 +02:00
Maurice 3659297af1 feat(tours): import KML and KMZ routes as tours, with a preview first
A route planned in Google My Maps or Google Earth can now become a tour
without converting it to GPX first. The import button and a file dropped on
the tours workspace take .kml and .kmz files beside .gpx, through the same
preview, the same import routes and the same transaction as a GPX file.

The preview shows every line of the file on its own, numbered, with its
distance and heights; two lines of one placemark are numbered after its name
and never joined. Heights count only where they are heights: kept for an
absolute line, dropped for any other altitude mode and for the all-zero
placeholder My Maps writes, so a missing height stays unknown. What the file
holds besides its lines (pins, areas, overlays, links to other files) is
counted in a dashed card, and nothing is fetched from elsewhere.

The KML reading reuses the place import's codec. A KMZ only ever has its KML
entry unpacked, counted byte by byte against the decompressed cap whatever
its header claims. A file that cannot be read is refused with its reason
(damaged, an archive without KML, too large unpacked), and the toast names
the file and the reason. A KML or KMZ import keeps the gpx origin of a file
import; its provenance names the real format from the stored file name.
2026-10-20 15:30:18 +02:00
Maurice 554d6f1b9f feat(tours): a city walk prepared from a briefing over MCP, and the station fields an assistant reads spelled out (CW-10, CW-11)
An assistant connected over MCP can now prepare a city walk from what the
traveller tells it. propose_city_walk takes a start (and an end), a time
frame on a day of the trip, the themes of CW-05, the stations the
traveller wishes for, the places of the trip it may use and fixed
appointments with their times, and saves nothing. Places for the themes
come from the search along a tour, at most eight, each with the theme,
category and source that brought it; nothing is made up. The stations are
ordered on estimates over the straight distance, and a place of a theme or
of the trip goes in only where it is open on arrival, keeps the frame and
the appointments, and leaves every station already on the walk open when
the walk gets there. The walk in that order is then planned on foot by the
router the road trip uses (the traveller's own routing server where they
set one), and its legs give the walking distance and time from one station
to the next. Where the router cannot be reached, or a station lies more
than 300 m off every way (the planner's limit, now shared with the
server), the times are estimates over the straight distance with an
allowance for streets at 4.5 km/h, and the answer says so and why. The
answer gives the arrival and leave times, the stay where the traveller
gave it and unknown otherwise, which makes every later arrival the
earliest one, and the opening hours checked against the arrival on that
day with the helpers the planner uses. Walking and stays are counted
apart, the total is complete only when every stay is known, and the
minutes the frame leaves for the unknown stays are named. Free time before
an appointment or at the end, wishes and appointments that cannot be kept
as asked, and places left out with their reason are listed; a tour of the
trip offered as a station is named as a tour. A wish or an appointment is
never dropped; an appointment is a must and protected. A place's default
duration is not read as a stay: every place carries one, so it says
nothing.

Once the traveller confirms, create_city_walk saves the walk through the
same creation the planner's save uses: one write, a key for every station,
the links to the trip's places checked, the trip told, with place editing
permission on the trip. Its way on foot is planned by the same router and
saved with the walk, together with its distance and walking time. Where
the router gives no way, the walk is saved with its stations only and the
answer says why: no line, no distance and no walking time. The tours list
says so (route_pending), the tour detail says in one line that the way is
planned in the planner, and opening the walk in the planner plans its way
at once and offers to save it, with a note once it is planned that it was
just planned. Such a walk can be saved as a new tour and taken over into
another trip, starting at its first stop, and is never offered as a place
to stay. A tour without a line needs two points of its own.

get_tour_plan now spells out every field of a city walk's stations: key,
name, stay (null when not known), importance (null for normal), protection
(false when not set) and opening hours (null when none are known);
list_tours names the station count and whether a walk waits for its way,
get_city_walk_progress gives each station's key and opening hours, and
take_over_tour says a walk keeps its stations with their protection. No
tool changes a walk's stations; deleting a walk's place takes its stations
along, protected ones included, and both delete tools now ask the
assistant to name those and ask the traveller first. REST answers the
stations as before.

A tour create refused for a type that is switched off, a description that
is too long or planned times out of range is a domain refusal now, so an
assistant gets the same text REST sends; REST still answers it as an error
at 400.
2026-10-20 07:09:57 +00:00
Maurice 73237ffedb feat(tours): bike tours in the planner, CyclOSM, and the planned cards' last points (BIK-01 to BIK-06, RTE-01, IO-01, LIB-02)
The tour type is a dropdown; a bike tour picks its profile, routes for it,
says when a point lies off every way, and shows what it runs on: surfaces,
kinds of way and, for a mountain bike, the trail grades, each pointed at on
the map. A type's own map (OpenHikingMap for a hike, CyclOSM for a bike tour)
is offered while a tour of that type is open, with an admin switch each.

From the planned cards: a hike avoids steep climbs, steps or ferries, nothing
else; a saved tour can be shared where the platform takes GPX files; and the
tours rail adds a tour from one menu: plan new, import GPX, or take one over
from another trip.
2026-10-19 21:33:49 +02:00
Maurice 03d6457bde feat(tours): paddle obstacle card, admin regions panel and obstacle marks on the route band
PAD-05B: a click on an obstacle on the map or on the route band opens its card in the look of the via ferrata card: the kind with its glyph, the name, how it stands to the route (on it at a distance along it, near it with the link marked unclear, or only in the area), the paddling rule its tags give with canoe read before boat, a condition as written, height, grade, hours and fees, the source with the date of the data and the OpenStreetMap link. A missing rule reads as unknown, never as allowed, and a portage way recorded as private says so. Objects only in the area show from zoom 13 on, on Leaflet and GL alike; on GL the lines only near the route are dashed as on Leaflet, one click handler picks the marker over the line, and a picked marker no longer flickers. The route band marks the obstacles on and near the route by their share of the measured line. The obstacle notice also names an answer cut at its limit.

PAD-05A: the admin area gets a Paddle water data card under the addons, on desktop and phone: a search in Geofabrik's list with each region's path, the regions with their state (waiting, reading with its progress, ready with objects and data date, failed with the reason), reading again and removing after a confirmation. The list follows a read without reloading. Region changes write audit rows.

Map colours of paddle objects and portages come from theme tokens read at draw time, as the via ferratas do; the ferrata layer now shares that reader. The rail takes the planner page's parts as one bag, the map hit tests of the tour layers are one helper, and the unused noWaterData string is gone from every locale.
2026-10-16 23:03:09 +00:00
Maurice 2c3b20bb57 feat(tours): paddle portages and locks, paddle time, wind at the start and obstacle data from OpenStreetMap
PAD-03 portages and locks on planned tours and recorded tracks, PAD-07 paddle time with portage and lock stops, PAD-08 wind forecast for the start, PAD-05A regional obstacle import from Geofabrik extracts with admin endpoints and a monthly refresh, PAD-05B first cut of the obstacle layer on Leaflet, GL and 3D. Work in progress: checklist (PAD-06), check bench (PAD-09), admin UI, translations and tests follow.
2026-10-16 23:44:58 +02:00
Maurice eb4e74b5c3 fix(forms): keep password managers out of a place's contact fields and the admin user forms
1Password filled the signed-in user's login into a place's e-mail field, and
offered the admin's own credentials for an account being created. The fields
now carry each manager's opt-out; autocomplete=off alone is ignored.
2026-10-14 13:24:27 +02:00
Maurice 10a987fec1 fix(files): list a hotel booking's files and files pinned to a stop on the place card
A hotel booking reaches its place through the stay, not its own place_id, so
the card listed the booking but not its confirmation. Links to a stop, which
MCP's link_trip_file and plugins can make, were never returned by the file
list at all; it now carries linked_assignment_ids. Without files the row no
longer opens to nothing.
2026-10-14 13:24:27 +02:00
Maurice 18baafe57d refactor(pdf): the remembered preview switches move to utils 2026-10-12 21:14:28 +00:00
Maurice 7b023c6b54 refactor(map): the road trip via helpers move to utils, so the route hook imports no component 2026-10-12 21:14:05 +00:00
Maurice 3b933470d3 fix(planner): a booking pinned to a stop on another day is shown at that stop only, not on two days
A booking linked to a stop has two days to go by, its own date and the
stop's. They part when the stop is moved to another day or the trip is
shifted with "keep bookings on their dates", and the plan then listed the
booking at the stop and again in the timeline of its own day. The stop now
wins wherever both are drawn: the desktop day plan and day detail, the
phone's plan tab and day sheet, and the road trip rail on both. The
booking's date is left as it is; a stop on one of the booking's own days
changes nothing, so a rental picked up at a stop still runs across its
other days.

Refs #2604
2026-10-12 20:42:30 +02:00
Maurice bdd8c08dd4 fix(planner): places put on a day, day names, day notes and trip details changed offline wait in the queue and sync once the connection is back
The day plan wrote straight to the API, so offline a place dropped on a day
snapped back with "Error assigning place", a renamed day or a new day note
failed, and a moved stop, a leg's travel mode or who joins a visit were lost.
Visits (put on a day, taken off, reordered, moved, travellers, leg mode), the
day's name, notes text and travel mode, and the day notes (add, edit, delete,
move) now go through assignmentRepo, dayRepo and dayNoteRepo and the mutation
queue: the cached day changes at once, a visit or note made offline gets a
temporary id that the edits after it wait for, and an online write held behind
an older queued one joins the queue, since none of these carry a version
token. A new order of a day that names a visit made offline is sent with its
real id, or without it when its create failed. Desktop, the phone timeline and
note sheet all write through the store, so both are covered.

The replay reads each answer for what it names: the day answer keeps the
cached notes and visits, the participants answer lands on its visit, and a
visit the server refuses with 409 (a Tour already on that day) fails instead
of waiting as a conflict no retry can solve.

The trip's name, description, reminder and the archive flag wait in the queue
too, from the planner, the phone and the dashboard. What only the server can
do says so instead of failing: new dates, a day count, a currency or a cover,
a new trip, a copy or a delete, members, guests, share and invite links, and
a new order of the days, which moves the dates of the bookings.

Refs #2601
2026-10-12 20:17:57 +02:00
Maurice 92af437ff6 fix(roadtrip): the stop a booking put on its check-in day keeps the night over a rest later that day
The night went to the last visit of the hotel. On a day without hours the
booking seats its own stop first, and Days does not show that stop, so a rest
the traveller put mid-day was the last visit: it was held to the check-in and
the places behind it followed from there, the way the report describes.

The night is now the visit that ends the day, else the booking's stop, else
the last visit. A visit the traveller adds to the check-in day no longer moves
the night or its check-in. nightVisits in shared holds the rule, and the road
trip, calculate_roadtrip, the stored ride slot and the stop editor all read it.

Refs #2636
2026-10-12 16:05:17 +02:00
Maurice cb04ebe6d2 fix(trips): the trip form drops its complaint once you edit, and the typed date hint follows your language
"Title is required" stayed on screen after the title was typed, until the
next save. Any edit in the trip form, desktop and phone, now clears the
last save's complaint; the next save checks again.

The keyboard date field always hinted DD.MM.YYYY, while it was filled and
read back in the user's own order: a Korean user saw "2026. 12. 10." under
a German hint. The hint is now the locale's own pattern (YYYY. MM. DD. in
Korean, MM/DD/YYYY in US English), and the field's name is translated.

Refs #2604
2026-10-12 15:46:02 +02:00
Maurice dbbbd07b1c fix(costs): amounts follow their currency's decimals and format, and payments count on the local day
Several small things in the costs views, on desktop and phone alike:

- The outstanding card no longer says "You're all settled up" while a
  payment is still open in Settle up.
- The custom split shows its suggested shares as a faint placeholder in
  the currency's own form (30000 for won, not 30000.00), so it no longer
  reads like an amount already typed.
- Amount fields take only the decimals the currency has: won and yen take
  none, KWD and the other three-decimal currencies three. A switch of
  currency cuts what was already typed. 1000.555 won was saved as typed,
  listed as 1,001 and reopened as 1001.
- The expense sheet on the phone prints shares and sums with the same
  formatter as the list, with grouping and the currency's decimals.
- A payment recorded with Settle or Settle all is dated on the viewer's
  own day, and one without a date of its own is grouped by its UTC record
  time read on the local calendar. In Seoul before 09:00 it landed on the
  day before.
- "1 expenses need a payer" now uses the plural forms of each language.

Refs #2604
2026-10-12 15:04:32 +02:00
Maurice 93173b1573 fix(planner): bookings a collaborator moves on a day take their new slots in every open list
reservation:positions was on the ignored list, while the stop order the same
reorder sends was applied, so in other tabs and for other members the bookings
sat between the wrong stops until a reload. The event now writes the slots the
way the mover's own list did, for the day it names (day_id from REST, dayId
from the MCP tool) or as the global slot without one, and caches them offline.

A booking handed back by an edit or a reservation:updated event without day
slots keeps the ones it had, in the store and in the offline cache.

Refs #2631
2026-10-12 12:33:19 +02:00
Maurice 12f08e3646 fix(planner): a timed booking dropped against its time goes back alone instead of taking the stops behind the drop along
The day list draws a row with a time by that time, and an untimed stop takes the
time of the timed row before it. A transit ride dropped behind a later stop was
stored there, drawn back at its time on the next render, and every untimed stop
behind the drop went with it; the next drag then stored that shuffle. A dropped
booking or timed note now lands in the slot nearest the drop the list draws it
in, so nothing else moves and what is stored is what is shown. Rides dropped
where their time allows stay on the drop, and the rule for untimed stops behind
a booking is unchanged.

The chronology check for a timed stop reads a booking's time for the day (the
arrival on the last day of a multi-day ride) instead of its departure, and the
three copies of the remove and insert step share one helper.

Refs #2631
2026-10-12 12:33:12 +02:00
Maurice 565333a90c fix(roadtrip): a hotel visited again on its check-in day is the night only at its last visit
A stop counted as the booked night whenever a stay began at its place on its
day, so a hotel on its check-in day twice (the booking's own stop and a rest
the traveller planned there) was read as two nights. Both were held to the
check-in: the rest waited for the room or was set back to it, and with a check-in
far from its arrival the rest and every stop behind it moved onto the next day's
card.

oneNightPerStay in shared keeps the night and its check-in on the last visit of
the place and turns the earlier ones into ordinary stops, still a hotel. The
browser and calculate_roadtrip both run it, the stored ride slot times only that
row by the check-in, and opening an earlier visit in road trip mode opens the
stop rather than the night.

Refs #2636
2026-10-12 11:05:41 +02:00
Maurice 0246e5c317 fix(map): a distance between near antipodal points is half the globe instead of NaN
haversineKm in src/utils/geo.ts answered NaN for some near antipodal pairs,
for example 47.3812, -10.8827 and -47.3812001, 169.1173. Rounding pushed the
haversine term a hair past 1, and asin of a value above 1 is NaN, which then
spread into route sums and range checks.

Cause: unlike the Studio legKm and the roadtrip haversineKm in
@trek/shared, the shared client helper had no clamp before Math.asin.

Fix: clamp with Math.min(1, ...) before Math.asin. With the clamp the helper
runs exactly the operations of Studio legKm in the same order (2 * 6371 is
12742), so Studio now uses haversineKm and legKm goes, with its cases carried
into the new geo tests. The roadtrip variant multiplies its terms in another
order and gives different last bits, so it stays as it is.

Red test: src/utils/geo.test.ts FE-UTIL-GEO-002 failed on the unfixed code
with "AssertionError: expected NaN to be close to 20015.086796020572,
received difference is NaN, but expected 0.0005".

Removed tests: src/components/Studio/legKm.test.ts went with legKm. Its four
cases (zero for one point, a degree of latitude about 111.19 km, the same
both ways between 8700 and 8900 km, 0,0 to 0,180 equal to 12742 * PI / 2) now
run against haversineKm as FE-UTIL-GEO-001, 003 and 004.
2026-10-09 19:00:45 +02:00
Maurice 60e8f1cd34 refactor(roadtrip): read the wall clock minutes and today through localDate
useMRoadtrip and useMRtCorridor each kept a nowMinutes helper and their
own check whether the selected day is today. Both now use localMinutes
and isLocalToday from utils/localDate.ts, which hold the same rules.
2026-10-09 16:15:49 +02:00
Maurice 81decb843d refactor(admin): share the GitHub release history between desktop and phone
useGithubReleases holds the paged fetch, the prerelease filter, the expanded rows and the date format, and parseReleaseNotes splits a release body into the blocks both panels draw. The phone keeps its page walk past hidden prereleases through the fillPages option, its repository link and its own inline link scan; the desktop keeps one page per load.
2026-10-09 15:19:44 +02:00
Maurice 9529b55bd7 refactor(planner): share the booking files filter between desktop and phone
filesFor moves out of the desktop booking parts into utils, and the phone
transport sheet now reads a booking's files through it as the desktop
transport detail and booking cards do.
2026-10-09 07:16:40 +00:00
Maurice 20d7e62afc refactor(planner): share the day plan reorder writes between desktop and phone
The desktop day plan and the phone timeline derive the place order, the note
and transport positions and the per leg positions of a reordered day from
mergedOrder, and write the moved bookings back through the same helpers.
2026-10-09 07:16:40 +00:00
Maurice 9f3e1f8c83 refactor(weather): share the day weather anchor and icon map between desktop and phone
The day plan, the day panel, the phone timeline and the phone day sheet read
the day local weather anchor from dayWeatherAnchor, and every weather icon
comes from one map in weatherIcons.
2026-10-09 07:16:40 +00:00
Maurice 9a7c17e7cc fix(i18n): a quantity under another name than count is a plural group too
The count-string gate only knew {count} and {n}, the params t() picks a
plural form by. A quantity named anything else kept one form in every
language and passed: Russian read "Все {days} дней" for 2 days and
"{objects} объектов" for 3 objects, Greek "{minutes} λεπτά" for 1 minute.

Twelve such en strings become plural groups with their forms in all 27
locales, the quantity renamed to {count}: the trip length limit, the
chat image limit, the API key limit, the storage move prompt and its
reclaimable line, the Dawarich wish rule and suggestion duration, the
road trip lateness warnings, and the road trip echo and window texts.
Their callers pass the count as a number; the storage panels used to
pass it as a string, which no form is picked by. The reclaimable line
loses its dash for a comma in every locale while it is rewritten.

i18n-parity now treats the quantity names in QUANTITY_PARAMS ({days},
{minutes}, {objects}, {max} and the like) like {count}. The four strings
where the number is followed by nothing but a unit symbol (the Dawarich
durations, the upload size limit) go on NOT_PLURAL with that reason.
2026-10-08 13:04:42 +02:00
Maurice 0126ef3401 fix(client): the version notice asks about a change during the session only
Three gaps in the notice from the previous commit, found in review.

A page served from the network and claimed later by its own build's
worker counted as already taken over, so Reload went to that worker,
got the old build again and spent the session's reload: no notice and
no new build until the next start. Only a worker that replaced the one
that served the page skips the handover now, as at launch.

A reload that came before the new worker could take over (a slow
precache, a failed update, an install gone redundant) also spent that
guard, so the launch behind it could no longer finish the handover with
its own reload. That reload leaves the guard alone now.

An install whose server never reports the bundle's version (a hand-set
APP_VERSION) showed the notice in every session, and Reload could not
clear it. The store keeps the first version a page hears of, from the
app config at launch, and the notice only asks when a later report
differs: a different build at launch is the launch handover's to fix.

The server's wire-envelope test pins the welcome frame's fields and now
expects the version beside the socket id. The wiki's update page says
what an open tab shows after an update.
2026-10-08 00:01:33 +00:00
Maurice a094f79efa feat(client): offer the new version when a release lands during a session
A tab kept open across a deploy ran the previous build until its next
start. The server now names its version in the realtime welcome frame,
which every open client reads again when it reconnects after the
restart a deploy brings. When that version is not the build the page
runs, a notice offers to reload. The reload goes through the same
service worker handover a launch uses: it waits for the new worker to
take over, or for a short timeout, and records the version only when
the takeover happened.

The notice reads the error boundary's texts for the same situation, so
every language has it already. It is one component for both shells,
sits above the offline pill, stays away offline and after a reload for
that version that did not bring it, and can be closed for the session.
2026-10-07 22:40:12 +00:00
Maurice c3c21fb0b4 fix(client): five loads that catch their own error recover from a deploy too
The PDF export on desktop and phone, the language switch, the holiday
regions and the PDF's map image load a chunk on demand and catch the
failure themselves. After a deploy the old page asks for chunk names the
new build no longer has, so they showed their own error on every retry
(or, for the language, silently stayed in the old one) instead of
reloading onto the new build the way an uncaught chunk error does.
importChunk reloads once for a missing chunk and still hands the caller
its error.
2026-10-07 17:05:36 +02:00