Commit Graph

2770 Commits

Author SHA1 Message Date
Maurice 543a052da7 Merge pull request #2387 from liketrek/fix/pr2273-audit-0917
fix: eleven from a full audit of the 4.3.0 release branch
2026-09-17 16:55:23 +02:00
Maurice 7806560c8f fix(roadtrip): four more from the audit, each one traced to what the user would see
**"Today" was read off UTC while the clock beside it is local.** `localDate.ts` exists for
exactly this and says so in its own comment: between local midnight and the UTC rollover the
two disagree, so east of Greenwich the up-next card hung on yesterday's stage for the first
hours of every night, which is when somebody on a long stage is actually driving. Both the
stage hook and the corridor hook now ask the same helper the rest of the app asks.

**A refused day end left the switch showing a state the trip never reached.** The writer
catches its own failures and shows a toast, so it never throws and the sheet's catch could
not fire, which left the optimistic value standing. It answers whether the day end moved
now, and the switch comes back when it did not. The desktop control ignores that answer, as
it reads its state back from the trip.

**A timed-out corridor search reported more unsearched boxes than it had.** Every worker
still alive ran the deadline branch and each added the whole remainder, so three workers
reported three times the boxes left, which can exceed the number of boxes there ever were.
The remainder is claimed before it is counted, which also ends the other workers' loops.

**A night that fell mid-leg had its morning drive dropped from the stage map.** The join
mark went on the first leg of every day opening on an automatic night, but only a night
standing ON a stop is a connection between two days. `dayWindow` records that difference as
a whole `position` against a fractional one, and where it is fractional the morning drive is
the last stretch to this day's own first stop. It was being filtered off the stage while its
distance stayed in the day's total.
2026-09-17 16:49:03 +02:00
Maurice c8f9edad87 fix(i18n): the corridor's truncation note takes a singular, and three plurals were wrong anyway
`mobileTrip.rtTruncated` interpolated a count into a sentence naming the stretch once, so at
one truncated area it read "1 stretches". Two keys now, picked by the count, the shape this
codebase already uses where a number changes its noun.

Writing the singular turned up three plurals that were wrong at counts the old one-form key
could never reach. Polish and Czech both had the genitive plural with a singular verb, which
is right at five and wrong at two; Arabic had the 3 to 10 form, wrong at two and again from
eleven. All three now put the count after a colon, where no form has to agree with it.
Russian and Ukrainian were already correct at both, since their phrase leads with a
locative, and were left alone.

The eight languages that keep the singular noun after a numeral differ only in the numeral,
which is what they should do.
2026-09-17 16:40:05 +02:00
Maurice 92ccd24eed fix(roadtrip, journey): six the audit turned up, each one verified before it was touched
**A saved stay left an invisible sheet standing, and the day swipe stayed dead behind it.**
The stay sheet reopened 'rtstop' by spreading its own payload, which carries a place id.
That sheet is located by day and assignment, so it resolved to nothing and drew nothing,
while `shell.sheet` still pointed at it and `menuOpen: !!shell.sheet` kept the day swipe
blocked until something else closed it. The row rides along now, both ways, and with no row
to go back to the sheet closes rather than opening one that resolves to nothing.

**"End the day here" showed off for a day that already ends there, and switching it on
deleted that day end.** Two things close a day: the stop's own flag and a manual boundary
filed against it. The writer has always known about both; the phone switch read only the
flag, so the tap ran through the writer's boundary branch and cleared the boundary. The
question is asked once now, in the planner beside that writer, and the desktop reads the
same answer it always computed inline.

**The road trip chain printed 24-hour clocks at a reader on a twelve-hour one.** The model
hands clocks over as HH:MM, which is what the schedule computes in, and the chain, the head
card and the up-next card printed it straight. The desktop rail, the day timeline and this
tab's own sheets all honour the setting; these go through the same formatter, carried in the
row chrome so no two rows can disagree.

**The up-next card's navigate button read "places.navigate".** No locale has that key. It is
`inspector.navigation`, which is what the stop sheet beside it, the place inspector and the
place sheet all use, with the single-target label when there is only one.

**Offered roads were invisible on a satellite basemap.** The imagery is anchored under the
first layer TREK draws, found by prefix, and the roads offered for a leg are added before
the route source so they sit under the current route. That also puts them before the first
`trip-` layer, so the raster went straight over them. The prefix list knows them now.

**A journal grew a day section for every day Dawarich recorded, all year.** The hook reads
every open stay the account has, and the timeline took all of their dates. A week in May
therefore sprouted sections from September. Bounded by the journal's own span now, its
entries together with the dates of the trips it links, and with no span at all it offers
nothing rather than everything.
2026-09-17 16:38:17 +02:00
Maurice 08df1812be fix(sonar): spell out the order these five sorts already have
SonarCloud's S2871 fires on a bare .sort() over strings and asks for localeCompare, and it
had five of them on new code, which is what put new reliability at D and failed the gate.

All five sort ISO dates or query parameter names, where the default lexicographic order is
exactly the one wanted. Taking the rule's suggestion literally would have been the wrong
fix twice over: a locale-aware comparison reorders dates differently per runtime, and on
the Amap signature it would produce a canonical string Amap itself does not compute, so
every signed request would come back rejected.

So the comparator is written out rather than changed. Byte order, explicitly, with a line
saying why it has to stay byte order. Behaviour is identical: the 43 Amap cases, the 40
track cases and the 30 client cases over these modules all pass unchanged.
2026-09-17 16:19:02 +02:00
Maurice ce5cc49b58 Merge pull request #2384 from liketrek/feat/roadtrip-mobile
feat(roadtrip): bring the road trip addon to the phone
2026-09-17 15:33:47 +02:00
Maurice 8860e16149 test(journey): cover the Dawarich fold, and mend what the switches and the wiring assumed
Three things the CI would have caught.

The settings dialog's switches are the shared ToggleSwitch now, which is a button with
aria-pressed rather than a role="switch" with aria-checked, and three tests still looked
for the old shape.

The page's wiring test mocks the whole hook, so the day map the timeline reads was
undefined there and every case in the file died on it. The mock carries it now, empty,
because the fold has a suite of its own.

That suite is new: what it pins is the fold rather than the rows inside it, which the
panel's own tests already cover end to end. That it says nothing on a quiet day, that the
line reads shut, that the count takes the singular at one, that it starts closed, that
both actions reach the journal with the stay they belong to, and that the span follows
the traveller's own clock.

The grouping the hook does moved into the model beside the other pure shaping, where it
is four cheap tests instead of a round trip through MSW: by day, in the order they were
lived, only what is still waiting, and no bucket at all for a day whose stays are all
handled.
2026-09-17 15:32:19 +02:00
Maurice 185f6c5335 Merge pull request #2373 from liketrek/fix/journey-skeleton-per-day
fix(journey): a place kept across two days is two days of the journal
2026-09-17 15:31:37 +02:00
Maurice 1e338e2c71 Merge pull request #2383 from liketrek/fix/mobile-markdown-place-description
fix(places): render markdown in the mobile place description
2026-09-17 15:31:13 +02:00
Maurice 2879433bd3 fix(dawarich): the panel opens to its full height and the rail scrolls past it
Capping it was the wrong half of the fix: the stays were reachable but the panel was cut
off at half the rail, which is not what opening something means.

It sits inside the places list's own scroller now, above the rows, so it simply grows to
whatever the day's stays need — 3350px for forty of them — and the rail scrolls past it
to the places. No second scrollbar, no fold. As a band of its own it could never do that:
the list is the flex child that scrolls, so anything unbounded above it squeezed the list
to nothing and took the rail's scrolling with it.
2026-09-17 15:05:29 +02:00
Maurice 1908d6f5f9 fix(dawarich): the places rail scrolls again with the panel open
The panel sat in a wrapper that could not shrink and did not scroll, so opening it took
whatever height the day's stays needed — forty of them is over 3000px — and squeezed the
places list, which is the flex child that scrolls, down to nothing. The rail stopped
scrolling and most of the stays sat below the bottom edge with no way to reach them.

The wrapper is capped at half the rail and scrolls on its own. Open, the stays are all
reachable and the list underneath is still a list; shut, nothing changes, because the cap
only bites once the content passes it.
2026-09-17 15:02:58 +02:00
Maurice 425e1ef183 fix(dawarich): the trip rail drops the duration badge
A stay in the trip rail showed both when it happened and how long it lasted. The rail is
narrow and the row does not wrap, and of the two it is the arrival that places a stop; how
long somebody stood there is a second figure competing for the same line.

Off wherever the panel is scoped to a trip, which is the planner rail and the phone's
places browser, and the same test `showTrip` already uses. The journal keeps it: there a
stay becomes an entry, and how long it lasted is part of what the entry says.
2026-09-17 14:58:30 +02:00
Maurice 9f57bbf4fb fix(dawarich): the stay times follow the clock the traveller set
Every time Dawarich shows was cut straight out of the timestamp and printed, so on a
12-hour setting the app said "2:05 PM" everywhere and "14:05" here. Both ends of a range
go through the same formatter the rest of the app uses now, and the argument is
required rather than optional so the next caller answers the question instead of
inheriting the default.

That covers the rows in the panel, on the phone and in the planner, and the summary line
a journal day carries, which reads its span through the very same function so the fold
and the stays under it cannot disagree about the clock.

The accept dialog was already right: it fills a CustomTimePicker, which knows the
setting, from the 24-hour value it keeps internally.
2026-09-17 14:53:58 +02:00
Maurice 3dd3e1e8d2 fix(journey): the day count stays the journal's own
A day a pending stay was waiting on now joins the timeline so the stay can be reached,
and the header counted those days as part of the journey: a journal of seven days read
"11 days" because Dawarich had recorded on four more. The figure comes off the entries
again, which is what the word means.
2026-09-17 14:47:23 +02:00
Maurice bbcf17d816 feat(journey): the day carries its own Dawarich stays, folded
The stays Dawarich recorded used to be a panel above the timeline: every open stay of
every day, stacked over the entries. A fortnight of driving puts forty of them between
the reader and their own first entry, which is the scrolling Roel described on the
16th. They belong where the day is.

Each day of the timeline now ends in one line: the Dawarich mark, how many stays, and
the span they cover. A tap opens them, and what opens is the panel's own rows, imported
rather than copied — one row this detailed in two places would be two things to keep in
step and a straight hit on the duplication budget. Nothing is drawn on a day with
nothing pending.

A stay falls on the day it happened, which is often a day the journal has no entry on
yet, and that is the whole point of offering it. Those days join the timeline so their
stays can be reached at all; without that the only way to a quiet day's stays would be
to write an entry on it first. Not while a search is running, where a day whose entries
the query filtered out is not a day the reader is looking at.

The count takes two keys picked by the number, the shape this codebase already uses for
a noun that changes with it: there is no plural engine in the i18n layer, and one key
with a placeholder in it reads as "1 stays". Russian, Polish, Czech, Ukrainian and
Arabic inflect again at 2-4 and 5+, which two forms cannot express, so their plural puts
the number after a colon, where no form has to agree with it.

The phone is untouched: it opens the same list in a sheet from its own button, which is
where it works on a screen that shows one entry at a time.
2026-09-17 14:30:14 +02:00
Maurice d18077d171 fix(journey): the Dawarich button clears the card rail, and the settings switches read in the dark
Two on the journal.

The phone's Dawarich button floated a hard-coded 128px over the dock, which is short of
the rail underneath it: a day scrubber over a row of covers, and a cover grows from 152
to 180px when it becomes the active one. The button sat on the cards. It now clears the
rail's measured height, observed rather than read once, because that height changes with
every swipe. No rail, as on an empty journal, means no offset.

The settings dialog drew its four switches by hand in emerald and zinc, with a white
knob. In dark mode that is white on near-white: the screenshot is three pills where
neither the state nor the knob can be made out. They are the shared `ToggleSwitch` now,
which is the component every other switch in the app uses and which solved this exact
problem with `--accent-text` on the knob. The row is no longer one big button, since a
button cannot hold another: text left, switch right, the layout the settings screens use.
2026-09-17 14:10:52 +02:00
Maurice 84923b97a7 fix(roadtrip): the connection built by a day window is a connection too
The last change marked the drive into the next day and kept it off a phone stage, but it
only caught the connection `connectDays` builds through `inboundAt`. A trip with a day
window set never takes that path: the window switches `inboundAt` off and builds the days
itself, so day 2 opens on an automatic night standing where day 1 stopped and its first
leg is the drive on from there. That leg is the same connection, it just arrives as one
of day 2's own legs under day 2's number, and the stage drew the whole way back to
yesterday's last stop on today's map. The setting is not even involved: with a window it
happens either way.

So the leg out of an opening night is marked as a join as well, and the phone drops it
with the rest.

One exception, because the filter must not empty the screen: a day that is nothing but
driving, night to night with no stop of its own, has only the connection to show, and
there the line IS the day. When dropping the joins would leave a stage with no line at
all, it keeps them.
2026-09-17 13:55:56 +02:00
Maurice c0ff4dabb3 fix(roadtrip): a phone stage no longer draws the drive into the next day
"Connect the days" is a whole-drive setting: with it on, the leg from one day's last
stop to the next day's first is drawn in the colour of the day it leaves, so the desktop
map reads as one continuous drive across every day. The phone has no such view by
default. It shows ONE day, and the stage filter went by day number alone, so that
connection came along: a line running off the day on screen to a place it never visits,
and on this trip the longest thing on the map by far.

The assembler now says which lines are connections (`lineJoins`, parallel to `lines` and
`lineDays`) rather than leaving each surface to guess, because the day number cannot tell
them apart: it reads the leaving day for both a day's own legs and for the drive out of
it. A stage drops them; the all-days view keeps them, which is the view the setting is
for and is one tap away on the same map, next to the day chips.

Routes read back from an older cache carry no marks, so nothing is known to be a join and
nothing is dropped: those days draw exactly as they did before.
2026-09-17 13:48:18 +02:00
Maurice d07aa80093 fix(roadtrip): the stage map shows its own night pause, and only that one
The moon pill marking where a travel day ends is back on the phone, filtered to the day
on screen. The markers are built from the whole drive, which is right for the desktop
rail and for the all-days view, but a stage draws one day: unfiltered, day 1 carried day
2's pill as well, sitting in the middle of a map that is otherwise entirely day 1. Two
ends on a map that draws one is a question rather than a label.

Off a stage the list is left alone, because in the all-days view every night does belong
to a line that is drawn. The plan tab is unaffected either way: its vias come from the
route calculation, which has no night pauses in it.

This is what the map lost when the pills were dropped altogether: on a day whose drive
ends somewhere other than its last stop, the pill is the only thing that says where.
2026-09-17 13:31:43 +02:00
Maurice 911f3e2583 fix(roadtrip): the phone chain drops the placeholder leg into an automatic night
`dayWindow.stationary()` puts a leg between the last stop of a day and the automatic
night that closes it: same coordinate at both ends, nothing measured, no texts. It is
bookkeeping, not a drive. Drawn as a pill under the last stop it said "No route" — and
before the template was guarded, a bare " in " — under a place the traveller had simply
arrived at.

Matched on the shape rather than on the next stop being a night, because what makes it
not a drive is that it goes nowhere: a day ended at a boundary dragged down the road HAS
a leg with a distance, and that one keeps its pill. A leg nothing has routed yet keeps
its row too, so it can still say it is pending.
2026-09-17 13:26:10 +02:00
Maurice 0b1ca1c98a feat(roadtrip): switch a stop between a pause and a destination from the phone chain
The desktop rail turns a place on the drive into a service stop by its own disc: click
the 3, pick the pump, and the 3 becomes an orange disc while everything below it
renumbers. The phone chain drew the same disc but it did nothing, so the only way to
mark a petrol stop was to open the trip on a computer.

The disc is now the control here too, for the same reason it is there: it is exactly
what changes, so it is its own preview. It opens 'rtkind', a sheet rather than the
desktop's popover under the disc, because a palette aimed at an anchor would land under
the thumb that opened it. The kinds stay discs rather than seven rows of words, each
named for a screen reader. A second tap on the kind a stop already is takes it back to
a destination, and once there is something to undo the sheet says so in words too.

It writes through planner.setRoadtripStopKind, the very call the rail makes, so the
chain redraws off the store with no second code path. Without place_edit there is no
handler and the disc is a plain disc, not a disabled-looking control.

A leg pill with no figures in it is fixed on the way past: a segment that reaches the
chain before its routing round has landed carries metres and seconds but empty texts,
and reading only the texts printed the template with both slots empty, so the pill read
" in " and said nothing. The numbers are formatted when the texts are missing, which is
what the rail does with the same segment, and with neither the leg says it is pending.
2026-09-17 13:10:28 +02:00
Maurice a250fa6339 fix(roadtrip): the phone map loses the stage bar over the dock
It named the day's destination with its picture, its stop count, its arrival and the
stage distance, and it stood there for the whole of the road trip tab. The day and
the distance are in the shell's stage header already, and on a map that fills the
screen a permanent card over the bottom edge costs more than the three facts it
added. Tapping it opened the destination's stop sheet; the chain opens the same sheet
from the row that names it.

The slot stays for the one bar that earns it: while other ways of driving a leg are
on offer, the picker's bar stands there. That wrapper is now mounted with the bar
rather than lying empty over the map's own buttons the rest of the time, and the map
floor lifts only while it is up, so on the stage the round controls stand on the dock
like everywhere else.
2026-09-17 12:58:09 +02:00
Maurice 50f0641ad9 fix(system-notices): a notice waits rather than showing its own key names
The keys come from the server's registry, the copy from the bundle in the browser,
and a deploy parts the two for as long as it takes the page to reload: the service
worker keeps serving the old app while /api/config already reports the new version,
and the release notice for that very version arrives naming keys the old bundle
never had. What is drawn is then system_notice.release_notes.headline where the
headline belongs, on the one notice whose whole job is to be read once per release.

Worse, a dismissal in that window is a real one. The entry is per-version, so
whoever closes the gibberish never sees the notes for that release at all.

So a notice made only of unresolved keys is held back until the reload App.tsx is
already waiting for. Nothing is spent by waiting: a notice is used up by
POST /dismiss and by nothing else. A release notice is judged by the strings its own
two-column layout draws, not by the generic title and body it never shows. Copy
written out in words rather than keyed is left alone, and one resolved string is
enough to show a notice: a notice hidden by mistake is the worse failure.
2026-09-17 12:43:53 +02:00
Maurice 782bb365b5 fix(maps): the phone map carries no credit and no night pause labels
The credit was a round (i) under the locate button, and on a full-bleed phone map a
lone control there is the first thing the eye lands on. It is off on the phone now:
one rule in mobile.css against both engines' containers, so Leaflet, MapLibre and
Mapbox cannot drift apart. The desktop map is untouched and still shows the full
credit with its links. Owner's call, made knowingly.

With the credit gone the band the round controls floated on had nothing under it to
clear, so the 38px credit row comes out of --bottom-nav-h and out of the camera
padding the offered roads are framed with. The controls now stand on the map floor.

The night pause pills go with it. On the desktop one is a drag handle with a hint;
a phone has neither hover to explain it nor room to drag, and the markers come from
the whole drive rather than the stage on screen, so day 1 drew day 2's pill as well.
Both via lists go through the same filter, because the plan tab carries them too.
2026-09-17 12:43:45 +02:00
Maurice 222313d198 fix(roadtrip): closing offers or other roads no longer flies the phone map back to an old stop
A final pass over the feedback commits found two gaps in the phone map.

The planner keeps a point it was asked to show until its next routing
round, and once the fuel offers or the other roads for a leg close it hands
that very array back. MMapArea read it as a new focus, so closing either one
flew the camera to the stop "Show on map" had pointed at, on another day's
stage too. holdFocus now remembers the arrays it already held and only
takes a new one as a focus, with the open picker's own roads as the one
exception.

The all days view still took its pins from the planner's planned list,
which on a phone leaves out booked nights and hidden service stops, so the
drive ended at a hotel with no pin to open it from. It now reads the places
of the stored visits, like the stage does.
2026-09-17 00:36:57 +00:00
Maurice 6a6c7f1882 feat(roadtrip): other ways to drive a leg, on the phone
The desk offers alternative routes from the drive band between two stops;
the phone had nothing. A leg in the phone chain now carries one round
button that asks the router for other ways and switches to the map, where a
bar takes the stage bar's place: the ways as chips with their colour, time,
distance and what sets them apart, and a confirm button. On glass there is
no hover, so a tap on a chip or on a line only previews that road; confirm
writes it. The button shows only where the desk offers it (a routed leg
between chosen stops), for someone who may edit the day, and not offline,
since vias are written online only.

The picker could not have stayed open on a phone at all: the planner closed
it whenever road trip mode was off, and that mode is off on a phone by
design, so the next render shut it. The close now follows where the picker
can be seen (road trip mode on the desk, the road trip tab on a phone), and
useRouteAlternatives returns a memoised object.

The rules both shells need are shared: legReroutable in the row model,
alternativesPhase and alternativeSubline next to the overlays, and the desk
bar and sidebar read them too with no visible change. Both map engines take
an optional fitPadding, so the phone frames the leg between the day chips
and the bar instead of under them.
2026-09-17 00:10:51 +00:00
Maurice 9b415c8ab9 feat(maps): the phone trip map keeps its credit in the bottom right corner
The credit button sat in the middle of the round controls band, beside the
locate button. On the trip map it now has the bottom right corner to
itself, a gap above the dock (or above the stage bar on the road trip
tab), and the band floats one credit row higher, so the switcher, the
compass and the locate button stay on one line.

MMapArea marks its layer as a credit corner and defines the floor the map
ends at; mobile.css places both GL engines' compact control there, keeps
an opened GL credit's button in the corner, moves the Mapbox wordmark into
the same row instead of under the compass, and unfolds the Leaflet credit
to the left of its button. MapAttributionToggle reads the same two
variables with today's values as the fallback, so the other phone maps and
the desktop keep their placement.
2026-09-16 23:21:40 +00:00
Maurice bf0ec85372 feat(roadtrip): badges and a place picture on the phone stage bars, a compact head card
The stage bar over the road trip map joined its figures with middle dots
and led with a bare day colour dot. It now leads with a picture of the
place it names (PlaceAvatar, falling back to the category icon), ringed in
the day colour while day colours are on, and shows the stop count, the
arrival and the distance as badges. The header pill over the tab does the
same with the day and the distance, and leaves out a distance nothing has
measured yet instead of printing 0 m. Both bars carry an accessible name
built from their parts, because pills side by side lose the spaces between
them when a screen reader reads them out.

The badges are a new non interactive MBadge, a span, since every place it
sits in is itself a button. The value rules shared by both bars live in
stageBadges.ts.

The head card at the top of the chain is more compact: the drive and the
stop count ride on the date line as badges, the two clocks are a size
smaller and the separate stats band is gone. The day colour dot is gone
from it: on the list half the line it keys lies hidden under the chain, so
it read as an unexplained status light.
2026-09-16 23:21:40 +00:00
Maurice 9de18634fe fix(roadtrip): a place tapped on the phone stage map opens its stop
The bar over the road trip map names a place, but a tap on it switched to
the whole day's chain and the place never opened. It now opens that stop's
sheet, the one its row in the chain opens. Its name, clock and tap come off
one row (stageEnd), so the bar no longer pairs the destination with the
automatic day end's clock (Kyoto Station 22:00 while the head card said
12:40). The header's list switch stays the way back to the chain.

A pin on the stage went through the plan tab's marker click, which moves
the place selection and brought up the plan tab's place card. On the road
trip tab a pin now opens its stop on the stage card, or the first routed
visit in the all days view, and only a place no routed day stops at still
gets the place card. The handler keeps one identity for the life of the
map. "Show on map" in the stop sheet and the up next card move the camera
through focusRoadtripPoint instead of the selection, picking the stop's
card first when it is not on screen. A pending focus now lets go once the
day moves, so later stages frame their own drive again.

Stage pins come from the trip store instead of the plan tab's map list.
That list carries the plan tab's day declutter and place filters, so after
the all days switch and back most pins of the next stage were missing, and
the unplanned filter or hidden service stops removed stops the chain draws.
2026-09-16 22:49:44 +00:00
Maurice 5b60dda150 feat(roadtrip): edit a stop's place from its phone sheet, title centred on the disc
The stop sheet had no way into the place, so a fixed time could not be set
from the road. A pencil in the header now hands over to the place editor
the plan tab uses, with this visit in context, so Start (the time the chain
anchors on) can be set or cleared there. It shows with place_edit, once the
place row resolves from the whole trip store and the visit carries a server
id. No second writer for the time is added.

Opening the editor on a booked night or a service stop the day lists hide
used to miss the visit, prefill Start from the pool place and write that
over the pin on a plain save, dropping the note too. MPlaceEditSheet now
resolves the visit from the stored assignments.

The header row is centred, so a one line name no longer hangs from the top
of the 34px disc.
2026-09-16 22:23:01 +00:00
Maurice bf03d91345 feat(roadtrip): the phone dry band gets the desk's mascot and reserve lamp
"Tank runs out here" on the phone was a pink card with a black pill. It now
has the desktop band's design sized for a thumb: the mascot on its
skateboard and not enjoying it, a small uppercase warning, and the reserve
lamp as a 44px button. While a search runs or its offers show, the lamp
steps aside for a close (nothing in the band could clear the offer pins
before), and an empty answer leaves a retry.

Which control shows, which offers and which empty sentence is decided in
refuelBandState in refuelSuggestion.ts, next to the fuel and battery words.
The desktop RefuelBand reads the same helper now, with no visual change, so
the two shells hold markup only.
2026-09-16 22:23:01 +00:00
Maurice 79450a5380 fix(roadtrip): head the phone stage with its start, not the first stop's departure
The head card read LEAVE 11:30 over a chain whose first stop is pinned at
10:00 with an hour and a half there. The figure was the departure from the
first stop, a clock no row prints, while ARRIVE was the last stop's arrival.
So the number was the wrong quantity and "Leave" the wrong word for it.

Both figures now come off the arrival column the rows print: START is the
first timed arrival (or the resume point on a morning after an automatic
night), ARRIVE is the last one. The rule lives in stageClocks in the shared
row model and reaches the tab as rt.clocks. The label is a new key,
mobileTrip.rtStart, because roadtrip.stay.leave keeps meaning a stop's own
departure in the stop and stay sheets. Both clocks carry dir=ltr like the
rows.
2026-09-16 22:23:01 +00:00
Maurice bd347f553b fix(maps): the satellite switch answers on the phone, and the compass stops hiding under it
Two faults behind one round button on the phone map.

The base layer switcher that both engines draw in the bottom left corner
sat on top of the GL compass, which started 8px to its left and ran on
under the frosted shell, so it read as a second button showing through.
The compass now stands beside the switcher, placed off the switcher's own
exported inset and size so the two cannot drift apart again.

Satellite never drew on GL because applySatellite gave up whenever
isStyleLoaded() was false and only retried on styledata. That flag is false
while any source has tiles or a setData in flight, which on a phone is the
usual state, and styledata does not fire when those finish. On the road
trip stage every tap met a busy style, because the settings write
re-rendered the shell and handed the map fresh stage arrays. The guard is
gone (the pass only runs after load, and addSource/addLayer only refuse a
style document that is not in yet), an existing layer just flips its
visibility, and a source left without its layer still gets the layer.

The stage data in MMapArea is memoised, so a store write no longer refits
the camera or sets the route, access and place sources again.
2026-09-16 22:22:46 +00:00
Maurice aaff8f9db4 feat(roadtrip): bring the road trip addon to the phone
A road trip tab on the mobile trip screen: the stage as a chain, the same
map underneath it, the sheets a passenger needs, and a search along the
route.

The addon was desktop only because road trip MODE is a data switch rather
than a view switch. It rewrites `assignments` and `places` for the whole
planner hook, and every mobile sheet is permanently mounted and reads those
same two lists, so turning it on there brought back documented regressions.
The mode flag is therefore untouched. A second flag, `roadtripFeedActive`,
feeds only the routing round and does it off the STORED assignments, so the
phone gets the drive without what the mode drags along with it.

The map is one instance shared by the plan tab and the road trip tab,
hoisted into a single JSX expression so React keeps it in the same position
and the WebGL context is never torn down on a tab switch.

The corridor search is scoped to what a phone can afford: a window of the
day rather than all of it, a smaller tile ceiling, a capped retry pass and a
deadline. The standard search is three requests where the desk makes
fifteen. The categories are deliberately not narrowed, because they all ride
one query per box, so a second one costs nothing.

Two gaps found while wiring it up are closed here as well. The fuel search
drew its offers nowhere on the phone, so the button did nothing visible;
they are in the dry band now, with the detour, the spare range and an accept
button. And the map credit opened as a two line ribbon across a screen the
map fills: both GL engines now start it collapsed, and Leaflet, which has no
collapsed mode of its own, is given one.
2026-09-16 21:55:18 +02:00
Konstantinos Thermos 5f57c63426 fix(journey): make the source_assignment_id migration replay-safe
An unreleased migration has to survive being replayed on a schema where it
already applied — installs skip releases, and a test rewinds a fully migrated
database to the last released version to prove it. The bare ADD COLUMN threw
"duplicate column name" on that replay, which took every migration test down
with it.

Guard the ALTER on pragma_table_info, and add `source_assignment_id IS NULL`
to the backfill so a replay cannot re-resolve a row that has since followed
its stop to another day.

The trips e2e harness hand-builds a minimal journey_entries table rather than
running migrations, so it needs the column spelled out too.
2026-09-16 16:32:02 +03:00
Konstantinos Thermos bd44197974 Merge pull request #2330 from liketrek/feat/pdf-trip-route-map
Feat/pdf trip route map
2026-09-16 16:10:31 +03:00
Konstantinos Thermos 37163e9a41 fix(places): render markdown in the mobile place description
The mobile shell printed place descriptions and notes as plain text while
the desktop rows and inspector run the same field through react-markdown,
so a description written with the formatting bar read as `# headings` and
`**asterisks**` on the phone.

Put the renderer in one shared component instead of copying it next to
each mobile twin, with a clamped variant for list rows that keeps them one
line high the way the desktop sidebar does. A clamped caption sits inside
the tappable row, so its links keep their text and lose the anchor.

Fixes #2337
2026-09-16 15:42:07 +03:00
Maurice 61a8a2765b fix(geo): read the Nominatim override the way the schema validates it
Follow-up to #2379.

NOMINATIM_URL arrives from a compose file or a ConfigMap with whatever
whitespace was typed around it. The schema validates the trimmed string and
calls a blank one unset, the derivation did neither, so " " passed startup as
"use the default" and then became the base URL that cannot be fetched, and a
padded value lost every geocoding call the same way. It trims now, which is
also what the places client next door does with its own override.

The chart could not pass the variable at all: the ConfigMap is an allowlist and
an unknown key is dropped without a word, which is exactly the locked-down
cluster the override exists for.

The docs said what to set and not what it costs. They now say that it replaces
the public service with no fallback, that the endpoint has to speak the
Nominatim API, and that the one-per-second spacing stays either way, so nobody
stands up their own instance expecting it to run faster.

Test IDs follow the file again.
2026-09-16 11:08:07 +02:00
Luffy c0195557d4 feat(geo): support overriding Nominatim endpoint via env 2026-09-16 14:30:31 +08:00
Maurice 06610bd97e Merge pull request #2377 from liketrek/feat/release-notes-430
feat(notices): bring the release notes back on every update
2026-09-15 23:51:17 +02:00
Maurice 05ee3d50e4 feat(notices): bring the release notes back on every update
The release modal takes the 4.3.0 design: the release and its headline cards
on the left, the note and the support links on the right, at the design's own
size. The cards carry small drawings of the features instead of an icon.

The 4.0.0 entry is retired. In its place sits one entry, release-notes, that
every big release rewrites and keeps. It recurs per version with no upper
bound, so every user sees it once on each update, whatever they did with it
before, and a reload of the same version keeps it closed. An integration test
holds that promise through the real entry and the real dismiss route.

All 23 locales carry the new copy and drop the 4.0.0 strings.
2026-09-15 23:50:15 +02:00
Maurice e2bdd2dd08 Merge pull request #2362 from liketrek/dependabot/npm_and_yarn/js-yaml-4.3.2
build(deps): bump js-yaml from 4.3.0 to 4.3.2
2026-09-15 23:11:57 +02:00
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 a9cd47caeb build(deps): bump js-yaml from 4.3.0 to 4.3.2
Only the js-yaml entry changes. The Dependabot commit was built on main and rewrote the lockfile with another npm, which dropped the peer flag from 23 rollup entries and carried two wiki commits that dev does not have yet.
2026-09-15 22:49: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 f0a4e4c74d Merge pull request #2376 from liketrek/fix/atlas-visit-dates
fix(atlas): date a country's first and last trip from the trips that count
2026-09-15 22:05:34 +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
Konstantinos Thermos 48cd46f818 feat(pdf): put the trip's route map in the export 2026-09-15 15:58:36 +03:00