Commit Graph

1513 Commits

Author SHA1 Message Date
Maurice f190aa5a01 refactor(collab): format the moved collab model and drop its stale comments
The model left the format baseline when it moved into components/Collab,
so it is now Prettier formatted. Its header no longer points at the
deleted services layer, and the colour palette no longer cites a rule
that stopped applying once the file sat beside the desktop panel. The
chat data hook says why it still returns setters nobody writes.
2026-10-09 15:19:44 +02:00
Maurice c9c3cc6175 refactor(files): keep each view's id rules in the shared link toggle
The shared toggle had mixed the two views. Desktop again treats any empty
column as free and finds a link record by comparing ids as they come. The
phone link sheet again treats only a null column as free and compares the
numeric value of a record id.

Starring stays per view. It is a single filesApi call with different
error handling and refresh on each side, so a shared wrapper would add a
layer without removing any logic.
2026-10-09 15:19:44 +02:00
Maurice a809de1b7a refactor(collab): keep the phone notes tab loading without a trip id guard
The shared notes hook skipped loading and listening without a trip id,
which only the desktop panel did. That guard is now a desktop option, so
the phone tab loads and listens as before.
2026-10-09 15:19:44 +02:00
Maurice 1c88a42890 refactor(collab): keep the phone's own poll reconciliation in the shared hook
The shared polls hook had taken the desktop rules for both views. It now
keeps each view's logic as it was before the move. The phone again
replaces a poll with the one a live closed event carries, puts a vote
answer on the poll it voted on, and stops the countdown tick once a
deadline passed. The desktop panel again ticks until a poll is closed.
2026-10-09 15:19:44 +02:00
Maurice 74e1853c39 refactor(docsync): share the scope picker, lane switch and folder name between desktop and phone
useScopePicker holds the provider's containers, binding one, and making
and binding a new one under the suggested name; the desktop dialog keeps
its search filter. docSyncModel holds the direction a lane switch leads to
and the folder name made from the trip title, which the panel and the
phone sheet each carried a copy of.
2026-10-09 15:19:43 +02:00
Maurice dd7cfca0b2 refactor(collab): share the notes list and attachment upload between desktop and phone
useCollabNotesData holds the note list with its first load and the live
note events, and the attachment upload both shells run after saving a
note. Writing notes, their colours and categories stays in each shell,
since the desktop panel keeps empty categories and colour batches the
phone tab does not have. A failed load still empties the desktop list only.
2026-10-09 15:19:43 +02:00
Maurice 428b4e6006 refactor(collab): share the polls logic between desktop and phone
useCollabPolls holds the poll list with its live updates, the countdown
tick, and creating, voting, closing and deleting. The desktop panel reads
the vote totals, winner and closed state from collabModel like the phone
tab does and keeps its own countdown label. A failed load still empties the
desktop list only, and closing still takes the server poll on the phone
only.
2026-10-09 15:19:43 +02:00
Maurice 7298285a43 refactor(collab): share the chat logic between desktop and phone
collabModel moves from the phone tabs into components/Collab so both
shells read it, and the desktop chat drops its own copies of the time,
date separator and emoji only helpers for the model's.

useCollabChatData holds the messages, paging, live updates, the draft with
its images, and sending, deleting and reacting. The desktop hook keeps its
emoji picker, hover and delete animation, the phone tab its long press
popover; where they differed (older page errors, scrolling after a remote
delete, upload progress, the edit permission on send) the hook takes an
option.
2026-10-09 15:19:43 +02:00
Maurice 676de4c1d9 refactor(files): share the media lightbox logic between desktop and phone
useMediaLightbox holds the signed image address of the file on screen,
paging by button, arrow keys and swipe, and Escape to close. The desktop
lightbox keeps its own index and thumbnail strip, the phone lightbox stays
controlled by the files tab; both draw their own overlay over the hook.
2026-10-09 15:19:43 +02:00
Maurice 076ed897e7 refactor(files): share the file trash, paste and link rules between desktop and phone
useFileTrash holds the trashed files with their restore, permanent delete
and empty actions; the desktop trash view loads it on demand and the phone
sheet while it is open. fileActions holds the clipboard file picking and the
two tier link toggle (own column first, then link records) that the desktop
assign dialog and the phone link sheet both ran on their own.
2026-10-09 15:19:43 +02:00
Maurice 000c9dfa30 refactor(roadtrip): share the add stop draft between desktop and phone
The stop popup and the phone draft sheet take the kind and length a found stop opens with, the kind pick that brings its usual length along and the six length choices from useStopDraftForm and stopKinds. The desktop keeps its night booking and still starts from the draft it mounts with; the phone still seeds the answers for every draft it is handed.
2026-10-09 15:19:43 +02:00
Maurice 745cd5f65d refactor(roadtrip): share the stay length draft between desktop and phone
The stay dialog and the phone stay sheet take their presets, the five minute step, the full day limit, the draft and the arrival and departure preview from useStayDraft. Each still seeds the draft when it opens and saves it its own way; the phone keeps whole minutes when a step moves a stored fraction, the desktop does not.
2026-10-09 15:19:43 +02:00
Maurice 97d3512aa7 refactor(planner): share the trip export between desktop and phone
The export dialog and the phone export sheet keep their rows and run the PDF, the calendar file and the GPX download through useTripExport, which also reads the print settings and whether my plan is offered. The desktop still runs one export at a time and closes after each one; the phone still lets a calendar or GPX file run beside a PDF and stays open after the PDF.
2026-10-09 15:19:43 +02:00
Maurice a4e3e83d3f refactor(planner): share the transport detail day and segment labels between desktop and phone
The transport detail dialog and the phone transport sheet name the booking's day and each segment's own booking code through transportDetailModel, next to the files filter they already share. The rest of the two views reads the booking differently on purpose and stays as it is.
2026-10-09 15:19:42 +02:00
Maurice fc3cc1bbbe refactor(planner): share the booking form and its hotel stay between desktop and phone
The booking dialog and the phone booking sheet keep their markup and run on useReservationForm: how a saved booking or an import fills the form, the end before start check, the start date and time, the place and hotel picks, the stay range and the request body with the stay a hotel booking creates come from reservationFormModel. The desktop's check-in window, its link to a stop inside the form and its imported price stay its own, behind options where the code is shared.
2026-10-09 15:19:42 +02:00
Maurice 203e425e7e refactor(planner): share the transport booking form between desktop and phone
The transport dialog and the phone transport sheet keep their markup and run on useTransportForm: the route rows, how a saved booking or an import fills them, the day options and the request body come from transportEndpoints. Where the two shells differ today (unknown type fallback, stopover days, which rows are reset, train anchors, empty metadata, the link field) the shared code takes an option. The phone's upload helper moves to bookingFormModel next to the traveller checks both booking forms use.
2026-10-09 15:19:42 +02:00
Maurice 131d57b755 refactor(budget): share the costs ledger and settlement form between desktop and phone
costsModel moves from the phone tabs to components/Budget.
useCostsLedger holds the settlement, the totals, the filters, the CSV
export and the payments, and useSettlementForm holds the payment form.
CostsPanel and MCostsTab both run on them.

The old differences stay behind flags. The desktop also loads the
settlement on a trip change, leaves expenses nobody paid out of my share
(#2225) and writes each currency as stored in the CSV. The phone sheet
starts blank, refills when it opens and ignores a second save while one
is running.
2026-10-09 15:19:42 +02:00
Maurice 80c66d3f83 refactor(budget): share the expense editor between desktop and phone
useExpenseForm holds the payers, the splits, the ticket items and the
receipts of the expense editor in CostsPanel and MCostSheet.

Flags keep the old differences. Only the desktop sends the people named
on a ticket line as members (#1382). The phone ignores a second save
while one is running and stays busy after a successful save. The
rebalance effect uses useEffectEvent instead of an eslint-disable.
2026-10-09 15:19:42 +02:00
Maurice ad220d9608 refactor(packing): share the bag editor and the bag weight totals between desktop and phone
useBagCardEditor holds the editing of a bag's name and weight limit for
the desktop bag card and MBagsSheet, and bagLoadSummary computes the
weight totals for the bag sidebar, the bag modal and the phone sheet.
The desktop card follows the bag when it changes elsewhere, the phone
row resets a name it did not save.
2026-10-09 15:19:42 +02:00
Maurice 4d4ef17de8 refactor(packing): share the packing list logic between desktop and phone
usePackingItemActions, usePackingTemplates and usePackingImport take the
item, template and bulk import logic out of usePackingListPanel and
MPackingListTab, and the phone listsModel keeps only its priority
tokens.

When adding to a category the desktop looks for the placeholder in the
store and the phone in its rendered items. The desktop toasts on an
empty import, the phone runs one import at a time. The effects run in
the same order as before.
2026-10-09 15:19:41 +02:00
Maurice 6dc8699bd1 refactor(todo): share the to-do filters and the task form between desktop and phone
The to-do filters, counts, sort order and categories move from the phone
listsModel into todoListModel. useTaskForm holds the name, the fields,
the load and the save of a task for the desktop detail and new task
panes and for MTaskSheet. Each side keeps its own guard and error
wording.
2026-10-09 15:19:41 +02:00
Maurice 5a0e03929d refactor(lists): share the remembered lists sub-tab between desktop and phone
useListsSubTab remembers whether the packing or the to-do sub tab was
open, for ListsContainer and MTripShell. The desktop restores any stored
value, the phone restores todo and otherwise falls back to packing.
2026-10-09 15:19:41 +02:00
Maurice 71289ba43e refactor(places): share the place form and the maps search between desktop and phone
usePlaceForm and usePlaceSearch take the form state, the prefill, the
opening hours selection, the duplicate name check, the payload and the
maps search out of PlaceFormModal. MPlaceEditSheet, PlPlaceSearch and
PlCategoryPicker run on the same hooks, and PlTimeFields uses the shared
timeCollisions.

Where the two shells behaved differently the hooks take a dialog or a
sheet variant: the coordinate pair rule, abort and console handling,
when the billing session ends, whether a pick clears the suggestions,
the empty search cue, whether a map link clears the results, and how a
new category is trimmed and guarded. The open effect and the submit flow
stay per shell, because service stops, expense intents and delete arming
differ too much to share.
2026-10-09 15:19:41 +02:00
Maurice b2fd0ae5aa refactor(collections): share the add place to list logic between desktop and phone
useAddPlaceToCollection holds the maps search, the pick and the save
that AddPlaceToCollectionModal and MCollAddSheet each carried, with the
pure helpers in addPlaceModel.

The differences stay as variants. The desktop resets and stays open
after an add, sends links and typed coordinates and shows the error's
own text. The phone closes, takes coordinates only from the picked hit,
ignores a second search or save while one is running and shows the
fallback text.
2026-10-09 15:19:41 +02:00
Maurice bbe3da7722 refactor(places): share the places pool filters and selection between desktop and phone
usePlacesPool holds the category filter, the search, the multi select
and the bulk mark visited of the places pool, for usePlacesSidebar and
for MPlacesBrowser with its filter controls and sheet. When selected
places are deleted, the desktop leaves select mode once none of them is
left and the phone drops only the missing ids. The hook keeps both.
2026-10-09 15:19:41 +02:00
Maurice 4dc446424f refactor(places): share the GPX, KML and KMZ file import between desktop and phone
usePlacesFileImport holds the GPX, KML and KMZ import that
FileImportModal and ImpFileStep each carried. The desktop keeps one
toast and one undo label and counts only the summary's skips. The phone
keeps a toast per format, the importFiles undo label for a mix and the
top level skipped fallback.
2026-10-09 15:19:41 +02:00
Maurice f2b6a14e9f refactor(places): share the Google and Naver list import between desktop and phone
useListImport holds the Google and Naver shared list import that
usePlacesSidebar and ImpListStep each carried. The desktop closes the
dialog and clears the link before the undo step, the phone calls onDone
after it, and the hook keeps both orders.
2026-10-09 15:19:41 +02:00
Maurice 8a28f5e22f refactor(collections): share the bulk save to list logic between desktop and phone
useSaveTripPlacesToList holds the logic that saves a selection of trip
places to a list, which SaveTripPlacesToListModal and
MPlacesSaveToCollectionSheet each carried as a copy. The two copies did
the same thing, so the hook has no variants.
2026-10-09 15:19:40 +02:00
Maurice 1b51181930 refactor(planner): share the bookings and transports list filters between desktop and phone
The bookings model moves from the phone tabs into components/Planner/bookings, so
both shells read the same sections, traveller predicate and type colours. The
phone bookings and transports tabs had the same traveller filter and folded
sections twice; that state now lives once in useReservationListFilter, which only
those two tabs use. The desktop panel keeps its own filters in useBookingsView
because it remembers them for the session.

The status switch and the delete on a booking card stay per shell. The desktop
hook renders its ConfirmDialog and awaits the delete, the phone asks in an
MConfirmSheet and lets the delete run on its own; sharing them would change
when the question closes.
2026-10-09 15:19:40 +02:00
Maurice 9529b55bd7 refactor(planner): share the booking files filter between desktop and phone
filesFor moves out of the desktop booking parts into utils, and the phone
transport sheet now reads a booking's files through it as the desktop
transport detail and booking cards do.
2026-10-09 07:16:40 +00:00
Maurice d01d9df93a refactor(planner): share the day note draft and save between desktop and phone
The desktop note dialog and the phone note sheet fill their editor with
dayNoteDraft and save through writeDayNote, so both trim the title, store an
empty detail as null and default the icon the same way. Both count the detail
line against one shared limit.
2026-10-09 07:16:40 +00:00
Maurice 70c96be193 refactor(planner): share the stay editor rules between desktop and phone
The desktop hotel picker and the phone accommodation sheet take their day
options, range changes, checkout default, places on offer and request body
from stayFormModel, and both save a stay through writeStay.
2026-10-09 07:16:40 +00:00
Maurice f6d51ba68e refactor(planner): share the day detail logic between desktop and phone
The desktop day panel and the phone day sheet load the day's forecast with
useDayForecast, rename the day with useDayRename, and read the day's
bookings, the stay labels and the display temperature from dayDetailModel.
2026-10-09 07:16:40 +00:00
Maurice 38c7d76c5d refactor(planner): share the per leg travel mode between desktop and phone
The day plan's connector menu and the phone timeline's are built by
legModeMenuItems, and both save a leg's mode through useLegModeActions, which
reports a refused write and reloads the days the same way on both.
2026-10-09 07:16:40 +00:00
Maurice 20d7e62afc refactor(planner): share the day plan reorder writes between desktop and phone
The desktop day plan and the phone timeline derive the place order, the note
and transport positions and the per leg positions of a reordered day from
mergedOrder, and write the moved bookings back through the same helpers.
2026-10-09 07:16:40 +00:00
Maurice ae31b55441 refactor(planner): share the day route export and optimize slots between desktop and phone
dayRoute moves from the phone's lib into components/Planner. The desktop day
plan builds its Google Maps and CoMaps stops with dayExportStops, and both
optimizers put the reordered stops around the fixed ones with fillAroundLocked.
2026-10-09 07:16:40 +00:00
Maurice 9f3e1f8c83 refactor(weather): share the day weather anchor and icon map between desktop and phone
The day plan, the day panel, the phone timeline and the phone day sheet read
the day local weather anchor from dayWeatherAnchor, and every weather icon
comes from one map in weatherIcons.
2026-10-09 07:16:40 +00:00
Maurice 34127b99bb refactor(planner): share the place detail actions between desktop and phone
The place inspector and the phone place sheet now run on one module for
their participants, file uploads, image picks, ratings and place updates,
and both build the save to collection target with placeToSaveTarget.
2026-10-09 07:16:40 +00:00
Maurice 9c39a0fc58 test(offline): count the trip member the v2 case stores 2026-10-08 20:22:17 +02:00
Maurice 0dde01baa2 fix(oidc): sign in after SSO on a success flag, not the deprecated token
The token in the sign-in bodies was declared deprecated and "read by
nothing of ours", but the web client still decided an SSO sign-in by
reading data.token from GET /api/auth/oidc/exchange, an endpoint the
deprecation notice left out. Removing the field later, as documented,
would have broken SSO sign-in.

The exchange now answers { success: true, token } and the login hook
signs in when the response is OK and success is true, so it never reads
token. The field stays for API clients and is now on the deprecation
list in the API docs, the controller comments, server/CLAUDE.md and the
wiki (Security-Hardening, OIDC-SSO). Adding a field to the body breaks
no existing client.
2026-10-08 18:44:00 +02:00
Maurice 9aab9e7513 feat(auth): track sign-in sessions so a session can end before it expires
A session token was a signed JWT and nothing else. Logout only cleared
the cookie, a 30-day "remember me" token stayed valid wherever a copy of
it had ended up, and nobody could sign out one device short of changing
the password or rotating the JWT secret for everyone.

Every session token now carries an id (jti) naming a row in the new
user_sessions table (id, user_id, created_at, last_seen_at, expires_at,
revoked_at, user_agent cut to 256 characters; no address is kept). The
rows live behind UserSessionsRepository and the new sessions domain,
whose SessionsService is the one issuer: AuthService.generateToken and
the OIDC sign-in both go through it, and sliding renewal re-signs a
tracked token under the same id and moves its expiry. verifyJwtAndLoadUser
takes the repository and refuses a token whose session is revoked,
expired or gone, refreshing last_seen_at at most every five minutes.
A token without a jti was issued before this change; it passes until its
own expiry (or a password change), so the upgrade signs nobody out, and
renewal turns it into a tracked session.

Sessions end on logout (the one the request names, verified even when
expired), on a password change (all, the device gets a new one), a
password reset, an admin setting a password, reset-admin.js, an MFA
disable (all but the current one), an admin clearing a user's MFA, and
with the account. GET /api/auth/sessions lists the active sessions,
DELETE /api/auth/sessions/:id ends one and POST
/api/auth/sessions/revoke-others ends all but the current, with their
contracts in @trek/shared and two new audit actions. There is no UI for
them yet. MCP exposes no account or session tools, so there is no
parallel tool to change; an MCP call made with a session JWT as bearer
goes through the same check.

The token in the login, register, demo-login, MFA and passkey bodies
stays for API clients but is marked deprecated in the API docs and in
the client type, which never read it.

A nightly session-purge job on CronRegistrarService deletes the expired
and revoked rows. The migration is 042500. The sessions domain gets its
own coverage entry at the 80 floor until a full run measures it.
2026-10-08 18:43:06 +02:00
Maurice ee921d5c28 test(offline): name the v6 upgrade case for what v6 does
Version 6 only adds the driving settings store; dropping the area cache
is version 7. The case name claimed both, so it now names only the new
store and a comment explains where the empty area cache it still checks
comes from.
2026-10-08 18:38:29 +02:00
Maurice 2d2d17619d docs(offline): narrow the rollback promise for newer-format writes
The skip of newer-format queued writes only exists from this release on,
so a rollback to an older release still replays them. On a rolled-back
install the skipped rows also stay pending with no retry or discard path
and hold back later writes to their entity, leaving Clear cache as the
only way out. Say both in the wiki and in client/CLAUDE.md instead of
promising that a rollback is always safe.
2026-10-08 18:38:29 +02:00
Maurice 5049f3762f test(offline): cover every Dexie upgrade path and guard new versions
Each version() of the offline database runs once, on a user's device, when a
new bundle opens what an older one wrote. None of them ran in CI: the v3 blob
cache backfill and the v8 area-fingerprint reset had never been exercised
against an older database.

The new test builds the database as each previous version left it in
fake-indexeddb, seeds a row into every table it had, opens it with the current
schema and checks that the rows survived and that the version did what it is
for. A guard reads the declared versions from offlineDb.ts and fails on a
duplicate, on a version without an upgrade case, or on stores the history
fixture does not reproduce. TrekOfflineDb is exported so the test can open a
named database with it.
2026-10-08 18:38:29 +02:00
Maurice 5bb45bc188 feat(offline): stamp queued writes with their format and skip newer ones on replay
A queued write outlives the bundle that wrote it. After a deploy a tab can
still run the previous bundle, and an install can be rolled back, so a row
written by a newer build may be replayed by an older one that does not know
its shape.

Each row enqueue() writes now carries schemaVersion (MUTATION_SCHEMA_VERSION)
and the UI build that queued it. Both fields are optional, so rows queued
before this read as format 1 and replay as before. A flush leaves a row from a
newer format in the queue without sending, marking or dropping it, and holds
back the later writes to the same entity as it does behind a parked write, so
they keep their order until a build that knows the format replays them.
2026-10-08 18:38:29 +02:00
Maurice 5aa9ac8529 fix(client): compare the running flush with null instead of testing a promise
_flushRun holds the running pass or null. Testing it as a boolean is
correct but reads like a forgotten await, and SonarCloud counts it as a
reliability bug (S6544). The explicit comparison says what is meant.
2026-10-08 14:53:13 +02:00
Maurice bd267c67f8 ci(client): fail the count ratchets on an entry above the tree
lint:size, lint:layers, lint:rtl (both baselines), lint:skips and
lint:warnings only failed a count that passed its entry. A count that
fell below its entry was logged and nothing more, so a file that shrank
could grow back to its old entry unseen, and the entry of a deleted
file stayed behind for the next file at that path. lint:format and
lint:offline already fail on such a stale entry, and the docs said
these counts may only shrink.

Each of them now fails on an entry that lowerCounts would lower or
drop, naming its count or saying the file is gone, and tells to run
--update, which still only lowers. staleCounts and reportStale in
lib/ratchet.mjs hold the shared part. Every baseline matches the tree
today, so none needed lowering. The client CLAUDE.md says so, and that
CI lints through lint:warnings rather than lint:check.
2026-10-08 14:11:54 +02:00
Maurice 4344dbe923 test(client): check the planner's note save through its store action
The planner hook tests spy on the store's actions for the visit times,
and the note now goes through a store action as well. Asserting on that
action instead of on the raw note route keeps these tests about the hook
and keeps the file inside its lint:size entry, which the response stub
the route needed had pushed three lines past.
2026-10-08 14:11:54 +02:00
Maurice df2768d30e fix(client): let a flush called mid-pass resolve with that pass
flush() returned at once when this tab already had a flush going, and
that includes one still waiting for another tab's Web Lock. The socket's
reconnect hook chains a refetch onto flush(), so a reconnect landing in
that window rehydrated the trip while a replay was still on its way, the
race the lock wait was meant to close. sendSoon and the online, periodic
and visibility triggers make such an overlap common.

flush() now keeps the running flush as a promise. A second call still
asks it for one more pass and then gets that same promise back, so it
resolves only once every pass, including the one it asked for, is done.
The pass itself moves to _flushPass, and the repeat runs as a loop
instead of a nested flush() call. The client CLAUDE.md sync bullet now
says so, and loses its dash.
2026-10-08 14:11:54 +02:00
Maurice 72c284467b fix(client): cache a visit note saved from the place dialog
The place dialog stopped reloading the days after a save, so the new
visit note only reached the Zustand store. Nothing wrote it to Dexie,
the tab never sees its own assignment:updated echo, and the time save
before it cached the server row with the old note. A trip reopened
offline then showed the old note until the next full sync.

The note now goes through assignmentRepo.setNotes, which caches the
saved row on its day online and queues the write behind any other
write of the same visit offline, like setTimes does. A new store
action applies it, so the page hook no longer patches the store by
hand. usePlaceEdits drops its direct API import and leaves the
offline baseline.
2026-10-08 14:11:54 +02:00