The assistant can now plan and save every type the Tours tab plans. A bike
tour or a run is proposed and created through the hike's own plan and save
(propose_tour, create_tour), routed on the server with the costing of its
profile, now kept once in shared and read by the planner as well. A paddle
or ski tour planned with plan_paddle_route or plan_ski_route is saved with
create_paddle_tour or create_ski_tour, and "Plan this" takes those plans,
bike tours and runs too. Every create has its card and its way back.
- A book carries schemaRev. The reader that meets a newer one opens the book
read-only with a reload banner and saves nothing, where it used to drop the
elements it could not read on the next save.
- The server refuses a save, a restore or a tool edit that would write an older
revision over a newer book or one newer than itself (409 book_schema_newer),
on REST and through every book tool alike.
- normalizeBookDocument moves to book-normalize.ts.
- Print waits for photos eagerly and up to 15 s for an export of more than 40
photos, and skips images without a source.
Every element kind and Studio tool gets an op, the auto layout and its
templates move from the client to shared with a parity fixture, and a pure
design engine turns directives into a finished book with design findings.
The hiking costing moves to shared, so the planner and the server ask the
router the same way. The road trip router learns to plan a hike on foot
with that costing (Valhalla only: OSRM knows no trail difficulty), to read
heights along a line in chunks and to read the OSM ways a line follows,
all through its one paced queue. A hike router builds on it: the way, its
heights, distance, climb and DIN walking time, and the SAC grades of its
stretches from the trail grade service, each part best effort.
New MCP tools in HikePlanMcp, in the assistant's tours group:
- propose_hike plans a hike without saving it.
- create_hike saves a confirmed hike through the planner's own creation;
a router that does not answer leaves it pending for the planner, a way
that cannot be planned is refused and nothing is saved.
- update_tour changes a saved hike against its revision, keeping linked
stations and refusing what would silently drop drawn legs or stages.
- import_tour_gpx takes a GPX file's text in as tours, like the import.
Same permission checks as REST, broadcasts from the services.
In the assistant a new hike or city walk is an "Add tour" card with its
route and difficulty, undone through delete_place while the tour is
untouched, and a hike being planned shows as a live step.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
With the Tours addon on, the Atlas panel gets a third tab, Tours, beside
Stats and Bucket List: what you have done on your tours across all your
trips. A period (all time, this year, the last 12 months or an earlier year)
and a tour type narrow it; the tab shows the distance large with the number
of tours at its corner, the ascent and descent, and the types with their
distance. "Show tours" opens every tour of the period in a dialog with the
same filters, the sums, the split by type and one row per tour.
What counts is said, never guessed. A tour counts once the day it is on is
over, with the values of the planning, badged "Planned" and explained as
not measured. A linked actual track (ACT-01) replaces those values for its
execution, badged "Recorded" and named, and is never added on top nor
counted as a tour of its own. A tour on several days counts once, a whole
route does not count beside the stages planned from it, a paddle tour sums
no heights, and a value nobody knows stays out of the sum, which then reads
"at least". Only the user's own part counts: a day names them among its
participants, or names nobody on a trip of their own; being able to open
somebody else's trip makes none of its tours theirs. Tours still ahead, on
no day or only on undated days are named as not counting. A tour that fell
through can be left out of the sums ("Leave out"): it stays in the plan
and in the list, dimmed, and "Count again" brings it back. Only the user's
own numbers change.
Server: GET /api/tours/activity-stats answers the user's activity entries
across every trip they can open, and PUT /api/tours/activity-stats/exclusions
leaves a tour out or counts it again (the tour must be in such a trip, else
404). A tour_activity_exclusions table keeps the exclusions per user and
tour. The sums are added up by a shared summary in @trek/shared, so the
Atlas and the assistant say the same numbers. MCP: get_tour_activity_stats
and set_tour_activity_excluded over the same service. The tab reads live
and needs a connection, like the rest of the Atlas statistics.
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.
An assistant can now prepare a change to a saved city walk and show the
traveller what it would do before anything is saved. Preparing writes
nothing: no tour, station, visit or day plan changes, and turning the
proposal down keeps the plan as it was.
- The changes apply in order and name the saved stations by their keys:
add a station after another (a spontaneous stop is an add after the
station the traveller is at), remove, replace and move. Stops put after
the same station keep the order they were given in.
- What the travellers did stays as it is: a station visited or skipped on
any day of the walk is never removed, replaced or moved, nothing goes
before the last station already done, a protected station keeps its
place and a leg drawn by hand stays as drawn. A proposal breaking a rule
is refused whole, naming every change and why. Other tour types, and a
walk that is a recorded track, are refused too: so far only a city walk
takes prepared changes, since the others plan their way with routing of
their own the server cannot run.
- The answer sets the walk after the change against the saved one: each
station with what changed it and where it stood, what was removed, the
way on foot before and after planned by the same router (the stored
distance and walking time given apart, never mixed in), the stays as far
as known, every leg with the new ones marked, the gaps that could not be
worked out, a warning for a must station removed or replaced, and each
new station as create_place takes it, to keep it for later as a place of
the trip instead.
- The proposal names the revision it was prepared on, so a save of it is
refused once the tour has moved on, and says whether the traveller may
change the trip's places at all.
The way on foot of a new walk, its creation and a prepared change now come
from one CityWalkFootWayService instead of a copy in the briefing service.
A tour can now be a ride (HOR-01). The planner offers "Horse ride" as a
tour type with its own horseshoe, and a Horse routes card beside the
waypoints looks, when asked, for the route=horse relations OpenStreetMap
knows in the map view. A route opens in a dialog that shows it on a map in
the sections it is made of, split at forks, gaps and crossings, so nothing
is ever joined across a gap. The rider picks a section and its direction,
sees what the horse=* tags along it say (open, limited, closed, or unknown
where untagged) and whether it is signposted the other way, and takes it as
one undoable step. A section that does not start where the ride ends is
only joined by a straight manual stretch once the rider switched that on.
Nothing of a ride is routed: every leg follows a section or is drawn by
hand, and the waypoint list and the map say which (a section as a solid
line with its route, a manual stretch dashed and marked unchecked). A point
at a section stays where the route has it, a ride works out no riding time,
and distance, heights, day assignment, GPX import and export, the 3D view
and the reversed ride keep working with each leg's origin saved and opened
again. A failed lookup leaves the ride and the routes already found as they
were.
The server reads the routes through the shared Overpass client
(GET /api/trips/:tripId/tours/horse-routes, Tours addon, trip access),
keeps an answer twelve hours per grid-aligned view, and the tour contract
refuses a routed leg, a via ferrata or a riding time on a ride and a horse
route section on any other type. A migration enables the horse tour type.
SKI-02. Ski joins the types a GPX file can be imported as, offered in the
import preview's type field only: TREK routes no ski track, so the planner
never offers it. The preview asks for the discipline on purpose (alpine,
snowboard, cross-country or ski touring, none chosen at first) and, for
cross-country, the technique (classic, skating or unknown); Import waits
until both are named, and the technique stays in sight but dimmed for the
other disciplines. The track is stored exactly as recorded, with its file
name as provenance, its distance and heights from the file and no time:
no ski time is worked out and no hiking estimate stands in for one.
Opened in the planner, a ski track gets a rail card with its discipline on
a tinted tile, a cross-country technique as a badge beside it, the ski time
said as not worked out, and an info dot on what TREK does not say (snow,
avalanches, open runs, and that the 3D gradient colours are no difficulty
or avalanche rating). Change opens a dialog to classify the track again;
only the classification changes, through the offline core, with the
revision it was made at. Planning over the import is not offered for ski.
The existing 3D terrain view, the elevation profile and its focus work on
the track as for any import, with the standard map, topo and satellite and
no via ferratas. Lists, the day plan and the detail show the discipline as
a frosted corner badge on the tile and as a pill with the technique inside;
day assignment, copies and take-overs work as for any tour. The corner
badge is now one component that the winter label shares.
The server keeps tours.ski_discipline and tours.ski_technique (CHECK lists,
NULL on every other type), switches the ski type on without a routing
profile, refuses a ski import without its discipline, a technique outside
cross-country and ski fields on any other type, and takes a planned save's
classification off. PUT /api/trips/:tripId/tours/:placeId/ski classifies a
ski track again (place_edit, 409 on a stale revision, 400 for a tour that
is no ski track) and broadcasts tours:changed. GPX export names the
discipline (alpine_skiing, snowboarding, cross_country_skiing,
backcountry_skiing), and list_tours describes both fields with what a ski
track does not say. Ski is labelled plainly as Ski in every language, so
it no longer reads as ski touring.
RUN-01. Run joins the tour types in the planner, with Running as its first
profile, chosen on purpose like a bike tour's. A run is routed on foot for
runnable, mostly paved ways: tracks and ferries only where there is no other
way (use_tracks 0, use_ferry 0, measured against the public Valhalla), no
hiking limit and no via ferrata. The runner types a pace in min/km; the
running time is the distance at that pace and nothing else, shown under the
field as badges (8 km x 6:00 /km = 48 min). TREK suggests no pace: without
one the time stays unknown and Save waits, text that is no pace or a pace
outside 2:00 to 20:00 is marked. The info dot, the overview and the head say
climbs, descents and difficulty are not in the time, and the time reads as
planned everywhere, so it never passes for a recorded one; the heights are
shown as for any tour on land.
Type, profile, pace and route come back on opening, survive a draft reload,
an undo of the type, a copy and a take-over; switching to another type drops
profile and pace. Day assignment, the 3D terrain view (with topo and
satellite, no ferratas) and a routing failure keeping the last route work as
for every tour. A GPX file can be imported as a run with its profile and no
pace, and planned over as a run. Runs export to GPX as running and get their
own places along the way (water, toilets, parks, viewpoints).
The server keeps tours.run_profile (CHECK list, already holding trail for
RUN-02, since SQLite cannot widen a CHECK in place) and
tours.run_pace_seconds_per_km (120 to 1200), switches the run type on,
refuses a run without a profile and run fields on any other type, and
list_tours names both with what the planned time does not include.
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.
A city walk now shows, at each stop, when the walk gets there, until when
the stay lasts, and what the stop's opening hours say about that visit. The
same lines appear under each stop row in the planner and under each stop in
the walk's detail, in the user's 12 or 24 hour format. A conflict names the
stop, the arrival, the end of the stay and the hours it was checked against,
for instance "Lenbachhaus closes at 18:00, during the stay from 17:20 to
18:20 (open 10:00 to 18:00)", in the warning colour. Nothing is changed by
these hints: no stop, order or route moves.
The times belong to one day the walk is on. Opened from a day of the plan,
the detail keeps that day, and checking stops off stays bound to it.
Opened otherwise, and in the planner, a small pill above the stops names
the day and its start, and the walk's other days can be picked from it; it
starts on the first day with a start time. The pick is only what is looked
at and is never saved, and another day never takes over the times quietly.
Without a day, a date or a start time the line says what is missing and no
arrival is shown. TREK keeps no time zone on a trip, so start, arrivals and
opening hours are all read as the local time of the plan, and an arrival
after midnight is said to be on the next day without being checked.
Opening hours come from the trip place a stop stands for, as the travellers
entered them, and otherwise from OpenStreetMap; a pill at each stop names
the source, and a notice says the hours are a hint to check. A stop without
hours stays unknown and is never shown as open, hours that are not in a
plain form are shown as written, and an arrival that is only a lower bound,
because a stay before it is not entered yet, is not judged unless no later
arrival could escape the conflict. A trip place that a stop stands for also
gives the stop its name before "Stop N".
The arrival at a stop is found where the route passes it in the walk's
order, so the end of a loop and a stop passed on the way out and back get
the right time. The rules live once in the shared package, for the server
to give the same answers. The walk's detail reads its stops again after a
save, its own or a teammate's.
A walk saved from the planner kept its stops without the keys the server
gave them, so the next save from the same open planner handed every stop a
new key and dropped the visits checked off in the day plan meanwhile. The
planner now takes over the keys a save answers, point by point, without
planning the route again, so a stop keeps its key and its visits from one
save to the next.
The edits of a stop's dialog now reach the planner: a changed name, stay,
importance or protection counted as no change and was dropped. It is a step
of its own that can be undone, Save stays available, and the route is not
planned again for it.
A save that takes stops out of a walk, or makes it another type, tells every
client of the trip which visits are left on each day the walk is on, the
client that saved included, so the progress in the day row no longer counts
a stop the walk has lost.
The way back of a loop is the start again and no stop of its own. Closing a
loop gives it only the start's spot and the place it stands for, and the
planner's stays, the stops of the tour detail, the stop count of the tour
list, the visits and the progress an assistant reads leave it out, so a loop
through three stops counts three, the start's stay counts once and the way
back cannot be checked off on its own. A loop saved before keeps working.
The rule lives in one helper the server and the client share, and the tour
list takes its stop count from the walk's points through it.
The tests that seed a walk for the assistant's progress go through the ORM
factories.
A tour can be a city walk. Its points are stations that keep a key across
saves, a name, a stay (0 for a point passed through, empty when unknown), an
importance and a protection; the visits of a walk's stations are kept per day
assignment, so another day or a copy starts with nothing visited. A station
that is gone takes its visits with it. An assistant reads how far a walk got
on a day, what is open and which times are known.
isomorphic-dompurify builds a jsdom window as soon as it loads on Node,
and the root barrel is what the server and each of its ~900 test files
import, so every one of them loaded jsdom without ever sanitising. The
sanitisers move behind @trek/shared/sanitize (a build entry of its own,
like roadtrip), escapeHtml stays in the root from its own module, and
the client's translation context imports the sanitiser from the subpath.
The production server stops loading jsdom at boot as well.
A tour can be a bike tour with a routing profile (touring, road, gravel,
mtb), stored per tour by a new migration. The server grades the ways of a
mountain bike route from OpenStreetMap (mtb:scale) through one shared
Overpass client, offers CyclOSM behind its own admin switch, and exports a
bike tour's GPX with the activity of its profile. A hike's route choices
are now exactly the three the card names: steep climbs, steps and ferries.
POST /api/trips/:tripId/tours/poi-search looks for what a hike or a paddle
tour stops for along a line: viewpoints, peaks, shelters, drinking water,
food, toilets, picnic spots, campsites and sights, and on the water
slipways and boat rental. The line comes in the body, so a draft not yet
saved can be searched, thinned to at most 1000 points; the corridor is
50 m to 2 km wide.
The line is cut into stretches as the road trip's corridor search does,
each searched through the map search (the TREK places index first,
OpenStreetMap after), three at a time; a long line is searched along its
first twelve stretches and the answer says so (capped). The places kept
lie within the corridor of the line itself, each once, nearest the start
first, up to 60, each with its source, kind, address, website, phone and
opening hours as the source has them, its distance straight across to the
line (distance_m, never called a detour) and how far along it lies. A
stretch that fails leaves the others' places and marks the answer partial;
a cut answer is truncated. Nothing is written. It is a read, so anyone who
sees the trip may ask.
The map search's OSM filters gain those outdoor categories; the explore
pill does not offer them. MCP: search_tour_pois, along a saved tour's line.
GET /api/tours/library lists the tours of every trip the user owns or is a
member of, with the trip each is in and how many tours each trip holds, to
choose one to take over into the trip being planned. It is narrowed by a
part of the name (matched ignoring case, a "%" or "_" just a character), a
type or one trip, and the asking trip can be left out; up to 100 tours, and
the answer says when there were more. GET /api/tours/library/:placeId
previews one: its facts and its line thinned to 400 points, its ends kept.
Neither is trip-scoped, so every answer is bounded by the user's own trips,
read anew each call: a tour of a trip out of reach is "not found", nothing
hints that it exists. Measures stay null where the tour has none.
POST /api/trips/:tripId/tours/take-over copies one into this trip, which the
guard checks with 'place_edit'; the source trip is checked against the
user's trips as they are at that moment, and the source is read again inside
the write. The copy is a tour of its own: its line, measures, settings and
control points, without days, without its stops' links to places of the
other trip and without the obstacle decisions taken there; it keeps the
import record of its line. A copy that fails leaves nothing behind.
Where it came from is kept as text (tours.taken_over_from_trip_title and
taken_over_from_tour_name, migration 045100), not as a pointer, and read
back in the provenance as takenOverFrom; a copy of such a copy names it too.
The in-trip copy (P2C-07) and the take-over share one copy routine.
MCP: list_tour_library and take_over_tour, over the same service and
checks. The repository tests follow the hiking preferences column of MF-08.
A tour's waypoint can stand for a place of its trip, chosen on purpose from
"Trip places" in the planner: searchable, nearest to the tour first, grouped
into near the tour and the rest, and a place already in the tour says so. A
pick adds the stop on the place's spot as one step of the history; the row
names it after the place and says it is a place of the trip. The point keeps
its own coordinates, the place stays a place of its own and its days are
left alone. Moving the point or "Unlink from the place" drops the link;
removing it never touches the place.
tour_waypoints.linked_place_id (migration 044900) references places with
ON DELETE SET NULL, so a deleted place only unlinks. A save checks the links
inside its transaction: a tour of the trip is refused, a place that is gone
or of another trip leaves the point unlinked, the same either way. The trip
copy points the links at the copied places, the GPX export writes the linked
stops as named waypoints, and get_tour_plan names them.
nearnessToLine in @trek/shared measures a point against a route, for the
picker and for the search along a route that comes next.
Paddle tours are planned on the water by TREK's own router: a water network
per paddle region, built from the same Geofabrik extract as the obstacles and
searched in a worker thread of the server (no sidecar). Rivers, canals and
lakes (direct or along the shore), portages round weirs, locks by policy,
whitewater by boat, gaps marked. Found portages and locks become points of
their own; an unplannable leg says why and offers a way out.
- Shaping points: a click on the line sets a point that steers the route
without being a stop (hikes and paddle tours); recompute a single leg.
- Lake style direct/shore, suggested from the wind forecast.
- Regions load by themselves where a paddler plans (admin setting: automatic,
on request, off; size cap and a daily count), with requests for the admins,
a notification, and the load followed on the leg.
- Portage from the last point, portages follow footways via the walking
router (or stay straight), "put in here" on slipways and put-ins.
- MCP plan_paddle_route with the REST route's rules.
A trip can now show one journal from the Journey addon as its own: any
journal its owner picks, and one journal on as many trips as they like.
The trip members chosen for the link get into the journal as viewers or
editors, see it in their own journal list and follow it live, for as long
as the link holds them and they stay on the trip.
That access is never copied into journey_contributors. It is read where
journal access is read: the contributor repository answers with the
stronger of a direct role and a link role, the journal list and the live
update recipients take the link members in, and the photo access check
goes the same way. Every one of those reads starts from linkReach, which
only counts members still on the linking trip, so unlinking, deselecting
someone and their leaving the trip all end exactly what the link gave.
A member who leaves is also taken off the link, so coming back later does
not quietly reopen the journal.
Linking or changing the members takes the journal's owner and the new
journal_link trip permission (trip owner by default); unlinking takes
either. /api/trips/:tripId/journal answers the link as each member may see
it, /candidates says who a journal would be opened to for the first time,
and four MCP tools do the same over the same service. A trip:journal:changed
event tells the trip to look again.
The checklist and the obstacle data sorted texts without a compare
function. They now sort by code units through one shared compareCodeUnits,
the order a bare sort gave, the same on every machine; the material data of
a lock's parts no longer orders by the server's locale, so its hash cannot
change with it. The note editor takes the focus through a ref instead of
autoFocus, a direct function reference in a map becomes a call, and the
GPX import's default fields are a named constant.
PAD-06 and PAD-09 in shared. A paddle tour's checklist lists the obstacles
of the OpenStreetMap data along its route, each bound to a basis: the route
near the obstacle as samples around every passage, and the material data of
the obstacle as a hash and its position. A decision keeps that basis, so it
reads as one to check again once the route or the data changed there, and a
portage decision holds while the paddler's own portage carries round every
passage. The check bench sums the list up for the tours list and the day
plan. Nothing in it is a safety or legal clearance.
The route relation learns the passages of a route by an object and the
distance of a point to a route. The client takes the obstacle key from
shared instead of its own copy.
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.
- Paddle as a plannable tour type: one boat (kayak, canoe, SUP, other), river or
lake, a pace that a boat change never overwrites; time is distance over pace
- Hand-drawn legs beside routed ones, drawn dotted and labelled as not routed,
kept through save, reopen and edits elsewhere
- Reverse a tour and close it back to its start, each one undo step
- GPX import shows a preview first (line, measures, duplicates, type) and only
the confirmation stores the tour
- Base maps and overlays per tour type in the planner: hike keeps topo and the
via ferrata layer, paddle gets the OpenSeaMap seamark overlay
- P2C-02: tours record their origin (planned, GPX with file name, wanderer)
instead of reading it off their shape; the detail answers a provenance
contract and a trip copy keeps a tour's import record
- MF-03: a wanderer snapshot's source can be checked on purpose, read only,
with a confirmed reimport as a new snapshot
- PR1-18: every way of putting a tour on a day shares one set of rules
- PR1-07/08: each person can hide tours from the trip map; the list shows
each tour's line colour and the picked tour stands out on both maps
- GPX files drop onto the map or a day; the import lands on that day
- wanderer connection per user in Integrations; the workspace offers the
user's own trails only, behind an admin switch under the Tours addon
- wanderer difficulty is stored on the SAC scale (T1 to T3), backfilled
- a GPX or wanderer import can be planned as an editable tour, either in
place (the recorded track is kept and can be restored) or as a new tour
that remembers the import it came from
- rails, rows and detail dialog follow the plan sidebar's cards and badges
- admin addon shelves with more than one row fold, collapsed by default
- reverse-geocoded waypoint names are cached on the device
RESERVATION_TYPES lists every type with its flags (transport, carrier,
rental, stay, legs, creatable through MCP), and the named subsets derive
from it in picker order. The MCP transport and booking enums, the share
view's travel filter, the road trip carrier query and the shared
CARRIER_TYPES now read it. The reservation response types type and status
as the known values or any stored string, so rows outside the catalog still
pass; requests stay as tolerant as before.
PERMISSION_ACTIONS, PermissionLevel, PermissionKey and evaluatePermission()
move to shared/src/permissions. PermissionsService decides through
evaluatePermission and re-exports the catalog for its importers, and
@RequirePermission takes a PermissionKey, so a misspelt action fails to
compile. Levels, defaults and every verdict stay as they were.
toMinor, sumMinor, splitEqualShares and currencyDecimals move to
shared/src/money. The budget service and its MCP tools call them instead of
private copies, and the share table both sides were pinned to becomes the
spec of the shared function. Stored values and every computed figure stay
the same.
A session token was a signed JWT and nothing else. Logout only cleared
the cookie, a 30-day "remember me" token stayed valid wherever a copy of
it had ended up, and nobody could sign out one device short of changing
the password or rotating the JWT secret for everyone.
Every session token now carries an id (jti) naming a row in the new
user_sessions table (id, user_id, created_at, last_seen_at, expires_at,
revoked_at, user_agent cut to 256 characters; no address is kept). The
rows live behind UserSessionsRepository and the new sessions domain,
whose SessionsService is the one issuer: AuthService.generateToken and
the OIDC sign-in both go through it, and sliding renewal re-signs a
tracked token under the same id and moves its expiry. verifyJwtAndLoadUser
takes the repository and refuses a token whose session is revoked,
expired or gone, refreshing last_seen_at at most every five minutes.
A token without a jti was issued before this change; it passes until its
own expiry (or a password change), so the upgrade signs nobody out, and
renewal turns it into a tracked session.
Sessions end on logout (the one the request names, verified even when
expired), on a password change (all, the device gets a new one), a
password reset, an admin setting a password, reset-admin.js, an MFA
disable (all but the current one), an admin clearing a user's MFA, and
with the account. GET /api/auth/sessions lists the active sessions,
DELETE /api/auth/sessions/:id ends one and POST
/api/auth/sessions/revoke-others ends all but the current, with their
contracts in @trek/shared and two new audit actions. There is no UI for
them yet. MCP exposes no account or session tools, so there is no
parallel tool to change; an MCP call made with a session JWT as bearer
goes through the same check.
The token in the login, register, demo-login, MFA and passkey bodies
stays for API clients but is marked deprecated in the API docs and in
the client type, which never read it.
A nightly session-purge job on CronRegistrarService deletes the expired
and revoked rows. The migration is 042500. The sessions domain gets its
own coverage entry at the 80 floor until a full run measures it.
Unfinished, continued in the next session: t() resolves key.one/.few/...
by the language's own plural rule, the parity check knows which forms a
language needs, 16 singular/plural key pairs are merged in every locale
and every count === 1 branch in the client uses the plural form. Still
open: the forms each locale needs for the remaining count strings, the
two new labels in every locale, and 35 tests that expect the old English
wording.