12 Commits

Author SHA1 Message Date
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 2e1dca9fe9 feat(tours): let a background check hint at changes of your own wanderer trails
Settings, Integrations, wanderer gains an optional background check of the
trails you imported, off until you switch it on. While on, TREK reads the
short record of each such trail at most once a day (never its GPX file) and
leaves a hint when the trail may have moved on: "may have changed" when only
wanderer's change time moved, "changed" when its name or description proves
it, and "not checkable" when the trail is gone, no longer yours or carries no
change time. Nothing is ever said to be unchanged. A new hint arrives as a
notification that opens the tour (a new ?place= link in the planner); the
tour's provenance shows it with the source check right beside it, and the
card lists all open hints in a dialog, each leading to its tour. The check
reads the source in full as before and spends the hint; taking a change in
stays the confirmed reimport. No tour is ever changed or deleted.

The check only asks through your own connection, only about trails your
account imported into trips you can still open, and stops when you switch it
off, remove the connection, wanderer refuses the token (until a test passes
or a new token is saved) or the admin switches wanderer off; hints and
snapshots stay. A run asks at most 25 connections for at most 10 records
each, one after another, and an instance that fails (an outage, a rate
limit) is left alone for an hour, doubling up to two days.

Server: migration 046120 adds the switch and the run bookkeeping to
wanderer_connections and the tour_source_hints table (one row per snapshot,
for its importer, gone with it). WandererWatchService and an hourly
wanderer-watch job on the CronRegistrar live in the tours domain and read
through WandererService; GET and PUT /api/integrations/wanderer/watch and GET
/api/trips/:tripId/tours/wanderer/sources/:placeId/hint (place_edit, like the
check) answer with @trek/shared contracts. A new user notification event,
wanderer_source_hint, can be muted per channel. Saving a new token now clears
what the last check said about the old one. There is no MCP tool, as for the
rest of the personal wanderer connection. All 27 locales carry the strings,
and the admin wiki page says what the check does.
2026-10-20 15:35:32 +02:00
Maurice f833a05461 feat(tours): point a tour at its trip's packing list
A tour can now point at the packing list of its trip, as a whole or at one
of its categories (say "Hiking gear" for a hike), and shows it in its detail.
The new Packing list section reads the list live as the user may see it: the
backpack tile with the item count on its corner and the category's own
colour, how many are packed with a thin progress bar, the first items ticked
off or not with their quantity, how many more there are, and a button that
opens the trip's Lists tab. Who may change the tour links one, changes it or
lets go through a dialog that lists the whole list and every category with
its colour and item count, and writes only on its confirm.

It is a reference, never a copy: the items stay in Packing, are ticked off
and edited there, and each traveller sees in the tour only the items Packing
shows them, so an item kept to oneself never shows through a tour. A
category that is renamed, cleared or only holds items of others reads as
missing, an empty one as empty, a link that could not be read says so with a
retry, and while Lists are switched off the link stays, dimmed, and can
still be let go. Removing the link, or the tour, leaves the list and every
item as they are. Nothing suggests or changes an item by itself.

On the server the link is a row of its own in the tours domain
(tour_packing_links, migration 046110), one per tour and gone with it.
GET, PUT and DELETE /api/trips/:tripId/tours/:placeId/packing-link read it
with the trip and change it with place_edit; linking needs the Lists addon
and, for a category, an item of it the user can see. The writes answer the
tour with its link and tell the trip through tours:changed, so the offline
client queues them as writes of the tour and replays them like one. A copied
trip keeps its tours' links, a tour saved as new or taken over does not.
The MCP tools get_tour_packing, link_tour_packing and unlink_tour_packing run
over the same service and checks; the read also hands the tour's facts as
context for suggesting what to pack, which only becomes an item through the
packing tools. The empty category placeholder now lives in @trek/shared so
both sides leave it out alike. All 27 locales carry the new strings, and
the wiki describes the link and the tools.
2026-10-20 15:35:08 +02:00
Maurice 2449616ee9 feat(tours): plan a multi-day tour as one route split into stages
A hut-to-hut hike or a paddle trip over several days is now planned in the
Tours planner as one tour: the whole route is planned as before, and stops
along it are marked as stage ends (MDT-01). The stages are read off that one
route in its order, never kept as copies, so a change inside a stage changes
the whole tour and every figure on it.

- A Day tour / Multi-day tour switch beside the tour type; a stop's menu
  ends a stage there or lets it go on. Back to a day tour removes every stage
  end after a confirmation, as one step undo brings back.
- The list of points is headed per stage with its distance and moving time;
  a stop that ends a stage carries a moon at its number, on the map too.
- The overview lists the stages from stop to stop with distance, climb,
  descent and time (hiking rule per stage, paddle estimate per stage, the
  router's time shared by distance otherwise), marks where each ends across
  the elevation profile, and a stage of more than a day on the move warns.
- A stage end a change cannot keep (its stop removed, moved to an end or
  made a shaping point) is dropped and said so, with undo, never moved
  to another point.
- A multi-day tour may take a day a stage, so its time limit grows with its
  stages; the tour keeps its one type, portages included.
- The list and the MCP tools show the stage count and where stages end.

Stage ends are a flag on the waypoint (migration 046000); a tour without one
is a day tour, as every tour saved before.
2026-10-20 15:26:18 +02:00
Maurice d0cc6fc2f4 feat(tours): link the track a tour was actually done with, and show its own values (ACT-01)
A planned tour can now be given the track it was actually done with. In the
tour's detail, "Add actual track" opens a dialog that asks for the day the
tour was done on (or none) and a recording of the trip: an import nobody
planned over, or a new one through the existing GPX import, its preview
preset to the tour's type. What the choice measures is shown before anything
is written, and only "Link" writes. The linked track keeps its own name and
source; the detail shows its distance, heights, profile and, only where the
file carried time stamps, the time it took, marked as actual values next to
the untouched plan. A track can be changed or unlinked again.

Server: a tour_activities table points a tour's execution (a day assignment,
or none) at a recording of the same trip, and the tours GPX import keeps the
first and last time of a track whose every point carries one. The values are
read from the recording's own line on every read, so a changed recording
updates them while the plan's line, measures and revision stay as they were.
Every check runs inside the write: the recording must be an import of this
trip not planned over, documenting no other tour; the day must be one of this
tour; a refusal names its reason and changes nothing. PUT and DELETE
/api/trips/:tripId/tours/:placeId/activities answer the detail, which now
carries the actual tracks; the list names them per tour and per recording.
MCP: assign_tour_track and unassign_tour_track over the same service and
checks, get_tour_plan reads the values without the line.

A recording linked as a tour's actual track is no longer counted as a tour of
its own in the trip's tour summary, and the GPX preview imports a city walk
as one instead of as a hike.
2026-10-20 15:22:55 +02:00
Maurice 19946039ad feat(tours): get_tour_plan reads the whole saved plan of a tour
An assistant reading a tour over MCP now gets everything the saved plan
holds, from the same reads the app uses, so its suggestions start from the
actual plan:

- legs lists every step from one control point to the next with how its
  line came to be (routed, via ferrata, drawn by hand, portage), keyed by
  the points' stable keys, and the length of a fixed leg's own line; a
  routed leg has no length stored apart and says so with null.
- line gives the saved line in brief: its point count, whether it carries
  heights, and a preview thinned to 200 points; null while the way waits
  to be planned.
- A city walk can be read for one of its days (dayId): each station then
  carries its visit status that day and run what is still open. Without a
  day no visit is told, so one day's ticks are never passed off as the
  walk's; dayId on another type or a day the walk is not on is refused.
- derived names every field worked out on reading rather than stored,
  including the control points an import without its own answers from the
  ends of its track.

Reading still writes nothing: no revision moves and no visit changes, and
drafts in a browser stay invisible. The wiki row of the tool says the same.
2026-10-20 15:13:41 +02:00
Maurice e5f01e570a fix(tours): check a city walk's stops off and save a stop from its dialog, with the walk under test (CW-01 to CW-05, CW-09)
Marking a stop of a city walk visited or skipped answered with a server
error every time: the write that keeps a changed status stamped its time as
a function call, which neither SQLite nor Postgres takes. It now uses the
clock the database itself spells, so a stop checked off, skipped or taken
back stays that way and the day plan of everyone on the trip follows.

The Save button of a stop's dialog in the planner did nothing, because it
sat outside the dialog's form. It saves the name, stay, importance and
protection again. A walk saved with two points that carry one key, as a
loop back to the start does, keeps that key on the first point and gives
the other one of its own, so checking one off never marks both. A walk put
on one day twice is read for an assistant from its first assignment.

The server tests cover the visits from the repository to the routes: per
day, through a moved and a removed assignment, a copy of the walk and a
copied trip without visits, a removed stop taking its visits along, and
the refusals for another trip, a stop the walk does not have, a member who
may not edit days and an unknown key or body. The MCP progress of a walk,
the station fields and counts of the tour list, and the contracts in
shared are tested too; the existing tests expect the keys every saved
point now carries, the station count of a list row and the new entity.
The client tests cover the stop dialog and its pills, the walk themes and
categories of the search along a tour, the stop menu of the waypoint list,
the walk's timing, routing and type choice, and answer the trip planner's
read of the visits in the tests instead of leaving it unanswered.
2026-10-20 07:09:56 +00:00
Maurice 89f3055a29 fix(journey): one journal link per trip as a unique trip id, factories within tsc's depth
The link table keyed by the trip id made both entities a one-to-one, and the
test typecheck failed with TS2589 in the itinerary factory. It now holds its
own id with trip_id UNIQUE, as trip_invite_tokens does, and its members hang
off link_id.

The factory error was not this table's alone: any further relation on trips,
journeys or users tipped the same check. insertRow and createRow now take a
caller's overrides as their own argument instead of a spread checked against
RequiredEntityData, so a new relation no longer breaks the itinerary and tour
factories.

The suites that hand-build TripMembersService get a journalLinksStub.
2026-10-18 17:41:18 +02:00
Maurice 724c88c201 style: format server and shared with prettier
A mechanical prettier --write over server/src, server/tests, server/scripts
and shared/src with the workspaces' .prettierrc; nothing but layout and
import order changes, and every file keeps the same set of imports. shared
is formatted throughout and its format baseline is empty. The server
baseline keeps the 35 files that Prettier would grow past their lint:size
limit (they need a split first, which is not a formatting change), and the
generated entity files stay as their generator writes them.
2026-10-10 21:23:37 +02:00
Maurice ca0138dc45 test(server): make the row factories return what they promise
findRow and the default category lookup asked findOne with an empty
where, which MikroORM refuses. insertRows returned a single id because
insertMany on plain data reports only the last insert id; it now inserts
row by row and returns every id. insertRow and updateRows reject instead
of throwing before the caller holds a promise.
2026-10-08 20:22:16 +02:00
Maurice 1bba65634c test(server): seed and read test data through ORM factories
The server suite seeds and reads its database through some 5,500 raw
better-sqlite3 statements on the sync handle. They restate the schema by
hand, skip the repositories the app goes through and run on SQLite only,
so nothing in the suite could ever exercise the dialect layer on another
engine.

tests/helpers/factories/ adds typed factories per domain (users, trips and
days, places, assignments, bookings, budget, packing, to-dos, files,
journeys, tours, collections, notifications, settings, plugins, tokens,
atlas, vacay, photos, collab) and generic readers (findRow, findRows,
countRows, updateRows, deleteRows) that go through MikroORM in a request
context of their own, against the app's ORM or a createTestOrm handle.
They refuse a write to a read-only foreign-key twin, which MikroORM would
otherwise drop without a word. The README there is the conversion guide.

The categories integration suite and the tags and calendar-feed e2e suites
are converted in full as worked examples; the tags suite also moves from
hand-rolled tables to the migrated snapshot.

lint:test-sql (scripts/test-sql-ratchet.mjs) holds the raw statements left
to a per-file baseline: growth and stale entries fail, files whose subject
is raw SQL are exempt with a reason, and a single statement can be allowed
with a test-sql-allow comment. CI runs it next to lint:boundaries.
2026-10-08 18:44:39 +02:00