Commit Graph

1594 Commits

Author SHA1 Message Date
Maurice a1f97ba548 style: format two ai-connections files 2026-10-21 22:29:58 +02:00
Maurice 77ad0a06d0 test: factories hand their overrides over apart from the row
The assistant tables gave Users three more relations, and a spread of
overrides checked against RequiredEntityData now ran tsc past its
instantiation depth (TS2589). Every factory passes its overrides as
createRow's own argument, the way rows.ts documents it.
2026-10-21 22:29:58 +02:00
Maurice 3c3a228737 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-21 22:29:58 +02:00
Maurice b6f7488591 fix(assistant): green CI round, trip notes the assistant can keep, calmer change cards
- the assistant keeps facts for a trip (remember_for_trip) as a change card,
  applied into the user's trip notes; existing expenses can be changed
- change card fields as a few icon chips with the rest folded under Details,
  the day band marks only what changes, the week grid as compact tiles
- no map button on receipts of things that have no place on the map
- tests follow the composer's accessible name, the receipt's status icon and
  the GL map's container; the hanging stream helper honours an early abort
- Postgres probe: qualified columns in the upsert updates; entities restored
  to the generator's output; strict ratchet reads the SDK client as untyped
2026-10-21 22:29:57 +02:00
Maurice 26df56d76c feat(assistant): one AI tile for assistant, parsing and MCP, a planner-aware chat
The three AI addons share one tile in the integration column; a dialog holds
the connection, the assistant's limits and AI parsing, which now reads with
the assistant's connection unless an install keeps its own. Existing parsing
configs stay where they are and keep winning.

The chat sees the map on screen, offers next questions under an answer,
shows its live step in the window head, previews waiting changes inside the
day list, checks whether a day still works, folds a multi-day plan into a
week grid, keeps per-trip notes and can estimate costs. An empty or cut-off
answer gets one push instead of ending silently, and the assistant's own
step and size limits are raised.
2026-10-21 22:29:57 +02:00
Maurice 214984f398 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-21 22:29:56 +02:00
Maurice 3101b36f3d fix(journey,tours): read a day's stays as a read, give the services the request context in the e2e runs and hold the tests to the foot-only walks and the translated notices 2026-10-21 21:16:36 +02:00
Maurice 5dc945145f fix(journey): keep the journey page and maps within their lint gates, format the journey locales 2026-10-21 20:00:07 +02:00
Maurice d62fc284e1 fix(journey): share the stays of a day report only with the recorded days, and hold the share read tests to the two new flags 2026-10-21 19:08:25 +02:00
Maurice 8f53d98d45 feat(journey): show the plan, what was recorded, the entries and the photos as layers of the journey map
Every journey map (the desktop aside, the phone screens and the shared page)
now carries four switches: Plan, Recorded, Entries and Photos, each with its
count, a spinner while it loads, the warning tone when it could not be read
and the reason when it has nothing to show. A switch only hides, in place:
the map is not rebuilt and keeps its view, and the choice is kept in this
browser per journey (per link on a shared page, by a hash of its token).

The plan draws the places the linked trips put on their days as hollow dashed
pins and the trips' GPX routes dashed; a place an entry was written about is
not drawn twice, the entry's pin carries a check instead. The stays a day
report was taken over from show as dots in the colour of their day. Marks on
one spot join into a disc with their number and open as a list; a picked
place or stay opens a card with its trip, days and times and leads to its
entry. A key beside the switches explains every pin and line and says that
a plan is no proof of a visit. The phone timelines now show the photos too.

The GPX routes come with the map's layers through the linked trips, read
only while the journey's track switch is on; the frozen /tracks answer stays
Studio's. The share settings on desktop and phone get Trip plan and Recorded
routes under Map, off by default and only switchable while the map is shared.
2026-10-21 18:55:54 +02:00
Maurice 9b71948497 feat(journey): read what a journey map shows beside its entries, and share its plan and recorded days on purpose
GET /api/journeys/:id/map-layers answers the planned places of the linked
trips (one per place, with every day the plan puts it on and the entry
written about it), the trips' GPX routes while the track switch is on, and
the located stays entries were taken over from. It goes through
journey_trips, so a trip nobody wrote about yet shows its plan too. Every
source is read on its own: one that fails says unavailable and the others
still draw. A position that is not one is left out, never moved.

GET /api/public/journey/:token/map-layers cuts the same on the server by the
link: nothing while the map is not shared, the plan and its routes only with
share_plan, the recorded days only with share_recordings and only beside a
published entry, stays only of published entries. Migration 046190 adds both
flags to journey_share_tokens, off by default, so a journey that is public
already shows what it showed before. The share link REST route and the MCP
share tool take them alike; get_journey_map_layers reads the layers over MCP
on the same access, a route by its size only.
2026-10-21 18:15:24 +02:00
Maurice bb10b94046 fix(journey): mark only the days a run reaches as prepared, name trips out of reach, show editors their own status in the day review and say when a fetch is already running 2026-10-21 17:53:39 +02:00
Maurice 94c369b913 feat(journey): prepare the recorded days of a journey's trips in the background, switched on per journey, with each day's state
Journey Settings get "Prepare recorded days automatically" under GPX Tracks,
off by default and set by the owner. With it on, the Dawarich background
check also reads the linked trips of the journey for the owner and every
editor with a connection of their own, within the same 400 day reach and
windows as the trip sync, and only for new and changed stays: a trip the
person travels on is not asked twice, and the stays of a trip they are not
on are filed under no trip. Nothing is written into the journey; the stays
wait at their day, and a stay taken over or left out is never offered again.

Under the switch the caller sees when their stays were last fetched, or why
not (no connection, no dated trip, background check off, the reason of the
last failure beside the state still shown), with a "Fetch now" that the
switch also runs when it is turned on. A day of the trips that is not final
yet carries a dot on its take-over button, on the phone in the stay sheet,
and the tooltip says whether it is still running, was fetched before it
ended, was not fetched yet or shows an older state after a failed refresh.
The day review says so for today too.

Server: migration 046180 adds journeys.auto_prepare_recordings and
journey_recording_prep (the last attempt, last success and last error per
journey and person). JourneyRecordingPrepService in the journey domain lists
the prepared trips and keeps the status; DawarichSyncService walks those
trips in the same loop as the person's own and records each journey's
outcome. GET /api/journeys/:id/recordings/prep answers the caller's status
under a @trek/shared contract, the MCP tool get_journey_recording_prep reads
the same, and update_journey takes auto_prepare_recordings like the PUT
contract. The column stays out of the plugin output. All 27 locales carry
the strings; the Journey, Dawarich and MCP tool wiki pages describe it.
2026-10-21 17:39:33 +02:00
Maurice 3f1d1a57cc fix(journey): take a day over from the phone stay sheet, keep a picked position inside its group and send stays without a place unpinned
A journey on a phone with stays on days that hold no entry offers each waiting day in the Dawarich stay sheet, so a day can be built from its stays alone; desktop and phone read the same days through openStayDatesWithin. A position picked for a group falls back to the longest stay with a place once that stay is unchosen or split off, a merged entry of stays without a position goes out unpinned instead of being refused, and a stay taken over meanwhile is never joined across. review_journey_day_stays is no longer marked read-only, since it refreshes the caller's stored stays. The already-taken notice no longer claims the stays were in this journal. An e2e case checks the drafts stay off the public share.
2026-10-21 16:49:47 +02:00
Maurice 7c1de6ed82 feat(journey): take a recorded day over with its stays, chosen, renamed and joined into drafts or one day report
A journey day now has a list button in its header beside the route, with the
number of stays still waiting, and the row of the day's Dawarich stays gets a
"Take over day" button; on a phone the journey's More options offers it for
the day of the card shown. The dialog reads the day again from the caller's
own Dawarich and shows the route and the stays on one map, numbered in the
order they were lived and pinned in the colour of the day, beside a list with
times, durations, the stays Dawarich only guessed or is unsure about, the ones
without a position, the entries the journal has on the day and how many people
will see the drafts. When Dawarich does not answer, it says why and shows the
stays of the last sync.

Open stays start chosen, the same place twice in a row starts joined as one
entry under a title and a chosen map position, any two neighbours can be
joined or kept apart, a name can be corrected and a stay left out. "Day
report" turns every chosen stay into one entry that lists them with their
times, at a chosen stay or off the map. The day's route can go along when the
journey keeps none yet. Nothing is written before "Take over N drafts", and a
blank name or title holds it back with the reason. Stays left out are dismissed
and can be restored in the stay list.

The take-over writes drafts only, never touches what the journal holds, and is
one transaction: every stay is checked to be the caller's and of that day, the
entries, the stays they hold and the marked suggestions commit together, and
the entries are announced after the commit. A stay the journey holds already
is reported instead of written twice, so a retry after a lost answer is
harmless; a failed attempt reads the day again and keeps the choices, and a
route that fails after the drafts can be kept again alone.

Server: migration 046170 adds journey_entry_stays (one row per stay of an
entry with its own name, place and times, unique per journey, source, person
and visit, gone with its entry), owned by the journey domain through
JourneyEntryStaysService. The single stay accept records its stay there too,
over REST and MCP alike. DawarichSyncService.refreshDay reads one day's visits
and upserts them without flagging anything missing; DawarichDayStaysService
lists and settles a day's stays. JourneyDayImportService in journey-recordings
serves POST /api/journeys/:id/recordings/:date/stays/review and /import with
@trek/shared contracts, and the MCP tools review_journey_day_stays and
take_over_journey_day_stays run on the same service and checks. createEntry
can write without announcing for a caller that announces after its commit.

The desktop dialog and the phone sheet of the route (JRN-01) and of the
take-over share one frame each. All 27 locales carry the strings; the Journey
and Dawarich wiki pages and the MCP tool list describe it.
2026-10-21 16:31:40 +02:00
Maurice 5116f7be2c fix(journey): refuse the second of two saves of a recorded day at once unless it asked to replace 2026-10-21 15:18:01 +02:00
Maurice 4f60cc5c98 fix(integrations): cut a journey day in the zone of its own first track, never in the UTC Dawarich writes times in 2026-10-21 15:18:00 +02:00
Maurice 65bb2d95eb feat(journey): keep the route recorded on a journey day from Dawarich, previewed first, with its gaps
A day header in the journey timeline now carries the Dawarich mark for an
editor whose Dawarich answers (dimmed with the reason while offline). It opens
a dialog that names the journey and the day and reads that one local day from
the caller's own connection: a sketch of the route, distance, first and last
time, pieces, gaps without recording, modes and whether tracks or thinned
points answered. The day is cut in the zone Dawarich records in where its
answer shows one, else in this device's, and a picker cuts it in any other.
Nothing is kept before "Keep route". On a phone the same sheet opens from the
journey's More options for the day of the card shown.

What is kept is the preview that was seen: the save reads the day again and
refuses with recording_changed when its hash moved, and the dialog shows the
new version. The same day taken over twice keeps one version and announces
nothing; a different version is replaced only on "Replace", never touching an
entry or text. A failed read throws before any write, so the kept version
stays. Gaps stay gaps: points are split where silence ends elsewhere, and no
day is joined to the one before. The dialog says how many other people will
see the route; editors may keep their own recording.

Kept days show in the day header (distance, whose recording, since when,
still running) and on the desktop and phone journey maps in the colour of
their day's pins, winning over the live trail of their date, and stay drawn
without Dawarich. A trip's GPX track is now drawn dashed as the plan, recorded
routes solid. Shared links do not show recorded days. Remove route asks first
and works without a connection.

Server: migration 046160 adds journey_recorded_days (one row per journey,
local day and source, segments as JSON, zone, window, fetch time, hash,
importer kept past their account). The journey domain owns the rows through
JourneyRecordedDaysService; the new journey-recordings domain takes a day
over through DawarichTracksService.fetchDay, which reads past the cache, asks
a window wide enough for every zone and refuses with DomainErrors. REST under
/api/journeys/:id/recordings (list, preview, PUT and DELETE /:date) with
@trek/shared contracts, and MCP list_journey_recorded_days,
preview_journey_recorded_day, save_journey_recorded_day and
delete_journey_recorded_day on the same service and access. A new user event
journey:recording:updated keeps every open map current. JourneyTrack gains an
optional kind. All 27 locales carry the strings; the Journey and Dawarich wiki
pages and the MCP tool list say what it does.
2026-10-21 15:05:21 +02:00
Maurice cbca3b2851 test(maps): the overpass place names cases get a suite of their own, the service suite back under its size 2026-10-21 13:53:24 +02:00
Maurice 8a75712041 fix(tours): tell a router that did not answer from a walk no way joins, count each boat of a leg on its own into the day and its arrivals 2026-10-21 13:53:21 +02:00
Maurice 6e13f7e655 fix(tours): keep a city walk on foot across the water, mark a crossing by boat apart from the walking time and save it only once taken 2026-10-21 13:16:43 +02:00
Maurice 41f3267503 fix(tours): name every derived field of a walk's remaining time, read a second run on the same day, say when only the way back is left 2026-10-21 12:35:02 +02:00
Maurice 43353b9394 feat(tours): read the walking time left of a city walk on a day over MCP, apart from its stays and the whole walk 2026-10-21 12:16:57 +02:00
Maurice 4eaf5e6a11 fix(tours): read e-bike chargers as mapped, keep unnamed ones on the map search 2026-10-21 12:05:38 +02:00
Maurice 1a722e63fb feat(tours): find e-bike charging points along a bike tour, only those tagged for bicycles, with what they state 2026-10-21 11:58:39 +02:00
Maurice 924e38472e fix(maps): stretches beside the overpass probe wait for it, empty note only after a full search 2026-10-21 11:43:04 +02:00
Maurice bf2efca8f0 fix(tours): let a failed search along the tour really ask again, with a probing breaker, hedged mirrors and a countdown 2026-10-21 11:35:03 +02:00
Maurice 988890866a fix(ski-routing): sort the network cache key by code unit, not by default 2026-10-20 23:18:54 +02:00
Maurice bed9f53615 fix(deps): raise the MCP SDK to 1.32 and hono to 4.13.11
GHSA-22jm-h49p-29qw and GHSA-6prh-2h8m-c8cw (MCP SDK, high) and
GHSA-5r4p-p66f-jhc7 (hono, moderate) are fixed in these versions. npm audit
for what the server installs reports none left.
2026-10-20 22:29:33 +02:00
Maurice b71e32cf12 fix(tours): clear the last lint, knip and wiring findings of the follow-up
The unused day pills go, a client-only type stops being exported, the place
form context moves out of the page hook so its view model stays the first
object it returns, the place form test helpers split the stub component
from the plain setup, and four router files are formatted.
2026-10-20 22:13:25 +02:00
Maurice e6002444a1 test(server): narrow a partial ways answer the way the server build can, format three tests 2026-10-20 21:56:06 +02:00
Maurice 20537193b6 test(tours): expect ski_options null on non-ski rows, let the ski choice settle before asserting 2026-10-20 21:43:47 +02:00
Maurice 60611aee65 test(server): run sequential awaits through oneByOne instead of a loop 2026-10-20 21:43:47 +02:00
Maurice 6c66cff365 fix(ski-routing): no promise in a condition and no await in a loop, ski route type from its schema
ski-areas.service checks the pending load and download against null and
undefined, the area refresh lane recurses instead of awaiting in a loop,
TourSkiRoute is inferred from its schema, the ways answer is read by
complete === false, and /v1/routes no longer needs a truncated field.
2026-10-20 21:43:46 +02:00
Maurice e5c5ffbc00 fix(tours): one get_tour_plan description for routed ski legs, entities as generated
A ski leg is routed where the piste router planned it and free elsewhere;
the description said both. The collection tours entity takes its generated
form again after the merge.
2026-10-20 21:17:08 +02:00
Maurice 562176bd83 chore(db): number the ski options migration after the Tours default 2026-10-20 21:16:09 +02:00
Maurice ae699187f7 fix(tours): open a saved ski tour as saved and keep what its router planned
A saved ski tour now opens with the line, heights, time, build and
planned legs it was saved with, read from the place this device keeps,
and nothing is asked of the piste router until a point, an option or the
discipline changes. Opened offline, while TREK Places is down or with
piste routing switched off, it no longer turns every leg straight, and a
rename saves the planned tour as it was. A kept line whose length is not
the tour's is another save's, and the tour is planned afresh.

- the save names the legs the router planned (ski_planned_legs, checked
  against the tour's own legs); get_tour_plan reports each leg routed or
  drawn from them instead of from the build alone, a tour that names none
  counting as planned only with the router's time
- copies keep ski_options in every path: in the trip, taken over, kept
  in a collection and made from one (collection_tours gets the column in
  the same unshipped migration)
- the route summary shows the data build, the km of runs, the height
  skied down on them and the lift rides, and the credit the server sends
  in place of a fixed one; a tour opened as saved says so
- answers refused while piste routing is off, or planned on an older kept
  copy, are not remembered, so they are asked again later
- terrain heights between known ones are filled in one walk along the line
- i18n: five summary keys in all 27 locales
2026-10-20 21:15:45 +02:00
Maurice 1d72fbd25f fix(ski-routing): keep one job's timeout to itself and bound the area refresh
The router thread now takes one job at a time from a queue kept on the
main thread, so a job's clock starts when the thread takes it up. A job
that times out ends only its own thread, and only while that thread is
still the current one; the jobs in line go to a fresh thread instead of
waiting out their own timers and killing the new thread in turn. A
thread that dies fails the job it held, and a thread that cannot start
fails each job on its own.

- the daily refresh fetches changed areas two at a time and stops once
  the TREK Places breaker opens; the rest keep their older copies
- the winter index cap is on the unpacked JSON fetch hands over, so it
  follows from the host's 8 MB gzip cap (64 MB) instead of matching it
- a build name is read to the rule a ski tour stores it by, so a build
  TREK could not save is refused when the winter layer is read
- an unused import in the piste router contract is gone
2026-10-20 21:15:21 +02:00
Maurice a85a50e186 feat(tours): keep a ski tour's piste router options, build and time
A ski tour now saves the options its legs were planned with and the piste
data build they ran on, as one JSON column (a new migration, since the
ski migrations have shipped on this branch); the server keeps only the
options its discipline reads. Both stay off every other type. The refusal
of a ski time is lifted: the piste router works one out, and the planner
sends it only when every leg followed the piste data. get_tour_plan names
the legs of a tour the router planned as routed, and list_tours says what
the new fields hold.
2026-10-20 21:14:44 +02:00
Maurice c8eb510bd4 feat(ski-routing): plan ski tours on the piste graphs of TREK Places
A new Nest domain plans the legs of a ski and winter tour on the runs,
lifts, cross-country trails, ski touring and winter paths the Places host
builds from OpenStreetMap, one SGR1 graph per ski area. TREK fetches the
index once, downloads an area by its id when a planner first plans in it,
checks it against the index's SHA-256 and keeps it in data/ski; no
clicked point ever leaves the server. A leg whose area is slower than two
seconds answers loading, and an older copy stands in while the host is
down.

The reader decodes the writer's golden file byte for byte, including the
two edge flags it added (area_has_run, type_unknown). The search is A* in
a worker thread over a profile per discipline (alpine, snowboard,
cross-country by technique, touring, snowshoe, winter hike) with the
times and penalties of the cost model, up to five snap candidates per
point, lift stations as entry points, legs that go on from each other,
pieces of a split network stitched by their boundary nodes, other ways by
the penalty method, and a failed leg explained by relaxing one rule at a
time (difficulty, lift class, technique, riding down, uphill) without ever
handing out the relaxed line.

POST /api/trips/:tripId/ski/route, GET .../ski/area-near and
GET .../ski/network/:areaId sit behind Tours and the trip, with
plan_ski_route as their MCP twin. A daily job looks for a new build and
sweeps areas unused for 90 days. The admin switch ski_routing_enabled is
on wherever TREK Places is.
2026-10-20 21:13:48 +02:00
Maurice beea4c762b refactor(server): move the routing heap and the metre helpers out of the paddle domain
The binary heap and the flat-frame distance helpers are not about water;
the piste router needs the same two. They move to src/lib/geo, where any
domain may import them without reaching into paddle-routing. Nothing
changes in what they do.
2026-10-20 21:12:15 +02:00
Maurice d924dcf99d fix(tours): keep the Places layers off the search breaker and keep partial way answers
The ways and routes layers now count failures on a breaker of their own, so
busy or slow layer answers no longer send place search to the fallback for a
minute, and a layer's 404 no longer clears the count of search failures.

When a later batch of /v1/ways fails, the batches that answered are kept and
only the ids still open go to the Overpass mirrors.

The wiki, the compose file and the chart now say that the way ids of a planned
tour and the horse routes box go to the TREK Places API.
2026-10-20 21:12:09 +02:00
Maurice 371083ed47 feat(tours): read way tags and horse routes from the TREK Places layers
The trail grades under a tour (BIK-05, MAP-03, MAP-04, RUN-02) and the
horse routes around a map view (HOR-01) asked the public Overpass mirrors.
The places host now serves both from its own planet build: GET /v1/ways
for the tags of ways by id and GET /v1/routes for route relations per box,
each in Overpass element form.

trek-places.client gains trekPlacesWays (batches of 500, one after
another) and trekPlacesRoutes, both through getJson with its off switch,
breaker, timeout, size cap and instance header, each element validated on
its own. TrekPlacesLayersService in the maps domain hands their answers to
the tours services, or why there is none (switched off, layer not built,
failed), and logs that once per layer until it answers again. Both tours
services ask the layer first and fall back to the existing Overpass path;
the responses TREK sends its clients are unchanged.
2026-10-20 21:12:09 +02:00
Maurice eaeaf68352 chore(server): format the tours MCP test 2026-10-20 21:11:44 +02:00
Maurice 5156fa2c0a test(addons): check the Tours migration against the addon table as it was before
The table holds the seeded addons too, so compare against a snapshot taken before the migration with only tours switched on.
2026-10-20 21:11:44 +02:00
Maurice 5580ff71a9 feat(addons): Tours is on for every install
A fresh install gets Tours enabled from the addon seeder. An install that
already has the row is switched on by a migration that runs once, so an
admin who turns Tours off afterwards keeps it off.
2026-10-20 20:56:19 +02:00
Maurice 7f14dde362 fix(tours): tidy the day times of a tour after review
The get_tour_plan description named the estimate of a tour "such a field"
right after the values of a recording, so it could be read as one of them.
It now follows the sentence on derived fields and says outright that it is
one of them, worked out from distance and heights or the pace of a run.

The arrow between a tour's start and end points from the start to the end
in a right-to-left language as well. A tour just added to a day, which
carries no tour_place_id until the next refresh, now says its overrun like
the row above it. A phone row of an import with a total the travellers
entered shows that one time, the estimate joining its words as on the
desktop. The German overrun line reads naturally without an item name.
2026-10-20 20:52:31 +02:00
Maurice c38eb7ebf0 feat(tours): estimate the walking time of imported tours
A hike imported from GPX, KML/KMZ or wanderer had no time at all, so
several hikes on one day could not be planned against each other. It now
gets the walking time the planner times a hike by (DIN 33466), worked out
from its own distance, ascent and descent; a run gets its distance at the
pace entered. Without heights in the track the time is unknown rather
than the time on the flat, and types timed by a router, by a speed of
their own or by nothing get no estimate.

The estimate is derived on every read and never stored, so tours imported
before get it too and no stored or entered time is touched. The rule moved
to @trek/shared, where the planner, the client and the server read it.
The tour list, the day rows on desktop and phone and the tour detail say
it as an estimate, apart from a planned total; list_tours and
get_tour_plan carry it as time_estimate and name it as derived.
2026-10-20 20:52:30 +02:00
Maurice 4009588078 fix(tours): give the latest stage date a starting value
Sonar flagged the reduce without an initial value; every stage has a date at
that point, so the empty start never wins.
2026-10-20 19:27:58 +02:00
Maurice ef583ede18 fix(notifications): name each wanderer hint text in a key of its own
The three texts of a wanderer hint were picked in a nested conditional, which
the client's key check cannot read, so it counted them as unused.
2026-10-20 19:16:20 +02:00