The pure model imported its direction type from the flow view. The type
now lives in the model, which takes it as the direction parameter, and
the flow view re-exports it for its existing importers.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
The count-string gate only knew {count} and {n}, the params t() picks a
plural form by. A quantity named anything else kept one form in every
language and passed: Russian read "Все {days} дней" for 2 days and
"{objects} объектов" for 3 objects, Greek "{minutes} λεπτά" for 1 minute.
Twelve such en strings become plural groups with their forms in all 27
locales, the quantity renamed to {count}: the trip length limit, the
chat image limit, the API key limit, the storage move prompt and its
reclaimable line, the Dawarich wish rule and suggestion duration, the
road trip lateness warnings, and the road trip echo and window texts.
Their callers pass the count as a number; the storage panels used to
pass it as a string, which no form is picked by. The reclaimable line
loses its dash for a comma in every locale while it is rewritten.
i18n-parity now treats the quantity names in QUANTITY_PARAMS ({days},
{minutes}, {objects}, {max} and the like) like {count}. The four strings
where the number is followed by nothing but a unit symbol (the Dawarich
durations, the upload size limit) go on NOT_PLURAL with that reason.
The panel fires a test photos_shared event with count: '5', which reads
like the string counts the plural work removed elsewhere. Notification
params are Record<string, string> from the server through the in-app
store, the real sender passes String(added), and resolveTemplate reads a
numeric string as the plural count, so the test event matches what
production sends.
The plural parity check only knows the groups en already declares, so a
"{count} ..." string added as a plain key kept one form in every
language: Polish read "Usuń 1 spakowanych", Czech "Odstranit 1
hotových" and Greek "1 λεπτά". 44 such en keys become plural groups with their
forms in all 27 locales: the collection, packing, day plan and offline
banner counts, the relative times, the audit log, the tour and transit
minutes, the share and photo hints. Labels like "Notes: {count}" get
their forms too, so a language can still inflect them later. The
dashboard's hand-rolled activeOne/activeMany pair, unused, becomes one
group, dashboard.subtitle.active.
i18n-parity now refuses an en string with {count} or {n} that is no
plural group. NOT_PLURAL lists the eleven where the number is never a
quantity (a day number, a step, a waypoint, the equator multiplier),
each with its reason, and an entry that stops matching fails as well.
Callers that worked around the engine:
- Copy to trip switched between two keys on placeIds.length > 1;
collections.copyN now carries the one-place wording as its `one` form,
where the language's `one` is the number 1 alone.
- The plugin dependency toasts said "addon(s)" and "plugin(s)"; they are
groups now and the panels pass the list length as the count.
- The dismissed suggestions count went in as a string and the comp days
as a formatted string, so no form was picked; both pass numbers.
login, completeMfaLogin and register start tripSyncManager.syncAll()
and the system notice fetch without waiting for them, and every
sign-in, loadUser included, registers the sync triggers with their
30 s heartbeat. #2613 stubbed the sync in the push spec after it was
caught logging while the worker shut down ("Closing rpc while
onUserConsoleLog was pending"); the same shape was left in the other
specs that sign in for real.
authStore.test.ts ran the real sync on every successful sign-in and
logged DatabaseClosedError from it, and its notice fetch went past MSW
to the network and logged a failed fetch, both after the tests that
started them. The sync is now stubbed there, and the login, MFA and
register tests assert that it was started. The notice store is reset
before each test, since FE-STORE-AUTH-024 left a stubbed fetch behind.
LoginPage.test.tsx signs in through the form, and AccountTab.test.tsx
and MSettingsAccount.test.tsx reload the user after an MFA change; they
now stub the sync triggers (and the login spec the trip sync), which
none of them is about.
GET /api/system-notices/active gets a default MSW handler returning no
notices, so a login or a mounted notice host anywhere in the suite
settles that fetch in the test instead of on the network. The notice
store's own tests still override it.
The planner's two time edits of a visit called the time route straight:
saving the place dialog opened from a visit, and removing a visit's time
before a reorder. They skipped the visit repo's check for an older write
to the same visit still in the queue, so with such a write parked or
backing off the new time landed first and Try again (or the next flush)
replayed the older time over it, with no version token to refuse it.
Both now use the store's setAssignmentTimes, which writes through
assignmentRepo.setTimes, queues behind the older write when there is one
and keeps the optimistic update and rollback the other time edits have.
The dialog no longer reloads the days afterwards, since the store already
holds the new time and a reload would show the server's older one while
the new one waits in the queue; a changed day note is set on the visit in
place instead. The retry docs drop the exception they made for these two
paths.
Keeping archived trips stopped the sync from wiping them, but left nothing
that ever took one off the device. The trip sync never refreshes an archived
trip, and the 7-day rule and the per-trip switch only looked at the active
list, so an archived trip stayed until Clear cache however long ago it ended.
The stale and switched-off rules now also run over the archived trips that
are stored here, with the same eviction as an active trip: its rows go, its
parked changes stay. The offline settings list the stored trips the server's
list leaves out, so an archived trip gets a switch the user can turn off;
before, the switches came from the active list only and could not reach it.
TripSummary leaves RoadtripSidebar.tsx for its own module and is
imported back by name; Unit and splitValue go with it unexported.
The day card's header becomes DayHeader in the same module. Its body is
the header element the card drew inline, together with the reads only
the header used: the language, the distance unit, the clock format,
the missed ride and the day colour. The card keeps its translation and
passes the day, the fold state and the track details straight through.
DayHeader returns the bare header with nothing around it, so the markup
is unchanged, and it sits at module scope so it never remounts.
The code moves as it was otherwise, with the dashes in the moved
comments rewritten as colons and commas, and the imports only these
two used leave the sidebar, which is down to about 700 lines. Nothing
renders differently.
New tests render both on their own: the summary's figures split from
their units, narrow and still loading; the day's facts and its fold
control; a day over the driving limit, roads it could not avoid, a
missed flight, the track badge with and without its name, and the
wash in the day's own colour.
ServiceStop, RouteViaStop and Stop leave RoadtripSidebar.tsx for their
own module and are imported back by name; effectiveFill goes with them
unexported, and the imports only they used leave the sidebar.
The badge row under a stop's name was written out twice, once in each
stop shape. It is now the local StopBadgeRow, which both render in its
place. It returns the same span with nothing around it, so the markup
is unchanged, and it reads the fill setting and the vehicle the two
shapes used to read themselves. Those are plain store reads, so moving
them one component down changes nothing either.
The code moves as it was otherwise. The note on the stop kind table now
sits at the lookup it describes, the comment on the two settings says
where they are read now, and the dashes in the moved comments become
colons, commas and full stops; each theme-lint-disable stays on the
line of its style. Nothing renders differently.
New tests render the rows on their own: a plugin halt with and without
its name and stay, the kind picker on a pause and on a numbered stop by
click, Enter and Space, the drive findings and the walk from the road
on both shapes, including the range and unknown findings the rail never
draws, and the fill badge with the traveller's default.
TerminalStop, BookendStop, RideBlock, BookingChips and UndatedRides
leave RoadtripSidebar.tsx for their own module and are imported back by
name. RIDE_DASH, DiscRow, CodeChip, TimetableClock and CheckInBadge go
with them unexported, since only these rows use them, and the imports
only they used leave the sidebar. The code moves as it was; the one
change is that UndatedRides spells out the type of its booking callback
instead of reading it off the sidebar's props, so the new module never
imports from the sidebar. Nothing renders differently.
New tests render each row on its own: a departure with its clock and
check-in, a hire desk, a booked night that is left after check-out, a
ride that is caught, missed or has nothing on its timetable, the chips
a reader may and may not open, and the rides on no day.
DriveBand and SpillBlock leave RoadtripSidebar.tsx for their own module
and are imported back by name. MODE_ICON goes with them, unexported,
since only the drive band reads it, and the imports only these two used
leave the sidebar. The code moves as it was, with the dashes in its
comments rewritten as colons, commas and full stops. Nothing renders
differently.
New tests render both on their own: every travel mode's sign, the
alternatives button and its pressed state, a hop, a ride with and
without its booking and timetable, and a night drive that is automatic,
has no leg yet or misses its figures.
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.
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.