Commit Graph

3055 Commits

Author SHA1 Message Date
Maurice 8e4874841f refactor(client): the fill-up band moves to RoadtripSidebarRefuelBand
RefuelBand leaves RoadtripSidebar.tsx for its own module and is
imported back by name; the imports only it used go with it. The code
moves as it was, with the dashes in its comments rewritten as colons
and parentheses. DriveBand's note, which the band used to separate from
its function, now sits directly above DriveBand. Nothing renders
differently.

New tests render the band on its own: idle, searching, offers with and
without the right to add one, each empty answer, and the battery words
for an electric car, which no existing test reached.
2026-10-08 05:02:47 +00:00
Maurice 97a93bb719 refactor(client): the stop badges and the arrival clock move to RoadtripSidebarBadges
OffRoadBadge, FillBadge, NightCheckIn, StayBadge, DriveFindingBadge,
DayCarry, Arrival and LateBadge leave RoadtripSidebar.tsx for their own
module and are imported back by name. The code moves as it was. The doc
blocks that sat stranded above other functions now stand above the
badge they describe, StayBadge's two notes as one, and the dashes in
the moved comments become colons and commas. Nothing renders
differently.

New tests render each badge on its own, including the walk from the
road and the range and unknown arms of the drive finding, which the
rail never reaches.
2026-10-08 05:02:47 +00:00
Maurice 7bd351f9b7 refactor(client): the rail's shared styles move to RoadtripSidebar.constants
RAIL_GRID, RAIL_DASH, DISC, STAT_LABEL and DAY_BADGE leave
RoadtripSidebar.tsx for a plain module beside it, so the parts the
sidebar is split into next can share them without importing from the
sidebar itself. Values and comments move as they were, except that the
dash in the DISC note becomes a colon; the two style objects are typed
as CSSProperties from a type import. Nothing renders differently.
2026-10-08 05:02:47 +00:00
Maurice b8c1fbf192 refactor(client): the plan map and its floating controls move out of the planner page
The plan tab's MapView with the route alternatives bar, the overview and
trail stack, the POI and compass cluster, the phone compass and the phone
POI portal move to pages/tripPlanner/PlanMapStage.tsx. It returns the same
children as a fragment in the same place, so the DOM and the portal order
stay as they were. Its props are picked from the planner hook's own types.

The four physical sides that moved are map geometry and carry an
rtl-lint-disable marker on their line, so the page's rtl baseline drops
from 17 to 13. Two moved comments lose a dash.
2026-10-08 05:02:47 +00:00
Maurice 7d8dcb02c8 refactor(client): the lists tab moves out of the planner page
ListsContainer, the packing and to-do subtabs of the lists tab, moves
unchanged to pages/tripPlanner/ListsContainer.tsx, taking along the
imports only it used. The page renders it exactly as before.
2026-10-08 05:02:47 +00:00
Maurice 3b9e5606ce refactor(client): the planner's lazy panels and their boundary get modules of their own
The lazyWithRetry declarations of the planner page move to
pages/tripPlanner/plannerLazy.ts, a plain module so Fast Refresh keeps
treating the page as a component boundary, and LazyPanel moves to
pages/tripPlanner/LazyPanel.tsx. The page imports both. Comments travel
with their code; two of them lose a dash. Nothing renders or loads
differently.
2026-10-08 05:02:47 +00:00
Maurice 5189d7fcca ci(client): source files may not grow past their size baseline
lint:size counts the lines of every source file under client/src, tests
aside. A file may hold 1000 lines; the 24 files that are longer today are
listed in scripts/size-baseline.json with their current length and fail
the check when they grow. --update lowers an entry once a file shrinks
and never raises one, so a long file is split by concern instead of
growing. A baseline that cannot be read or parsed stops the check with
its reason, and --update leaves it as it is. It runs in the client job
next to the reading direction check.
2026-10-08 05:02:47 +00:00
Maurice bee500880e fix(i18n): every language says one file and five files its own way
Finishes the plural work the previous wip commit started. t() picks a
count string's form by the language's own Intl.PluralRules, and every
locale now spells out the forms its language reaches with whole
numbers, about 160 count strings in all: one, few and many in Russian,
Ukrainian and Polish, one and few in Czech and Slovak, five forms in
Arabic, one in the others, none in Japanese, Korean, Chinese, Thai,
Vietnamese and Indonesian. Each language was translated and then
reviewed on its own. English no longer reads "1 files", "1 updates
available" or "photo(s)".

Count labels shown next to a number now take that number too: the
atlas and admin statistics, the trip cards, journey and shared trip
pills, packing templates, receipts, storage usage, the Studio export
note. A heading that shared its key with a counted label keeps a key of
its own (the boarding pass heading, the Bookings tab), so languages that
put a noun after a number in the singular keep a plural heading. The
plugin hosts chip passed its count through .replace() after t() and
never got a form; three toasts sent the count as text. Mail and push
texts for shared photos and suppressed storage failures pick their form
in code through pluralForm.

A whole number sent as text counts too, which is how in-app
notification params arrive, and the plural rule follows the language of
the strings on screen while another one still loads.

The parity check knows which forms each language needs and refuses
missing and impossible ones. It also reads keys that contain a colon
now: it skipped them, which hid 58 missing plugin permission texts in
Catalan and 35 in Vietnamese, both translated. The client parity test
covers every language of the registry instead of a hand-kept list of 19,
and the placeholder spec lets a form spell its number out only where
its category is that one number. Brazilian Portuguese error screens
were in Breton, and an Arabic dashboard label held Hebrew text.
2026-10-08 00:01:59 +00:00
Maurice 55697b75c1 test(db): the legacy step map knows the tour type migration
Migration20200101042300_every_tour_type_the_contract_names runs after
the legacy steps like the other post-legacy migrations, but was missing
from the test's list of migrations without a step, so LEGACYMAP-001
failed.
2026-10-08 00:01:43 +00:00
Maurice 960520c6d9 test(e2e): the iPad case checks what broke #1432, the guides read today's texts
The iPad case could no longer fail on its own regression: a draggable
row behind the long-press bridge is what tablets get by design since
#1616, and nothing in the case swiped. What turned swipes into drags in
#1432 was the drag-drop-touch polyfill, so the case now asserts that a
tablet never loads it.

Two help-media guides waited for "1 files" and "2 flight(s) imported",
which now read "1 file" and "2 flights imported".
2026-10-08 00:01:43 +00:00
Maurice d4eccb39a3 fix(client): three more places follow the reading direction in Arabic
On a phone in Arabic every system notice, the release notes included,
opened as an empty sheet: its three-slot strip is shifted to show the
middle slot, and in a right-to-left row that shift moved all three out
of view. The shifts point the other way in a right-to-left page now,
which the swipe handling already assumed.

The budget's total amount kept its parts in order but sat on the
opposite edge from its label, because the whole row was made left to
right. Only the amount itself is now. The phone chat's bubble tails
were still physical corners while the desktop ones follow the row; both
use logical corners now.

lint:rtl takes comments from the TypeScript parser instead of scanning
lines, so a "/*" inside a string (accept="image/*") no longer hides the
lines after it.
2026-10-08 00:01:43 +00:00
Maurice 0126ef3401 fix(client): the version notice asks about a change during the session only
Three gaps in the notice from the previous commit, found in review.

A page served from the network and claimed later by its own build's
worker counted as already taken over, so Reload went to that worker,
got the old build again and spent the session's reload: no notice and
no new build until the next start. Only a worker that replaced the one
that served the page skips the handover now, as at launch.

A reload that came before the new worker could take over (a slow
precache, a failed update, an install gone redundant) also spent that
guard, so the launch behind it could no longer finish the handover with
its own reload. That reload leaves the guard alone now.

An install whose server never reports the bundle's version (a hand-set
APP_VERSION) showed the notice in every session, and Reload could not
clear it. The store keeps the first version a page hears of, from the
app config at launch, and the notice only asks when a later report
differs: a different build at launch is the launch handover's to fix.

The server's wire-envelope test pins the welcome frame's fields and now
expects the version beside the socket id. The wiki's update page says
what an open tab shows after an update.
2026-10-08 00:01:33 +00:00
Maurice 254a214b7d fix(shared): the tour metrics pass the strict shared typecheck
computeTourMetrics indexed points[i - 1] and points[i], which the shared
package's noUncheckedIndexedAccess reads as possibly undefined, so the
shared typecheck in CI failed since the metrics moved there. It walks the
points pairwise from the first one now; the numbers are the same.
2026-10-07 23:09:42 +00:00
Maurice 8dca372c47 ci: run the browser flows and the map bundle check on every change
Both only ran on a developer's machine, and both had stopped telling the
truth. The end-to-end flows now run in CI in Chromium and in WebKit (the
iPad and iPhone cases need the engine every iOS browser uses), with the
browsers cached on the lockfile and the traces uploaded when a flow
fails. A second job builds the client and fails when one chunk carries
both map engines.

Making them pass meant fixing what had rotted. The setup closed every
first-run notice but the release notes, whose X is called Close, so the
notes covered the dashboard and every flow after login timed out on a
click. The iPad case still expected undraggable place rows; tablets drag
on purpose since #1616, behind the long-press bridge, so it now checks
what #1432 is about, a swipe that scrolls and raises no drop overlay,
with locators for the scroller and the Plan tab that match today's
markup. The iPhone case read the body scroll lock through
window.scrollBy, which moves an overflow: hidden viewport in every
engine; it now reads the lock where it takes effect and checks the page
keeps its place.
2026-10-07 22:49:36 +00:00
Maurice a094f79efa feat(client): offer the new version when a release lands during a session
A tab kept open across a deploy ran the previous build until its next
start. The server now names its version in the realtime welcome frame,
which every open client reads again when it reconnects after the
restart a deploy brings. When that version is not the build the page
runs, a notice offers to reload. The reload goes through the same
service worker handover a launch uses: it waits for the new worker to
take over, or for a short timeout, and records the version only when
the takeover happened.

The notice reads the error boundary's texts for the same situation, so
every language has it already. It is one component for both shells,
sits above the offline pill, stays away offline and after a reload for
that version that did not bring it, and can be closed for the session.
2026-10-07 22:40:12 +00:00
Maurice ba70ad28b8 fix(client): spacing and alignment follow the reading direction
Arabic runs the app right to left, but margins, paddings, insets, text
alignment, borders and corner radii were written for the left side, so
icons sat against the wrong edge of their labels, indents and badges
pointed the wrong way and amount columns aligned left. They now use the
logical forms (ms-, ps-, start-, text-start, marginInlineStart,
insetInlineEnd and their CSS equivalents), which are the very same sides
in a left to right language. Geometry stays physical on purpose: maps,
positions measured from the pointer or a bounding box, time axes,
sliders, transforms and print output.

Amounts drawn as several spans keep their order in a right to left row,
the time picker keeps hours before minutes, and two switches whose thumb
slid out of its track in Arabic now move the right way.

lint:rtl counts what is still physical per file against a baseline that
may only shrink (1512 uses in 370 files before, 319 in 113 now), and CI
runs it, so new code reaches for the logical forms.
2026-10-07 22:33:58 +00:00
Maurice de128cc4ae wip(i18n): plural forms through Intl.PluralRules
Unfinished, continued in the next session: t() resolves key.one/.few/...
by the language's own plural rule, the parity check knows which forms a
language needs, 16 singular/plural key pairs are merged in every locale
and every count === 1 branch in the client uses the plural form. Still
open: the forms each locale needs for the remaining count strings, the
two new labels in every locale, and 35 tests that expect the old English
wording.
2026-10-07 22:57:51 +02:00
Maurice 588e483205 fix(calendar): VTIMEZONE spells out the offset changes of its span
Each zone's VTIMEZONE carried one fixed offset, so a client that reads it
literally showed every event after a change to or from summer time an hour
off. It now lists every offset change in the calendar years its events
fall in, each as its own observance after the offset in force before the
first one. A zone without changes, or a span with no readable date, keeps
the single fixed offset.
2026-10-07 22:56:35 +02:00
Maurice ab343b0a7b fix(client): a phone turned sideways stays in the phone layout
The phone shell was chosen by width alone, and most phones are wider than
767px in landscape, so turning one swapped the whole route tree for the
desktop planner and an open sheet, its input and the map position were
lost. The phone query now also covers a touch screen under 500px tall,
which no tablet is. It lives in one place: the shell, the planner and todo
hooks, the warnings banner, dialog focus and the stylesheets read it, and
six components that measured the window once on mount now follow it.
2026-10-07 22:06:40 +02:00
Maurice e5ceba00f3 fix(tours): wait for the public Valhalla's turn before every request
The public instance allows about one request a second per client, and the
Tours elevation questions went out right behind the route request with no
pause, so they could be refused with a 429 and leave the tour without the
elevation it needs to be saved. Every request to that host now waits for
its turn, across callers; an operator's own Valhalla is not paced.
2026-10-07 21:39:57 +02:00
Maurice e0efa0314a feat(tours): tours read and write through the offline core
A tourRepo replaces the direct toursApi calls: reads fall back to a new
Dexie tours table, and a tour saved or edited offline is written there and
queued like a place, under one temporary id for the place and its facet.
The queue moves both to the real id on replay and drops both when the
server refuses the create. Trip sync caches each trip's tours while the
addon is on, so a tour no longer reads as a plain place offline.

The tour metrics move to shared so the offline copy shows the server's
numbers, and useTourPlaceIds keeps one Set per change as intended.
2026-10-07 21:30:45 +02:00
Maurice f7edd849ab fix(tours): one tour type catalogue, and no column nothing writes
tour_types now holds every key the shared contract accepts, all but hike
disabled, and create and edit turn a disabled type away with a 400 instead
of failing on the foreign key. tours.tour_group_id is dropped: nothing ever
wrote or read it. difficulty and wanderer_ref stay for the Wanderer import.
2026-10-07 20:54:10 +02:00
Maurice f64f9e58de fix(client): give the context menu a menu role, wrap the tour filter callback 2026-10-07 20:35:15 +02:00
Maurice bd8e1a287b test(oauth): the module now provides the token retention job 2026-10-07 17:55:25 +02:00
Maurice e6e270610c fix(places): type the tour mark on the single-place row 2026-10-07 17:37:26 +02:00
Maurice 57df945318 style: plain punctuation in the new comments and test names 2026-10-07 17:12:26 +02:00
Maurice 40bce5554d docs: the key check, the restore guard, the new probes, failed offline changes and reminders
Updates the wiki for the changes in this branch: TREK refuses an
ENCRYPTION_KEY that disagrees with the key file, the rotation script
covers the AI Parsing key and moves the file along, a restore refuses a
newer backup and keeps the replaced database, the chart's startup and
readiness probes, PORT and extraEnv, retrying or discarding failed
offline changes, trip reminders catching up, and why a Portainer
recreate used to keep reporting the old version.
2026-10-07 17:11:56 +02:00
Maurice 6b037e0fa0 refactor(tours): measure distances with the shared haversine
The Tours metrics carried their own copy of the great-circle formula
next to nest/common/geo.ts, which exists so there is one.
2026-10-07 17:10:59 +02:00
Maurice e4ed4bfc7b fix(mobile): the booking, transport and place sheets ask before dropping what was typed
On the phone a flick of the sheet's top edge, a tap on the scrim or
Escape closed the booking, transport and place forms and threw away
whatever had been typed, without a word; the desktop dialogs have asked
since #2253. The question moves into a shared hook that DialogShell and
MSheet both use, and the three sheets pass their form state to it.
2026-10-07 17:10:14 +02:00
Maurice c3c21fb0b4 fix(client): five loads that catch their own error recover from a deploy too
The PDF export on desktop and phone, the language switch, the holiday
regions and the PDF's map image load a chunk on demand and catch the
failure themselves. After a deploy the old page asks for chunk names the
new build no longer has, so they showed their own error on every retry
(or, for the language, silently stayed in the old one) instead of
reloading onto the new build the way an uncaught chunk error does.
importChunk reloads once for a missing chunk and still hands the caller
its error.
2026-10-07 17:05:36 +02:00
Maurice 0a1a5092f7 fix(ui): the context menu answers the keyboard and closes when the page moves
The shared context menu closed only on a click elsewhere: Escape did
nothing, the arrow keys did nothing, and a scroll left the fixed menu
hanging over content that had moved on. It now takes focus on its first
item, moves with the arrow keys, and closes on Escape, scroll and
resize. rules-of-hooks is an error again; its one hit was a test helper
named like a hook.
2026-10-07 17:03:47 +02:00
Maurice 094a46a3cf fix(vacay): stop swallowing a failed delete as a holiday API error
One try around the holiday fetch and the deletes that follow it caught
both, so a database error while removing vacation days that fall on a
public holiday was logged as nothing and left the days standing. Only
the fetch is tolerated now. Also drops two files long gone from the
Sonar coverage exclusions.
2026-10-07 16:59:52 +02:00
Maurice 48f9e7d132 perf(db): index every foreign-key column
86 foreign-key columns had no index. SQLite does not add one by itself,
so every cascading delete of a trip or a user scanned each such child
table in full, inside the transaction, while every other request waited
on the one connection. One migration adds them all, and the entities are
regenerated to match.
2026-10-07 16:56:58 +02:00
Maurice 1a4df59bf3 ci: give every job a timeout
No job had timeout-minutes, so a hung step held a runner for GitHub's
six-hour default. Test and housekeeping jobs stop after 45 minutes, the
image builds after two hours.
2026-10-07 16:52:26 +02:00
Maurice c60277c4d2 fix(tours): a tour another member creates is a tour on the phone too
The list read marked tour places with tour_place_id, but the single-place
read behind place:created and place:updated did not, and the phone's
places browser kept a second tour list of its own that no realtime event
updated. A tour another member made, or one imported from GPX, showed as
an ordinary place on the phone until the screen remounted, and every
tour did offline. The single read carries the mark now, the browser asks
the planner, and the tour id set is memoised instead of rebuilt on every
render.
2026-10-07 16:52:05 +02:00
Maurice 72fbbd55d4 fix: a trashed note file leaves its note, quieter MCP sweep, bounded compose logs
A note's attachment moved to the trash from the file manager kept
showing on the note; the hydration read now skips it, as the chat one
already did. The MCP session sweep logged a line every minute while any
session was open; it logs only when it cleaned something. The compose
file caps the container log, and three test mocks that pointed at
modules long gone are removed.
2026-10-07 16:47:53 +02:00
Maurice 76df44d8de fix(oauth): purge tokens whose refresh token expired a month ago
Every refresh writes a new oauth_tokens row and nothing ever deleted the
old ones, so an MCP client refreshing hourly grew the table, and every
backup with it, without end. A nightly job now deletes tokens whose
refresh token expired more than 30 days ago, leaves first, and keeps any
row a live chain still points at for replay detection. Also types the
backup's per-entry store flag.
2026-10-07 16:45:21 +02:00
Maurice a869269591 fix(deploy): follow PORT in every probe, wait for a slow boot, and probe the database
Changing PORT left the Dockerfile and compose healthchecks, the chart's
containerPort, Service target and probes on 3000, so the container went
unhealthy and Kubernetes restarted it in a loop. All of them follow PORT
now. The chart gains a startup probe (ten minutes for a first boot, a
long migration or a restore at boot, which liveness used to kill after
about 105 s), a readiness probe on the new /api/health/ready that asks
the database, extraEnv and extraEnvFrom for the settings the chart does
not list, slots for the four secrets it had none for, and the
DEFAULT_LANGUAGE and MCP_MAX_SESSION_PER_USER values it documented but
never set. The Dockerfile no longer hides a failed npm ci behind a ';',
pins gosu, and the language lists name all 27 locales.
2026-10-07 16:40:40 +02:00
Maurice 8c3fad5e82 fix(auth): reset-admin.js ends the account's sessions and matches its email in any case
Resetting a locked-out or compromised admin left every session on that
account valid, because the script did not move password_version on the
way every password change in the app does. It also looked the address
up case-sensitively and created a second account when the stored one
had capitals.
2026-10-07 16:36:39 +02:00
Maurice 8923918eb4 fix(i18n): send Catalan and Vietnamese notification mails in their language
Both locales had their notification texts, but the index never imported
them, so their users got English mails and the parity gate passed
because the files existed. The locale map is now typed against the
supported languages, so the next one left out fails to compile.
2026-10-07 16:36:01 +02:00
Maurice 8f02337d45 fix(i18n): hand Intl and the document the real language tag for br and gr
TREK's codes 'br' (Brazilian Portuguese) and 'gr' (Greek) went straight
into <html lang> and into ten Intl calls. 'br' is Breton, so Brazilian
users got Breton month names and screen readers announced the page as
Breton; 'gr' is no language at all, so Greek users got the default
locale. getIntlLanguage maps both, and every one of those sites goes
through it.
2026-10-07 16:34:59 +02:00
Maurice 461a63fc86 fix(auth): send the queued changes before a logout deletes the offline database
Logging out deleted this user's offline database straight away, so a
change made offline and not yet sent was lost without a word. With a
connection, the logout now flushes the queue first, for at most five
seconds.
2026-10-07 16:32:57 +02:00
Maurice 38656eeffd fix(offline): let the cache drop what was deleted elsewhere
The trip sync and every repo list only ever upserted, so a place a
collaborator deleted while this device was offline came back on the
next offline read, and a trip the user had been removed from stayed on
the device. A sync and a full list now replace the trip's rows, keeping
only the ones created offline that have not synced yet, and a cached
trip the server no longer lists is cleared.
2026-10-07 16:32:02 +02:00
Maurice f092e7a95f fix(offline): a write the server keeps failing on no longer blocks every other write
A queued change the server answered with a 5xx was put back at the head
of the line and the whole flush stopped, every 30 seconds, forever: one
bad payload held every later change of every trip. Now only its own
trip waits, with gaps doubling from 30 s, and after eight failures it is
parked as failed. A DELETE answered 404 counts as done instead of
showing as a failed sync. Settings > Offline offers to try parked
changes again or discard them.
2026-10-07 16:30:07 +02:00
Maurice 3f9b751ee3 fix(realtime): open no socket after a logout, and reconnect as soon as the network is back
A connect still waiting for its ws token when the user logged out went on
to open a socket authenticated as that user, which the next login on the
same browser then reused. And a socket in its reconnect backoff waited up
to 30 s after the network came back or the tab became visible. Both
events now reconnect right away.
2026-10-07 16:25:00 +02:00
Maurice f02253a44c fix(planner): drop search and day-panel answers that arrive too late
The desktop day panel stays mounted across a day switch, so a forecast
or the stays still loading for the previous day overwrote the new day's
once they arrived. The location search in the transport form showed
results for text the user had already changed, or refilled the list
after a pick. Both now drop an answer for a request that no longer
matches.
2026-10-07 16:23:47 +02:00
Maurice 205759e3cf fix(scheduling): do not start a cron tick while the last one still runs
CronJob ran a tick on schedule even when the previous one had not
finished, so an hourly auto-backup of a large library could overlap
itself, and a rejected tick only reached cron's own console line. Jobs
now wait for completion and log failures through the app logger. The
backup also stores photos, videos and PDFs instead of deflating
already-compressed files at level 9.
2026-10-07 16:21:16 +02:00
Maurice 7e8c9ac3a5 fix(server): give the last six outbound fetches a timeout, and lint for it
Overpass, the Wikipedia photo lookup (which holds one of five shared
photo slots), the plugin download and registry, and the two API key
checks had no signal, so a provider that took the connection and went
quiet held the request for undici's 300 s default. ESLint now refuses a
fetch() whose init carries no signal.
2026-10-07 16:19:55 +02:00
Maurice a8fc93dfde fix(notifications): take today from the instance's TZ and catch up missed trip reminders
The reminder crons run at 09:00 in the instance's TZ but computed today
in UTC, so east of Greenwich a trip reminder arrived a day late. A day
the job did not run also lost that day's trip reminders for good: the
match was 'starts exactly N days from today'. Trips now record the start
date a reminder went out for, and the job sends every reminder that is
due and not yet sent. The active-trip redirect and the upcoming
reservations use the instance's date and clock as well.
2026-10-07 16:16:26 +02:00
Maurice e0aedbb2c6 fix(trips): count trip dates in calendar days, the same on REST and MCP
Filling in a missing end date and keeping a trip's length when its start
moved both shifted a local-time Date, so a trip crossing a DST change
lost or gained a day. Both now use addIsoDays. The one-date-makes-a-week
default moves from the REST controller into TripsService.create, so the
MCP create_trip tool no longer stores a half-dated trip, and the trip and
its days are written in one transaction.
2026-10-07 16:11:41 +02:00