Commit Graph

531 Commits

Author SHA1 Message Date
Maurice 99783cc8da Merge pull request #2346 from WenyinWei/fix/share-map-clustering
fix(shared): cluster the public share page's markers
2026-09-15 23:06:41 +02:00
Maurice e5d85eda8d Merge pull request #2347 from WenyinWei/fix/share-header-overflow
fix(shared): keep the header's decoration from widening the page
2026-09-15 22:58:10 +02:00
Maurice 8a72d98bc6 test(shared): type the cluster icon size check and tighten the comments
iconSize is a PointExpression, which has no x on its tuple form, so the client typecheck refused the size assertion; it now compares against L.point. The test ids follow the FE-PAGE-SHARED-MAP scheme in the order they run, and the comments around the cluster group say in a line what they said in a paragraph.
2026-09-15 22:47:46 +02:00
Maurice 09e46bf723 test(shared): give the header clip test an id of its own
FE-PAGE-SHARED-040 already names the trip-wide marker colour case in the same file, so the new case moves to 042, and the comment above the clip says what it needs to in two lines.
2026-09-15 22:41:37 +02:00
Maurice acd6f1acdf fix(atlas): date a country's first and last trip from the trips that count
The Atlas tooltip took both dates from the start dates of every trip with a
place in the country. One trip showed the month it began twice, the month it
ended never appeared, and a trip only booked could become the last visit of a
country already visited.

Both now come from the trips that give the country its status, places and
bookings alike: the start of the earliest and the end of the latest, with a
trip still under way ending today. The optimistic mark on the client clears the
dates the same way the server does.

The second label rendered the raw key atlas.lastVisitLabel since #1908 removed
it from the locales; the strings are back as they were. The month is built
from the date's digits now, because a 'YYYY-MM-DD' string parses as UTC and
showed the month before anywhere west of Greenwich.

Closes #1535
2026-09-15 21:36:34 +02:00
Maurice c1d55c463c fix(dawarich): three things a tester on dev1 ran into
The Atlas country check timed out. It asked Dawarich for a year of visited
cities in one request, and Dawarich computes that over every point in the
window with nothing cached, so a dense recorder never got under the 15 s the
server allows. The year is now asked for a month at a time, one request after
another, and folded back together. The windows sit a second apart because
Dawarich reads the bounds as inclusive.

The recorded route only appeared after a reload. Every Dawarich call from the
browser carried the shared 8 s timeout, so the browser gave up while the server
carried on and cached the answer for the next page load. Calls that make the
server talk to Dawarich now wait two minutes, the Atlas four. On the GL
renderers the draw also gave up whenever isStyleLoaded() was false, which it is
while any tile is still loading, and only retried on style.load. It now waits
for idle. The hazard overlay had the same guard and gets the same wait. The
pill shows a ring while the route loads, so a slow first fetch no longer looks
like "done, nothing there".

A collapsed day now takes its recorded route off the map, following the same
folds the planner already applies to places, the road trip sidebar's included.
2026-09-14 23:00:57 +02:00
Maurice 60cbb41d5f fix(roadtrip): put a corridor hit on the drive from the day before ahead of the first stop
The corridor searches the whole line a card draws, and a card that opens
with a drive from the day before (a night carried over, or the joined road
on connected days) draws that drive ahead of its first stop. The insert
index still came from insertIndexForAlong, which never answers 0, so a
charger found on that stretch was added after the first stop and the day
drove there, turned back to the charger and came the same way again.

insertIndexFor now answers 0 for a hit before the first stop when the card
arrives from an earlier stop, and dropping a hit on the map asks it instead
of computing its own index, so a click and a drop cannot disagree.
2026-09-14 22:43:46 +02:00
Maurice 2ffe4d6054 fix(roadtrip): file a via on the drive between connected days after the stop it leaves
With connected days the drive from one day's last stop to the next day's
first is drawn at the head of the arriving card, in the colour of the day
before. anchorFor only knew that stretch as a night spill, so a click on it
anchored to the arriving card's first stop: the via was stored on the leg
after that stop, and the next day drove out to the point, turned and came
back, while the stretch that was clicked kept its shape.

assembleRoadtrip now names the stop that joined drive leaves from, and
anchorFor treats it like the stop a night drive left from. Placing and
dragging both resolve to the previous day's last stop, which is the anchor
the seam request already reads its vias from.
2026-09-14 22:36:29 +02:00
Maurice da679e6266 feat(roadtrip): add a stop by hand in the form the rest of TREK adds places in
The dialog built for this last round was its own thing and showed it: the
suggestion list was barely legible, clipped at the bottom, and never more
than two rows tall. It also lacked most of what the ordinary add-place form
has had for years.

So it is gone, and "Add manually" opens that form instead, in a service-stop
mode. The category control becomes the kind of stop and the time spent
there, the costs section is dropped, and an "Add between" row says where on
the drive it goes. Everything else comes along rather than being rebuilt:
the typed-ahead search with its source marks, the coordinate fields for a
charger no index knows, address, website, phone, notes, times, files and the
duplicate check. With the mode absent the form is the same component it was,
which its own suites still pin.

Where the stop lands is worked out when it is saved rather than when the
form opens, because the coordinate only exists once a place has been chosen.
One function answers both what the select shows and where the stop goes, so
the two cannot disagree, and the write goes through the same assignment path
as every other insert, via re-anchoring included: a stop pushed into a day
shifts every via anchored at or after it, and without re-anchoring first the
drawn road runs forward, doubles back and runs out again.

The select recommends rather than merely offering. Every leg of every routed
day is measured against the place separately and they are listed nearest
first, each with its own distance, so the preselected one visibly is the
closest and overruling it is an informed choice. The word they all shared
moved into the caption.

A stop with no position at all is not filed on a drive by guesswork. It
keeps its kind and goes to the trip's places, and the form says so and says
how to change it.
2026-09-14 16:22:18 +02:00
Maurice accdd5905e feat(roadtrip): add a stop the search never found, and clear the ones it did
Four things testers asked for in the "Along the route" panel.

Add manually. The corridor search reads OpenStreetMap, and a good share of
the chargers standing at a motorway junction are not in it, Tesla
Superchargers among them. The way round it was to leave road trip mode, add
the place under Days, drag it onto the right day, come back and mark it a
charging stop. The Search button now shares its row with a second one that
asks the two questions those four steps answered: which place, and where on
the drive. The place comes from a debounced typeahead over the place search;
the position from anchorFor, the same projection a via is placed with, and
it is offered as a dropdown of every leg of every routed day so it can be
overridden. Unlike a via, a place far from the drawn line is not refused,
only flagged: that place is the reason this dialog exists. From there it
hands over to the stop popup a corridor hit already goes through, so the
kind and the dwell are decided once for both paths and the write stays
saveStopDraft.

Clear results. A search that turned up the wrong thing left its hits in the
list and its pins on the map, and the only way out was switching to the day
plan and back. The search hook has exposed a clear() since it was written
and nothing ever called it. A control in the results header does now, and
takes the four narrowing states with it, because they were set for a
question that no longer has an answer.

Charging as the default for an electric car. "Looking for" opened on Fuel
whatever the vehicle. It opens on Charging when the trip's vehicle is
electric, and is unchanged for combustion, for an empty value and for a
vehicle nobody set, so nothing moves for anybody who never opened the
dialog. Seeded once, never over a category the traveller picked or a search
in flight.

Clicking a result moves the map to it. The row had no click handler at all;
only the trailing Add button did anything. The badge and the text are one
activatable target with that button as its sibling, which keeps the li a
list item and avoids a button inside a button, and it goes through the same
focusRoadtripPoint the rail uses, which closes the refuel and alternatives
panels first because those outrank the camera.
2026-09-14 12:28:30 +02:00
Maurice 39d832c772 test(journey): the entry picker takes clips, so the test may not insist on stills
FE-PAGE-JOURNEYDETAIL-105 still looked for accept="image/*" on the editor's
file input after it learned to take video (#2341), which is the one red test on
dev. The camera input beside it stays image-only and the test says why.
2026-09-13 23:49:01 +02:00
Maurice 61880ce7d6 fix(roadtrip): the vias, the alternatives, the pacing and the missing warning
Choosing another road on a leg that already carried a via appended the new
point behind the old one, so the day drove south to the first and north to the
second: a zigzag matching neither the line drawn under the button nor the
distance printed on it. The alternatives are computed for the two bare
endpoints, so the write has to replace the leg, which is what the direct branch
beside it already did.

A day's vias were only loaded while road-trip mode was being looked at, and the
three handlers that re-anchor them after a stop is added, removed or reordered
are reachable from the place inspector in both modes. With the switch off they
computed their correction from an empty list and wrote nothing, so a deleted
stop left the detour on a leg nobody chose, for the traveller and for every
collaborator. They are loaded whenever the addon is on now.

Seam routing paced itself by sleeping after each request, and storing a result
shortened the key its own effect depends on: React tore the effect down
mid-sleep, the cleanup aborted it, and the next request went out immediately.
Thirteen seams left at round-trip speed, the shared routing hosts refused the
tail, and a refused seam changes no state so nothing ever retried it. The wait
now happens before the request, against a clock that outlives the effect.

The "avoidance not honoured" badge could never appear on the rail: the field
behind it was published every round and never written. Only the second engine
reports what it achieved against what was asked, and the rail plans in the
browser, so it never saw the server's own copy — a motorway crossing read as a
motorway-free drive.

Two smaller ones. `trip.toast.loadError` has been called since the first commit
and exists in no locale, so both call sites printed the key itself; it is
written now in all 23. And the phone shell honoured the road-trip session flag
while having no rail, no drive lines and no switch to leave it, so narrowing a
desktop window took the trip overview away and listed every booked night twice.
2026-09-13 23:49:00 +02:00
Maurice 76f45512d1 feat(admin): the phone's API-keys card, rebuilt like the desktop tab
The desktop tab was reshaped around one idea: a key should be the exception
rather than the thing you are expected to paste. The phone still had the older
shape, where Google wore the "Recommended" chip and the TREK Places API was not
on the screen at all. Two pages, opposite advice, same setting.

So the phone gets the same three comparable blocks in the same order: the TREK
index first, with its own ribbon and a folded panel of what it does and does not
carry, then the two keys under theirs. The Google caveat is said before the
field rather than after it, because somebody about to paste a key should read it
while deciding. The four Google switches moved under the key they spend, folded
away: they are set once and never touched again, and open they were longer than
the rest of the card together. Open-Meteo left the card entirely, as it did on
the desktop, since it takes no key and had the loudest treatment of anything
there.

The provider choice is TREK's own picker now. A native select is drawn by the
operating system and was the one control on the page that did not belong to the
app.

Its description was also describing a TREK that no longer exists: it said one
service answers place search. The index and OpenStreetMap answer every search
and the setting only decides who is asked once they come up empty
(maps.service.ts, keyedProvider). Both that line and the missing-key notice say
so now, in all 23 languages, and the two key hints have short forms for the
narrow screen.
2026-09-13 21:07:55 +02:00
Maurice cbc28fad88 fix(journey): a dead translation key, and two cards that spoke differently
`common.errorOccurred` is in no locale. It sat in the reorder failure toast
already and I copied it into three more places, so a failed dismiss, restore or
reorder printed the key at the reader. They all say `common.errorTitle` now.

The suggestion mark on a phone card was the word spelled out, which took a third
of a 136px card and pushed the date off it; it is the same plus the desktop
suggestion card offers, and the card's italic dimmed title carries the rest.
Dismissing a suggestion from the entry sheet has its own full-width row above
the buttons rather than a two-line label wedged between Delete and Cancel.

And two cards that should have been one design: the dashboard's spotlight trip
wrote its facts in capitals on a panel of dark glass, the journey hero wrote
them in sentence case on a gradient with its badge loose in a corner. The
journey card is built the way the trip card is now, and the trip card's three
pills are set like the DAY x OF y badge beside them, a size down and on the
same ground.
2026-09-13 20:32:49 +02:00
Maurice 15aefa449e feat(journey): the marker card, and what testing it turned up
Everything here came out of putting the first commit in front of somebody and
watching them use it.

**The marker card is one card now, and it carries the entry's photos.** Leaflet
showed a one-line label and the GL map a two-line card, so the same marker said
different things depending on a renderer setting nobody connects to journeys.
Both now build from one module: a strip of up to three thumbnails bleeding to
the card's edges, the name, and the place and date as chips. On the GL map the
thumbnails open the entry's photos; the rest of the card still never takes the
pointer, so the map drags from anywhere on it.

**A pin that would not sit still.** Dragging the map left the marker trailing
behind and snapping back at the end. The app's press-feedback rule puts a 180ms
transition on `[role="button"]`, MapLibre and Mapbox give their marker element
exactly that role, and both rewrite its `transform` on every frame of a
movement. Markers are excluded now — every map in TREK, not just this one.

**The phone card, after four rounds of looking at it.** The day is the card's
own edge rather than a bar across its foot, and the ring is inset because the
carousel clips. A card without a photo is opaque: it was a wash of the day's
colour over the map. The country is a real flag rather than two letters —
Windows has no flag glyphs, so the emoji drew "DE" where a flag was meant, and
the image falls back to the emoji when it cannot be fetched. Mood, weather and
the date share the top edge; the flag sits opposite them.

**The day bar answers to a finger.** Press the date and slide sideways to run
through the days. The bar is still a row of buttons underneath, because that is
the only version of it a keyboard can reach, and each one is 20px tall now
rather than 3.

Also here:

- Videos upload to an entry (issue #2341). The gallery has taken them since
  #823 and an entry could not: everything went through the images-only filter
  and came back 400, reported as "1 of 1 photos failed". Same two-part upload,
  same absence of transcoding, and the editor now previews a clip in a `<video>`
  instead of a broken-image glyph.
- The search box takes the gap the toolbar already had instead of clipping its
  own placeholder.
- Entry cards line up with the day header again: the reorder arrows moved out of
  a column of their own into the feed's padding and appear on hover, and the
  day-coloured ring on the active card is a layer over it — an outline tight
  enough to sit on the card's edge was painting behind it.
- Map attribution is collapsed to its ⓘ. It cannot go away — the OSM licence
  asks for it — but it does not have to lie under the carousel.
- The suggestion dismiss and the day-header plus use TREK's own tooltip rather
  than the browser's.
2026-09-13 19:52:14 +02:00
Maurice c2707b7eff feat(journey): the enhancements from discussion #2299
roel-de-vries pitched seventeen changes to Journey. Four were already built or
rest on a misunderstanding, one cannot work as described, and three would take
away more than they give; the rest are here, plus two things the list did not
ask for.

**The entry you write on the move now has a name.** Quick capture hid the title
field outright and offered a note instead, so every entry caught on a phone
arrived nameless and a day's timeline read as a column of identical
placeholders. One line of input; it is the single change in this branch that
users will feel most.

**The map stops yanking.** Stepping through the timeline called flyTo with a
zoom floor of 12, so reading a journey from a country view snapped to street
level on the first scroll and stayed there — every marker filling the screen,
and no sense of where anything was. Both renderers pan now and leave the frame
the reader chose. The initial fitBounds still decides how close a journey opens.

**The phone timeline is photos.** A 64px thumbnail beside three lines of text
spent the card on a story nobody reads at that size and truncated the title
after a few characters. The picture is the card now, with the name and place on
it, the country's flag and the date in the corners, and the day carried as a
colour rather than as a number nobody was reading. Above the carousel, a
segment per day: it says how long the journey is, how far in you are, and takes
you to day nine without swiping past days three to eight.

Everything else, briefly:

- A plus in each day header, starting a draft on that day instead of on today.
- A journey opens on today when today is part of it, and at the beginning when
  it is not — a journey that ended last spring is something you read.
- Suggestions can be waved away one at a time. The row survives so the trip sync
  does not offer the same place again; journey settings brings them all back.
- Mood, weather and the pros/cons list can be switched off per journey. Nothing
  written is erased — the form stops asking, the values stay.
- The weather fills itself in at the desk the way it already did on the phone.
  The date picks the source, so writing up last Tuesday gets last Tuesday's.
- Enter opens the next pro or con, below the one you are in.
- The verdict moved behind the story's fold, and the fold itself came in from
  nine lines to four.
- The external picker lost the count it was printing twice and some padding it
  could not afford inside the editor.
- The phone can reorder entries within a day at last; the desktop arrows finally
  have translated labels.
- Marker labels are off where a carousel already names the entry, and on where
  they are the only name a marker has.
- Search over a journey's titles, stories, places and tags, accent-blind.
- An entry knows which country it happened in, resolved once from its
  coordinates against the bundled boundaries.

Not built, and why: the photos-for-markers idea needs clustering first or thirty
markers become soup, and the active entry's photo is already on screen in the
card below. Removing mood, weather and the verdict outright would take them from
everyone who uses them, so they became switches. `capture` on the file picker
takes the photo library away on a phone, which is why the camera is a second
input; it is hidden on desktop now instead. The suggestion strip in the new-entry
flow would be a third surface over the same data — tapping a suggestion already
opens the editor filled in.

The one gap the list missed and this branch does not close: Journey is the only
domain with no offline path. That belongs in its own change.
2026-09-13 18:45:57 +02:00
Maurice 3d201e1ced fix(roadtrip): the check-out leaves the drive, and a long stay carries the clock
A check-out is the LATEST the room has to be handed back, never the earliest
anybody may drive on, so it says nothing the chain can use. It shaped four things
it had no business shaping: the departure the day was built from, the length the
rail printed for a booked night, a day-selector and a clock field in the stop
dialog, and the branch that sent a night's STAY badge to the booking form because
its length was not editable here. All four go. The booking keeps its check-out;
it is a booking detail and lives under Days, where it is entered.

What replaces it is the thing that was always the honest answer: how long you
stand there. That is the traveller's own statement rather than a deadline, and a
stay running past midnight now carries the clock into the morning after — a stop
pinned before it ends keeps its hour and collects the late finding, as it would
from any leg it cannot make. A stay spanning whole days walks the clock forward
over them. An ordinary evening hands on nothing: the night between two days is
not a wait somebody is serving out.

The stay dialog goes to a full day rather than four hours, since it is now the
only place a night is given its hours, and offers eight and twelve as presets.
Every stop answers the same question the same way.

Overlay panels lose the skeleton placeholder. It was drawn in the page flow, so
the first time each dialog was ever opened a pale block flashed under the planner
and never again once its chunk was cached.
2026-09-13 14:23:49 +02:00
Maurice d0d3f455a8 fix(planner): let the day list see which stop a booking wrote
The row shapers name every column they pass on, and accommodation_id was in
none of them. So the field the day list hides on never reached the client, and
the booked night was listed as an ordinary place next to the overnight block
that already named it. The stop was correct in the table the whole time; the
three read paths just dropped it on the way out.

The stamp on the freshly written stop had the same hole from the other side: the
INSERT went in without the booking id and an UPDATE added it afterwards, so the
row handed back in the answer, the one the client that booked the night puts
straight into its store, carried none. It goes in with the INSERT now.

The shared link gains the same filter. It has no road trip view to need the stop
and already shows the booking as its own chip on the day, so the row was only
ever the same hotel twice.
2026-09-13 12:59:19 +02:00
Maurice edf0538260 fix(roadtrip): keep a booked night out of the day list, and off the traveller's stops
Three things the first pass got wrong.

A stop a booking puts on the day is for road trip mode, which needs the
hotel in the driving chain. The day planner already shows that booking as
its own overnight block, so the row there would be the same hotel twice.
It is hidden in Days whatever the service-stop switch says, and the switch
goes back to meaning what it says: the petrol stations the traveller added.

Cancelling a night from the road trip popup took the stop with it. That
control means "this is a pause now, not a night", and the stop sits
mid-drive where re-adding it would land it at the end of the day. The
booking now hands the stop over instead of deleting it.

Moving a booking whose stop belongs to the traveller put a second one on
the new day and left the first behind, which is the duplicate this change
exists to remove. Only a booking's own stop moves now. Stays booked before
this shipped get theirs from the migration, not from the next edit they
happen to receive.
2026-09-13 01:42:52 +02:00
Maurice 5b4a7e82ff feat(client): show Dawarich stays, routes and marks where they belong
The trip planner and the journal get a "From Dawarich" list: the stays
recorded for those dates, each one a row that can be dismissed or turned
into a place or an entry after a review step that shows time, name, note
and day. A place that came from a recording carries the Dawarich mark
next to its name so its origin stays visible afterwards.

The recorded route lies over the trip and journal maps as an overlay,
coloured per day and cut along the reader's own timezone rather than
UTC. It is fetched for the window on screen and never stored.

The Atlas gets its own panel beside the map for the countries and cities
the recordings prove, and the wish list is ticked off there, on the day
of the visit, undoably. The phone has its own take on all of it, in the
mobile shell's colours and with native sheets.
2026-09-12 23:37:05 +02:00
Wenyin Wei ef259e7d7d fix(shared): keep the header's decoration from widening the page
The two decorative circles in the header are positioned outside it on purpose
(-40 to -60 on each axis) and nothing in the ancestor chain clips them, so the
first one widened the document by its own overhang: a 390px phone reported a
450px scrollWidth and the whole page could be dragged sideways (#2345).

overflow: hidden on the header is what the decoration always meant.
2026-09-13 01:13:34 +08:00
Wenyin Wei 12eabbb685 fix(shared): cluster the public share page's markers
The share page drew every stop as its own pin while the planner drew the same
trip as a handful of clusters, so a shared link opened on three dozen
overlapping markers. On a 300px-tall map that covers a whole city, the pins
underneath can be neither read nor tapped (#2343).

The option block and the icon factory move to components/Map/markerCluster.ts,
which both maps import, so the two cannot drift apart again. The planner's
options are unchanged.

leaflet.markercluster's two stylesheets were imported by MapView.tsx alone.
The share page never mounts MapView, so it imports them itself: without them
the bubbles keep the library's default styling and spiderfy's legs are
invisible.
2026-09-13 01:11:14 +08:00
Maurice aa0d62f063 fix(admin): read key presence from the app config, not the form
The place search provider block now renders on a managed install too, and
there the key fields are empty by design: the operator owns the credentials
and user-profile never sends them. An operator key set through the
environment is invisible in the fields either way. The warning behind the
provider select read those fields, so it told every managed admin that
search "falls back to OpenStreetMap" while their key was answering.

app-config already carries the answer as has_maps_key, and it resolves the
same chain the search itself uses, environment included. has_amap_key sits
beside it, so both come into the store at boot and both follow a save.
2026-09-12 18:03:11 +02:00
Maurice 2fd5b6b00a fix(admin): keep the place search provider reachable on a managed install
The API keys card was hidden wholesale on a managed install, which took
the provider choice with it although the server leaves places_provider
to the admin there. The card stays; the keys, their Save button and the
Google switches fold away, on the phone shell now too. The mobile
warning uses the pending token instead of an undefined one.

The desktop tests find a key input through its labelled toggle rather
than by position, which is where the accessible name sits.
2026-09-12 18:03:11 +02:00
A1phaN 57eac32f9f feat(map): amap as map api provider and display template 2026-09-12 18:02:57 +02:00
Maurice bc04072dc9 keep overnight checkout planning after a later arrival 2026-09-12 16:22:11 +02:00
Maurice 0c78462567 show only planned places on the roadtrip map 2026-09-12 16:05:02 +02:00
Maurice ac57321840 merge dev and reconcile roadtrip planning 2026-09-12 15:45:34 +02:00
Maurice b10b02c685 finish overnight stops and cross-day range warnings 2026-09-12 15:37:09 +02:00
Konstantinos Thermos 359551653f Merge pull request #2319 from liketrek/feat/trip-route-overview
feat(maps): draw every travel day's route at once, with the trip total
2026-09-12 14:49:44 +03:00
Maurice f56e7a161e add open charging data and polish roadtrip warnings 2026-09-12 13:25:15 +02:00
Maurice 41b18f970c feat(vacay): add manual school holiday calendars 2026-09-12 12:51:27 +02:00
Maurice ff5fa55fad add Google Maps route import and plugin corridor search 2026-09-12 12:24:33 +02:00
Maurice b773faff70 feat(roadtrip): share trip preferences and improve service stops 2026-09-12 00:24:45 +02:00
Maurice 491cfccc56 feat(roadtrip): add daily scheduling and configurable day endings 2026-09-11 22:28:03 +02:00
Konstantinos Thermos 3c735c80a4 Merge remote-tracking branch 'origin/dev' into feat/trip-route-overview
# Conflicts:
#	client/src/components/Map/MapView.tsx
#	client/src/components/Map/MapViewGL.tsx
#	client/src/pages/TripPlannerPage.tsx
#	client/src/pages/tripPlanner/useTripPlanner.ts
2026-09-11 16:22:21 +03:00
Maurice 2c06c9904b feat(collections): a list as a file, so it can leave the instance (#2198)
Sharing a list works between people on the same TREK. This is the other
case: giving one to somebody who runs their own, or keeping a copy. Export
sits beside Edit and Share in the list header and downloads the open list;
Import sits beside New list in the rail, reads a file, shows what is in it
and makes a new list of it. Desktop only, which the route already decides.

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

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

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

An import is always a new list. Importing into an existing one would mean
deciding what happens to a place that is already there, and every answer to
that is a merge nobody asked for.
2026-09-11 14:58:34 +02:00
Maurice a8e4c45576 feat(share): show a stop and a booking in full on a shared link, and no more (#2320)
A public link showed a stop as its name and one line under it, the address
or the description, whichever came first. The owner had written both, and
a note on the place and another on the day, and had set how long they meant
to stay and how to reach it. All of that shows now, laid out the way the
planner lays it out, and every link leaves the page in a new tab with no
opener. A booking shows the note the owner attached and the page they
linked, labelled by where it goes.

What travels to the page is chosen column by column now, where it used to
be the whole row. The whole row of a booking carried its confirmation
number, its import trail and its raw metadata, and the metadata is where an
imported ticket keeps its seat and its record locator; the whole row of a
place carried the owner's booking notes on it and the Google identifiers.
None of that belongs to somebody holding a link. Metadata keeps the parts
that describe the journey, by an allow-list, so a field the import starts
writing tomorrow does not become public by appearing. Links are accepted
only as http or https, on the server and again on the page.
2026-09-11 11:01:25 +02:00
Maurice 9ce8b7ea9e test(roadtrip): cover the fold, the seam and the live via, and fix what that found
Cases appended to four existing suites for the lines this branch left unrun: the
fold state and the map places it hides, the owner-based via anchor, the day seam
and the proportional geometry slicing, and the live roadtripVia:changed events.
With the four new suites in the commits before this one, coverage of the lines
this branch adds goes from 69 % to 96 %, measured the way the gate measures it —
the routed lines against the diff, not a file average.

Every added case was mutation-checked: the rule it claims to pin was broken in
the source and the case had to fail. Two of them found real defects rather than
missing coverage, and both are fixed here.

A via dropped on the incoming night drive went to the wrong leg. The guard for
it asked the insert index whether the point lay before the card's first stop,
and that index cannot answer: insertIndexForAlong clamps to at least 1 for any
card with two stops or more, and the rail publishes no other kind. So the branch
read correctly and never ran, and a via on the night stretch was filed after the
first drawn stop — the route ran forward, doubled back to the point and carried
on, which is exactly what the comment above it said must not happen. Asked of
the distance instead.

The two one-field writes the rail offers, the stop kind and the fill level, went
straight to the API while their neighbour checked place_edit first. The server
refused them, so nothing was ever saved, but a reader got an error toast for
touching a control that should not have acted.
2026-09-10 21:40:51 +02:00
Maurice 3f0eb210d8 feat(roadtrip): move a stop reached after midnight onto the day it is reached
A day that runs past midnight used to keep every one of its stops on its own
card, so day one ended at 03:12 with three places nobody reaches on day one and
day two began with a drive that had already happened. The rail now lays the
stops out by the date they are actually reached: when the clock goes backwards
between two arrivals the ones after it move to the following day, into a block
that says where they came from, and the night drive and its kilometres go with
them. It is an arrangement, not a write — the day planner beside it is
untouched, nothing reaches the database, and a shorter stay the evening before
puts everything back on its own.

The map follows the same arrangement, because a boundary drawn in the rail and
not on the map is two answers to one question. Day one now ends where its last
reachable stop is and the line, the legs and the totals are cut to match.

Two switches in the driving settings, off by default. "Connect the days" routes
the gap between one day's last stop and the next day's first, which is real
driving the rail has never drawn, and counts it. "A colour per day" then puts
the days back into a line that has become continuous, and tints each day header
to match.

Collapsing a day from its header takes its route and its places off the map as
well as out of the rail, and the categories in the corridor panel fold the same
way. The refuel band is one full-width red band with the pump as its button,
the results are chips, and the driving dialog is two columns of panels with the
figures on one right edge.

Also fixes a via that would not adjust the route once dragged. Five separate
causes, all measured rather than guessed: leg threading used the card index
instead of the owning day's, the anchor lookup returned chain indices, it
filtered by the card rather than by the anchor's owner, seams were routed
without their vias, and a seam once routed was never re-fetched.
2026-09-10 21:40:51 +02:00
Maurice 96ab748f34 feat(roadtrip): let one stop say how full it fills
The fill level was one figure for the whole trip, and a trip does not have one.
A motorway rapid charger is worth about 80 % because the last fifth costs as
long again; the charger at the hotel is worth all of it because the car stands
there all night. Between those two the difference is a whole leg, and a single
setting has to be wrong about one of them.

So the badge on a refuelling stop is now the way in: it shows what that stop
will actually fill to, its own figure or the traveller's, and opens a panel of
the six answers people actually give. Under them it says what the choice buys in
kilometres, because 80 % is an abstraction and "about 290 km from here" is the
thing being decided. Picking the figure a stop already has hands it back to the
setting, the same gesture the kind picker uses to un-pick a kind.

The value lives in places.fill_percent, beside stop_type, because it is a
property of the stop rather than of the person. Null means "follow the setting",
which is every stop that predates the column and every stop nobody has an
opinion about, so nothing changes for a trip that never touches it.

Two pieces of shared machinery came out of it rather than being copied. The
popover chrome — measure before showing, flip when the window is short, close on
an outside click, Escape or a scroll — is one component now, used by the kind
picker and this one. So is the assembly of the range from seven settings, which
the routes hook and the rail both need and which would have drifted the first
time a field was added.
2026-09-10 21:40:51 +02:00
Maurice 5b147af0b6 feat(roadtrip): tell petrol from electric, and shape the refuel band like a drive band
Four things that only showed up once it was on screen.

The range budget treated a petrol station and a charger as the same thing, so an electric
car pausing for coffee at a filling station had its battery refilled on paper: the
warnings went quiet for the rest of the day and the driver was told nothing. A vehicle can
now be named, and then only what actually refills it counts — and the search asks for that
kind alone instead of offering pumps to a car that cannot use them. Not saying keeps the
old behaviour, which is what everybody who never opens the dialog is on.

The band was its own shape in a rail made of drive bands. It is one now: the same rail,
the same height, the same type size, a warning tint instead of a ring, and the figures
where a drive band keeps them. The buttons are icons of the same 18px square the shuffle
mark uses, so the band stays one line beside its neighbours.

The old range warning stood down where the band shows. Both at once said the same thing
twice in the wrong order — the answer above the problem — because the warning marks the
stop somebody finds out at while the band sits on the leg the fuel ends on.

Two measured corrections to the search itself. The server turns a box into a circle of
half its diagonal and silently narrows anything past 20 km, so a box of ±25 asked for 35
and came back flagged as narrowed EVERY time, which read on screen as "we could not
check"; ±14 asks for 19.8 and is answered in full. And the circle is centred 12 km before
the dry point rather than on it, because only road already driven is any use — centred on
the point, half the circle was spent on road nobody can reach.

An answer that finds nothing now leaves the button rather than only a close. The place
search is a shared public service that does time out, and "it did not answer" with no way
to ask again reads as broken rather than as busy.
2026-09-10 21:40:51 +02:00
Maurice 95d1752a3d feat(roadtrip): suggest where to fill up, not just warn that you cannot
The range budget has always been able to say "you have driven 245 km on this tank" and
never "and here is where to do something about it". The review put it plainly: the value
is in the suggestion, not in the warning.

The warning sits on the stop the car ARRIVES at, which is where somebody finds out. The
fuel runs out earlier, often most of a leg earlier, and a filling station offered at the
warning is one the car cannot reach. So the dry point is computed where it actually is,
a search runs around it on request, and what comes back is filtered to what can be
reached.

Three filters, each one there because its absence gives a wrong answer rather than an
untidy one. Only what lies before the tank empties, because a pump past it is on the far
side of an empty tank. The detour counts double, because 8 km off the road is 16 km of
driving. And a reserve is kept back, because the range figure is a round number from a
manual, not a measurement.

Sorted by the detour rather than by how far it gets. Measured on a real day between
Hamburg and Berlin: the station that got furthest was 9.2 km off the road for one
kilometre more of range, while one 0.9 km off sat almost as late.

Deduplicated by name and half a kilometre, which also came out of that measurement. OSM
maps a charging site as one node per socket: the first three offers were the same
"autostrom plus GmbH" three times over. The four nodes sat in two pairs 6 m and 7 m
apart with 450 m between the pairs, while genuinely different branches of one chain were
18 to 36 km apart — so anything from 500 m upwards separates the two cases.

An empty answer says which kind of empty it is. "Nothing on the reachable stretch" after
a request that failed or was cut short states something that was never checked, and the
server flags both cases itself. Offline it does not offer at all: the place write queues
offline but the day assignment does not, so accepting one would leave an orphan.

Nothing is searched until it is asked for, and one press is one request against a small
circle rather than the sixteen boxes a corridor run costs. Accepting hands the choice to
the same popup a corridor hit goes through, so the stop kind, the dwell time, the
position, the via re-anchoring and the re-route are the paths that already work.

The dry point counts DRIVING metres only. The day's drawn line concatenates every run,
ferries and walks included, while the budget skips those, so walking that line to the
same figure would overshoot by the length of any crossing and put the marker out at sea.
2026-09-10 21:40:51 +02:00
Konstantinos Thermos bf1900c3a0 feat(maps): draw every travel day's route at once, with the trip total
Extract the day route builder out of useRouteCalculation into a pure module and
aggregate it over the whole trip: one map with every day's route, each day in its
own colour, the mode per stage and the trip's total distance. Straight lines first,
roads as they answer; a refused leg keeps its line and adds no distance.

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

Addresses #1736
2026-09-10 15:57:48 +03:00
Maurice 59b7c35e87 fix(roadtrip): re-pin a via to the leg it was dragged onto
Dragging a via somewhere else on the map changed where the point IS and never where it
sits in the chain. The anchor that decides which two stops it is threaded between was
worked out once, when the point was dropped, and the drag sent only coordinates —
`roadtripViaUpdateRequestSchema` carried a lat and a lng and nothing else.

So a via pulled past the stop it used to precede kept claiming the earlier leg. The
router then drove out to it and turned back before carrying on, which on screen is not a
subtly wrong route: it is a spur that reads as the drag having done nothing at all, with
the original road still sitting there unchanged.

The anchor is not a property of the point but of where the point sits along the drive, so
it is recomputed on a drag the same way it is on a drop. The projection that did that was
inline in addRoadtripVia; it moves to a shared `anchorFor` and both callers use it.

A drag differs from a drop in two ways, both deliberate. It measures against its OWN day:
letting the nearest day win, which is right for a click on open map, would hand the via to
a neighbouring day whose road happens to pass closer and make it disappear from the day it
was dragged in. And it has no 2 km guard: pulling a via well off the current road is
exactly what the gesture is for, and the anchor only says which leg gets bent.

`after_order_index` is optional on the way in, so a drag inside one leg and an older
client still send just the coordinates, and the stored pin is then left alone rather
than cleared.
2026-09-09 19:03:00 +02:00
Maurice 77162aa8d5 feat(roadtrip): avoid tolls, motorways and ferries, through a second engine
The switch a road tripper keeps asking for has never worked. OSRM's `exclude` is the only
avoidance TREK has, and the two public hosts it ships with answer it HTTP 400 on every
leg: their car profile is built without excludable classes, and their own Lua says why
("no memory for classes on demo server"). So "drive this without the tolls" produced
nothing at all on a default install, and the fallback inside the alternatives bar quietly
gave up after one refusal per host.

FOSSGIS runs a public Valhalla on the whole planet next to that OSRM, keyless, and there
avoidance is a runtime weighting rather than a profile that has to be rebuilt. This adds
it as a second engine asked only the questions the first cannot answer, and puts three
switches under the road trip settings that change the road itself rather than offering an
alternative beside it. Paris to Lyon measured 465.8 km / 297 min with tolls against
492.0 km / 454 min without; Hamburg to Copenhagen comes back ferry-free at 469.8 km,
which OSRM cannot produce at all — where a ferry IS the crossing it answers NoRoute
rather than driving round.

Four things the docs would have got wrong, all measured against the live instance:

- `format=osrm` does not carry `has_toll`. That flag is the entire reason for doing this,
  and it exists only in the native answer — which is also 5.7x smaller, because the OSRM
  shape drags every intersection along. So the adapter translates instead of borrowing a
  familiar envelope.
- Without a `radius` Valhalla refuses a point it cannot bind to a road, where OSRM still
  snaps one. Vienna's Stephansdom, in a pedestrian zone, answers error 442 until a search
  radius is allowed. TREK is full of such places, so every location carries one.
- Ten locations is a hard cap and a road trip day is bundled into one request. A day over
  it is split into runs overlapping by one point, which is exact rather than approximate:
  a measured six-stop day came back within two metres and under a second of the same day
  asked whole.
- Its drive times run 5.4 % above OSRM's over ten European legs and 36 % above on one, and
  those times feed the arrival clocks of a day. So a day is answered by one engine or by
  neither, never by both, and a piece that will not answer loses the whole day rather than
  half of it — the schedule chains leg times blindly and would otherwise print a complete,
  plausible, silently wrong timetable.

Because the weighting can fail, the honest surface is the answer rather than the request.
`use_tolls: 0` still returns a tolled road where there is no untolled crossing, so what
the road actually turned out to be is read back per leg, and a day that could not avoid
what it was asked to avoid says so on its own header instead of wearing a label the road
disproves. The switches disable themselves, with a reason, on an instance pointed at its
own OSRM: that operator chose against public hosts, and a second one they never asked for
would undo the choice.

Two things that would have broken quietly and did not:

- The route cache keys on `profile:unit:coords` and is shared with the day planner, the
  studio and the booking geometry. Without the avoidance in the key, flipping a switch
  returns the old road out of cache in under a millisecond — and worse, an avoiding answer
  stored under the plain key hands those other callers a road nobody drives.
- `snapped` is OSRM-only and carries the dashed kerb-to-door spur from #2273. Valhalla
  echoes the coordinates it was given and never says where it bound them, but the ends of
  a leg's line are exactly those points: measured against Füssen, its first vertex matches
  OSRM's snapped location to the digit.

The alternatives bar gets the same engine, which finally makes its "no motorway" and "no
tolls" offers appear on a default install, plus a ferry-free one. `sameRoad` now compares
geometry rather than numbers there, because two engines' distances for the same road sit
kilometres apart and the old 200 m tolerance called that a discovery.

Desktop only, like the whole road trip mode: nothing under client/src/mobile renders it.
2026-09-09 19:03:00 +02:00
Maurice 756e0fb908 fix(places): let the pool follow the open day, and say that it does
The map has narrowed to the selected day on the Planned filter since #2024. The pool
beside it did not, so a trip with 55 planned places read 55 in the tab and drew five
pins on the map, with nothing anywhere to say the two were answering different
questions. Reported twice in the same thread as the map being broken: you pick a day,
open Places, tap Planned, see everything you ever planned, and the map shows a handful
of it.

The list and the tab count now use the same set the map does. Unplanned deliberately
stays trip-wide: a place assigned to some other day is planned, whichever day happens
to be open.

A note under the tabs says so and closes the day when dismissed. It sits there rather
than over the map because a chip on the canvas is out of reach on a phone, which is
where this was reported from.
2026-09-09 15:50:47 +02:00
Maurice 88129e4546 feat(maps): search the view in road trip mode too
The category pill was switched off whenever road trip mode was on, which left the
corridor search as the only way to find anything. The two answer different questions
though — 'what is along this drive' against 'what is in view right now' — and looking
for a hotel at tonight's stop is exactly the second one. Furkot shows accommodation
at a stop for the same reason (disc #1797 thread).

Both searches now draw together. A place found by both appears once, as the corridor
knows it, because only that copy carries how far along the drive it sits, and the
merged list keeps its reference while nothing changes so the map does not rebuild
every pin on each render.
2026-09-09 14:52:50 +02:00
Godric Hood 9543fdc3f2 feat(collab): add shared links, chat images, and plugin fullscreen
Adds a Collab Links panel (add/pin/delete) with translations in every locale so tab and button labels are not raw i18n keys, chat image attachments (up to 4), and native fullscreen on plugin iframes. Includes Zod request DTOs and CollabPanel coverage for the Links tab.
2026-09-09 00:11:00 +02:00
Maurice 59a1756529 style(admin): lay the admin panel out in two columns and match the banner buttons
The User Defaults and Notifications tabs were one narrow column of cards on a
page that has no max width, so the right two thirds stayed empty while the page
scrolled for ages. Both now use the two-column grid the Settings tab already
has, grouped by subject rather than by height.

User Defaults splits into "Default User Settings" (colour mode, units, time
format, currency, booking codes) and a "Map" card holding template, keys,
routing base, preview and engine. The columns are deliberately uneven — the left
one never needs more than its widest option row, the right one holds four fields
whose values are 60-character tile URLs.

Notifications keeps the three channel switches together on the left, because they
all write the same notification_channels list, and puts the operator's own
targets on the right. The admin preference matrix moves in with them instead of
running full width underneath: it decides which of those channels each admin-only
event goes out over. Its label column gets minmax(0,1fr) so it wraps rather than
overflowing in the narrower column.

The transit provider was the one native <select> left on the Settings tab. The
option list of a native select is drawn by the OS, so it followed neither the
colour scheme nor the radius nor the user's text-size setting; it now uses the
CustomSelect the rest of the app uses, and skips the request when the option
already selected is picked again.

In the update banner, "View on GitHub" was a size smaller than "How to Update" on
every axis — 12px against 14px, a 14px icon against 16px, less padding, and 2px
shorter. Two buttons side by side read as one group, so that reads as a mistake
rather than as hierarchy. Same box now; the fill, the colour and the weight still
mark the primary action.
2026-09-09 00:09:34 +02:00