Commit Graph

1314 Commits

Author SHA1 Message Date
Maurice cb04ebe6d2 fix(trips): the trip form drops its complaint once you edit, and the typed date hint follows your language
"Title is required" stayed on screen after the title was typed, until the
next save. Any edit in the trip form, desktop and phone, now clears the
last save's complaint; the next save checks again.

The keyboard date field always hinted DD.MM.YYYY, while it was filled and
read back in the user's own order: a Korean user saw "2026. 12. 10." under
a German hint. The hint is now the locale's own pattern (YYYY. MM. DD. in
Korean, MM/DD/YYYY in US English), and the field's name is translated.

Refs #2604
2026-10-12 15:46:02 +02:00
Maurice 35eba21128 fix(costs): row buttons, settle-up rows and share toggles say what they are about to a screen reader
A screen reader lists a page's buttons together, and the costs views gave
it twenty "Edit" and "Delete" buttons with nothing to tell them apart.

- Edit and delete on an expense name the expense ("Edit: Dinner"), edit
  and undo on a payment name who paid whom, on desktop and phone.
- A settle-up row shows only initials in its avatars, its names lived in a
  tooltip; the row now carries them for assistive tech too, and its Settle
  button says who pays whom and how much.
- The search field of every searchable select (currency, time zone,
  category) was read out as "..."; it now says Search in the user's
  language.
- The public link toggles on the phone share sheet and the journey share
  toggles say whether they are on (aria-pressed), not only by colour.
- Japanese still said "you" in the settle-up lines.

Refs #2604
2026-10-12 15:45:56 +02:00
Maurice dbbbd07b1c fix(costs): amounts follow their currency's decimals and format, and payments count on the local day
Several small things in the costs views, on desktop and phone alike:

- The outstanding card no longer says "You're all settled up" while a
  payment is still open in Settle up.
- The custom split shows its suggested shares as a faint placeholder in
  the currency's own form (30000 for won, not 30000.00), so it no longer
  reads like an amount already typed.
- Amount fields take only the decimals the currency has: won and yen take
  none, KWD and the other three-decimal currencies three. A switch of
  currency cuts what was already typed. 1000.555 won was saved as typed,
  listed as 1,001 and reopened as 1001.
- The expense sheet on the phone prints shares and sums with the same
  formatter as the list, with grouping and the currency's decimals.
- A payment recorded with Settle or Settle all is dated on the viewer's
  own day, and one without a date of its own is grouped by its UTC record
  time read on the local calendar. In Seoul before 09:00 it landed on the
  day before.
- "1 expenses need a payer" now uses the plural forms of each language.

Refs #2604
2026-10-12 15:04:32 +02:00
Maurice 9064dbb2f0 fix(costs): an expense made from a booking is dated on the booking day
Creating the expense from a booking opened the editor on today, and the
cost a booking import writes by itself had no date at all, so it sat under
"No date". Both now take the booking's own date, else a hotel's check-in
day, else the day the booking sits on. The server sets it for every
booking write that creates a cost, so REST, MCP, plugin RPC and the import
agree.

A start time typed on a booking with no day picked no longer borrows
today's date on desktop. Today was mostly outside the trip, the server
then moved the booking onto day 1 while the list kept today. Desktop now
does what the phone already did and keeps the time without a date.

Refs #2604
2026-10-12 15:04:22 +02:00
Maurice e70fc334bd fix(costs): people who left the trip are named and keep their balance in the costs lists
Someone removed from a trip stays on the expenses they paid for or shared,
but the costs views only knew the current members. They showed up as "?"
in the expense chips and the settle-up suggestions, and the balances and
final budget lists left them out, so the listed balances no longer added
up to zero.

The names now come from what the server already sends with the expenses
and the settlement, and anyone off the trip who still carries money gets a
row after the members, on desktop and on the phone. The expense form is
unchanged.

Refs #2604
2026-10-12 13:22:32 +02:00
Maurice 8acc035efa fix(collections): all saved reads its lists four at a time and reloads once per burst of updates
The "All saved" view read every list in full at the same time, and every
collections:* websocket event started that again right away. An agent saving
places one by one over MCP sends one event per place, so an open collections
tab stacked up full reads of every list on the server.

The union now goes through the same batched loader the trip's saved-places
picker uses (four lists at a time), reloads asked for while it runs fold into
one more pass, and a list or union read that lands after the user switched to
something else is dropped instead of painting its places over the new view.
The page waits for a run of events to settle before it reloads and keeps the
selection on "All saved" like it already did on a single list. Desktop and
phone share useCollections, so both get it.

Refs #2559
2026-10-12 12:59:44 +02:00
Maurice 2f0b867685 fix(settings): the integrations tab is always there on desktop so api keys stay reachable
API keys were split off from MCP to work without any addon, but the tab
that holds them still hid unless a photo provider, MCP, AirTrail, AI
Parsing or Dawarich was enabled. On a fresh install all of those are off,
so there was no way to create a key. The desktop tab now always shows;
the addon backed sections inside it keep their own gates. The phone layout
has no API Keys section and keeps its gate.

The wiki now says where keys are created and that no addon is needed.

Refs #2561
2026-10-12 12:46:12 +02:00
Maurice 12f08e3646 fix(planner): a timed booking dropped against its time goes back alone instead of taking the stops behind the drop along
The day list draws a row with a time by that time, and an untimed stop takes the
time of the timed row before it. A transit ride dropped behind a later stop was
stored there, drawn back at its time on the next render, and every untimed stop
behind the drop went with it; the next drag then stored that shuffle. A dropped
booking or timed note now lands in the slot nearest the drop the list draws it
in, so nothing else moves and what is stored is what is shown. Rides dropped
where their time allows stay on the drop, and the rule for untimed stops behind
a booking is unchanged.

The chronology check for a timed stop reads a booking's time for the day (the
arrival on the last day of a multi-day ride) instead of its departure, and the
three copies of the remove and insert step share one helper.

Refs #2631
2026-10-12 12:33:12 +02:00
Maurice c2dd66986c fix(maps): the day route leaves an imported gpx track to the track and carries on from its end
A GPX or KML import stores the track's first point as the place's position,
so the day route treated it as a single stop there and routed from the
track start to the next stop, a second road running alongside the track.
Any assigned place with a usable track now gets the gap a Tour already had:
the route runs up to the track start and continues from the track end, on
the desktop map and day list, the mobile timeline, the trip overview and the
PDF map, which now draws the track as a line. A place without a usable track
stays a plain stop and never shows as an invalid Tour.

Refs #2600
2026-10-12 11:43:50 +02:00
Maurice 3dbe76945d fix(roadtrip): the day view drives the road picked in the road trip
A chosen alternative or a hand-bent drive is stored as vias, and only the
road trip rail routed through them. The day route on the map, its
connectors in the day list and on the phone, and the overview of all days
asked the router for the bare stops, so they drew and measured the default
road again.

They now thread the same vias between two stops that follow each other in
the stored list, with the anchor counted the way the rail counts it, and
fold the answer back to one leg per stop pair. Only while the road trip
addon is on; avoid settings and the routing engine stay with the rail.

Refs #2580
2026-10-12 11:32:15 +02:00
Maurice 565333a90c fix(roadtrip): a hotel visited again on its check-in day is the night only at its last visit
A stop counted as the booked night whenever a stay began at its place on its
day, so a hotel on its check-in day twice (the booking's own stop and a rest
the traveller planned there) was read as two nights. Both were held to the
check-in: the rest waited for the room or was set back to it, and with a check-in
far from its arrival the rest and every stop behind it moved onto the next day's
card.

oneNightPerStay in shared keeps the night and its check-in on the last visit of
the place and turns the earlier ones into ordinary stops, still a hotel. The
browser and calculate_roadtrip both run it, the stored ride slot times only that
row by the check-in, and opening an earlier visit in road trip mode opens the
stop rather than the night.

Refs #2636
2026-10-12 11:05:41 +02:00
Maurice e3bf18f9a6 fix(collections): a list invite can be accepted where its notification leads, on phone and desktop
The invite notification links to /collections/:id, a list the invitee
cannot read until they accept. The phone screen had no invite UI at all
and showed "Start your first collection", and on desktop the rail with the
invites is hidden below 1080px while the empty state had no way to open it.

useCollections now recognises a route id that is a pending invite, keeps
it out of the route sync and puts it first in the invites it hands out.
The phone screen shows the pending invites as cards above the lists, and
the desktop body shows them when the person has no lists of their own,
or the one invite the route points at. Both reuse the existing invite
strings. A route id that is neither a list nor an invite (declined,
withdrawn, deleted) now drops back to /collections once the lists are in,
instead of leaving the page on a list it cannot show.

Refs #2645
2026-10-12 10:44:57 +02:00
Maurice c94d275cfd fix(journey): gallery photos can be deleted on touch screens and ask before they go
The delete on a gallery tile only showed on mouse hover, so a phone or
tablet never saw it, and the phone screen had no delete at all. Where it
was reachable it deleted at once, although the server removes the photo
for good and takes it out of every entry that uses it.

The desktop tile now shows its delete on hover, on keyboard focus and
always where there is no hover, with a larger target there and an aria
label; viewers no longer get the button. The phone gallery gets the same
corner button for editors. Both ask through ConfirmDialog first, saying
that the photo also leaves the entries it is used in, and both delete
through the store action. The entry editor's photo controls follow the
same hover rule.

Refs #2593
2026-10-12 10:32:32 +02:00
Maurice 90352b8e3a fix(journey): draw the dashed line between entries on fullscreen leaflet maps too
The Leaflet journey map only drew the dashed route between entries when
fullScreen was off. Since the desktop sidebar and the mobile map plus
carousel view both pass fullScreen, the line was missing on the public
journey page (desktop and phone) and in the logged in views with the
Leaflet provider, while the GL renderer always drew it. fullScreen now
only enables wheel zoom, matching JourneyMapGL where it only sets the pitch.

Refs #2606
2026-10-12 10:19:39 +02:00
Maurice 4ee5e3fa96 fix(bookings): booking cards always show the calendar date of their linked day
Phone booking and transport cards printed only the title of the linked day,
so a long title cut off in the field left no date at all. They now print the
date beside the title like the desktop card, as a span that never shrinks
while the title is truncated, and a day range takes the full width with the
time under it. The desktop card gets the same non-shrinking date.

A day without a date of its own (a trip planned without dates) now borrows
the booking's start or end date, on the desktop cards, the list, the detail
dialog, the shared page and the phone alike.

Refs #2590
2026-10-12 10:16:01 +02:00
Maurice 11ede4fbef fix(journey): emptying the location field in the entry editor removes the location
The field showed the stored place as soon as the search query was empty, so
deleting the last character snapped it back and the location could never be
removed. Clearing the field now empties the place and its coordinates, closes
the suggestions and drops any search answer that comes back afterwards. Typed
text without a picked result keeps the stored location as before. The hook is
shared, so the desktop dialog and the phone sheet both get the fix.

Refs #2615
2026-10-12 09:33:00 +02:00
Maurice 4a36a60f5a test(roadtrip): a ferry leg draws no car on its band, while the day head keeps its drive pill's car 2026-10-11 15:06:59 +02:00
Maurice c6f45b9be6 refactor(planner): a day head's pill row moves into a module of its own
The stays, hire cars and plugin label under a day's name now live in DayHeadPills, so DayPlanSidebar shrinks back under its size baseline. The stay fields are read through their types instead of an any cast, and the size, eslint and theme-disable baselines are lowered to the new counts.
2026-10-11 14:22:46 +02:00
Maurice 32606d20a6 fix(roadtrip): the rail passes the strict and lint gates, and its tests describe the new markup
The day head falls back to metric when no distance unit is set, VISIT_BOX is no longer exported, the day head is split into smaller parts and the booking icon is created once. The component tests now check the pill badges, the day tile and the ride cards instead of the old boxes, and both baselines are lowered to the new counts.
2026-10-11 14:01:18 +02:00
Maurice e8fd41b9b4 feat(roadtrip): the Google route import and the track dialog use the shared dialog frame, and Search and Manual match the day plan's buttons
Both dialogs now open in the same frame as the stay and stop dialogs: an icon tile and title in the head, the content in a card, and the actions in the footer. Closing stays blocked while an import runs. In the right column, Search and Manual take the size, text and icon of the day plan's toolbar buttons.
2026-10-11 14:01:18 +02:00
Maurice 0c554e319b fix(planner): a day head keeps its stays on one line, each name cut by the same share, under the name and the tools alike 2026-10-11 02:42:21 +02:00
Maurice 856f5b5b08 feat(roadtrip): the rail and its dialogs in the planner's card language
Day heads take the day plan's layout: the number on a raised tile with the date
under it, the name beside it and its facts as pills that never wrap but give way
in steps. Rides, terminals and hire desks are cards in their type's colour with
their icon on the line and their times in the arrival column; a stop's bookings
sit on its own badge row. Every badge on the rail is one pill shape, the drive
bands are round, and the stay, stop and driving settings dialogs use the shared
dialog frame.
2026-10-11 02:36:58 +02:00
Maurice 3ff88062f1 fix(planner): pills on a white row keep a hairline, share one height, and a transport reads its time before its phase 2026-10-11 02:36:58 +02:00
Maurice cc94bc6953 refactor(planner): the day header's stay order lives in its own module and its tests in their own file
The day badge fix grew DayPlanSidebar.tsx and its test past their size
baselines. The check-out, through, check-in order of the header's stays is
now dayHeadStays() with a test of its own, and the day header tests sit in
DayPlanSidebar.dayHead.test.tsx; both files drop below their old entries.
2026-10-10 22:18:36 +02:00
Maurice b9a8bfc3e7 refactor(budget): the cost preview splits through the shared cent split the settlement uses 2026-10-10 21:43:08 +02:00
Maurice d496dd5d30 fix(planner): a day without a stay centres its name on the day badge 2026-10-10 21:33:38 +02:00
Maurice f51a64017d refactor(client): type both trip map renderers against one MapViewProps contract
MapViewAuto took props: any and the Leaflet MapView destructured any, so a prop
one renderer learned and the other never heard of went unnoticed.
components/Map/mapViewContract.ts holds MapViewProps, derived from the GL
renderer's props and what the callers pass; MapView and the GL bindings in
glLazy declare satisfies ComponentType against it, and the GL renderer extends
it with its engine. The file imports types only, so check:gl-split is not
affected. Types only, no runtime change.

The types show two known gaps, recorded in the contract rather than papered
over: onMapReady is called by the GL renderer only, and callers pass pins
without trip_id (assignment and collection places) while both renderers type
their pins as Place; MapViewAuto bridges the latter with one commented cast.
2026-10-10 21:31:34 +02:00
Maurice 2b86d71d51 test(client): drop the mocks of the deleted server database service
server/src/nest/database/database.service no longer exists, so the five
vi.mock calls that guarded against importing it guarded nothing.
2026-10-10 21:31:33 +02:00
Maurice e971c41d1a chore(client): delete the old budget panel components that only their own tests still imported
BudgetPanel, its summary, category table, inline edit cell, add row, per person
line, pie chart, ring avatar and the useBudgetPanel hook were replaced by the
costs panel and nothing outside their own tests imports them any more.
BudgetPanel.constants, BudgetPanel.helpers and BudgetPanelMemberChips stay in
use. The rtl, format, offline and eslint baselines drop the deleted files.
2026-10-10 21:31:33 +02:00
Maurice 5644e3d77e fix(vacay): the desktop calendar no longer requests the trips again on every vacay refresh
Every vacay refresh on desktop (opening the page, a vacay:settings or
vacay:invite update from another member) sent a second GET /trips just to
redraw the same trip dots, even when the leave-year window had not changed.

Cause: useVacayCalendarLogic keyed the trip dot reload on the year settings
object when tripsFollowWindowShape was false (the desktop grid). loadAll()
replaces that object with a deep-equal copy on every refresh, so the effect
ran again. The phone screen already keyed it on the window shape.

Fix: the trip dots always reload on the window shape (year type, start month,
start day, hire date), which covers every field the dot filter reads. The
option only existed to keep the desktop behaviour, so it is removed from the
hook and from both callers.

Red test: FE-COMP-VACAYCAL-010 (useVacayCalendarLogic.test.ts) failed on the
unfixed code with "AssertionError: expected "list" to be called 1 times, but
got 2 times".

Changed assertions:
- useVacayCalendarLogic.test.ts FE-COMP-VACAYCAL-010: was "the desktop mode
  reloads trips on every new settings object" expecting tripsApi.list to be
  called 2 times after a deep-equal settings replacement; now expects 1.
- FE-COMP-VACAYCAL-011 no longer passes tripsFollowWindowShape and no longer
  names the phone mode; its assertions are unchanged.
2026-10-09 19:00:47 +02:00
Maurice 694e11c117 fix(vacay): a failed user load in the desktop invite and share dialogs no longer shows a stale list
When loading the users that can be invited to a fusion or given a calendar
share failed on desktop, the dialog kept showing the list from the last load,
so a user could pick someone who is no longer available and only learn it
from the failed send. The phone sheets already emptied the list and showed
their "no users available" state.

Cause: useVacayUserPicker only emptied the list on a failed load when its
clearOnLoadError option was true; the desktop VacayPersons and
VacaySharedCalendars dialogs passed false, a leftover of the old silent
catch in the desktop dialogs.

Fix: the hook always empties the list when the load fails. The option only
existed to keep the desktop behaviour, so it is removed from the hook and
from all four callers.

Red test: FE-COMP-VACAYPICK-002 (useVacayUserPicker.test.ts) failed on the
unfixed code with "AssertionError: expected [ { id: 2, username: 'bob' },
...(1) ] to deeply equal []".

Changed assertions:
- useVacayUserPicker.test.ts FE-COMP-VACAYPICK-002: was "a failed load keeps
  the last list on desktop" expecting available to equal USERS; now expects
  an empty list.
- FE-COMP-VACAYPICK-003: was "a failed load empties the list on the phone"
  through load() with clearOnLoadError true; now drives the same expectation
  (empty list) through a phone sheet reopening, since the option is gone.
2026-10-09 19:00:47 +02:00
Maurice 33db4ea7cb fix(dashboard): the desktop currency widget always shows the rate for the currency picked last
Switching the from currency of the desktop dashboard currency widget quickly
could show the rate of the previous currency: when the older rate request
answered after the newer one, its answer overwrote the newer rates.

Cause: useCurrencyConverter only aborted a request still in flight in its
phone mode (abortStale true); the desktop dashboard passed false and fetched
without any stale guard.

Fix: the hook now always aborts the previous request when a new one starts
and when the widget goes away, and ignores answers of an aborted request. The
abortStale option only existed to keep the desktop without the guard, so it
is removed and both dashboards call useCurrencyConverter().

Red test: FE-COMP-FXHOOK-008 (useCurrencyConverter.test.ts) failed on the
unfixed code with "AssertionError: expected 1.1 to be 1.3".

Changed assertions:
- useCurrencyConverter.test.ts FE-COMP-FXHOOK-002: was "the desktop mode
  fetches without a signal, the phone mode with one" expecting the desktop
  call to be [url] only; now expects [url, { signal: AbortSignal }].
- FE-COMP-FXHOOK-003 no longer names the phone mode; its assertions are
  unchanged. All tests call the hook without the removed option.
2026-10-09 19:00:47 +02:00
Maurice 1ad0ef6d78 refactor(settings): photo provider cards read their state from the shared hook
With the badge and toast options gone from usePhotoProviderConnections,
the desktop and phone cards called the hook identically and then derived
the same seven per card values (fields, config, values, badge, testing,
whether save and test have a route) line for line, which lint:dup counts
as a copied block of 25 lines in MPhotoProvidersSection.tsx.

The hook now hands each card those values through providerCard, and both
views keep only their markup. Nothing the user sees changes.

New test FE-COMP-PHOTOCONN-011 covers providerCard. No existing assertion
changed.
2026-10-09 19:00:47 +02:00
Maurice 24461ecf76 fix(admin): the default map template offers the Amap presets users can pick
In the admin's default user settings, the map template dropdown listed
seven tile presets, while every user's own map settings listed nine: the
same seven plus Amap and Amap Satellite. An admin running an instance for
people in mainland China could not pick the one basemap that works well
there as the default, short of pasting its URL by hand.

The Amap presets (57eac32f9) were added to the two user preset copies and
not to the admin's separate copy; nothing in that change or later gives a
reason, and the server stores any template as a default. The later
refactor kept the gap as DEFAULT_MAP_PRESETS beside MAP_PRESETS. There is
now one MAP_PRESETS list, read by the user settings and the admin defaults
on both desktop and phone.

Regression test FE-ADMIN-DUS-029 (DefaultUserSettingsTab.test.tsx) was red
on the old code: "Unable to find role="button" and name "高德地图 (Amap)"".

Changed assertions:
- useMapSettingsForm.test.ts FE-COMP-MAPFORM-001: it expected the admin list
  DEFAULT_MAP_PRESETS to hold 7 presets and the user list to add Amap on
  top; it now expects the one list to hold 9 presets including both Amap
  templates.
2026-10-09 19:00:47 +02:00
Maurice 76b1e67be8 fix(settings): the desktop photo provider badge follows the live status check
On the desktop, the Immich (or Synology) card could show "Connected" for a
server that was down. The settings page loads two answers at once: the
saved settings, whose connected flag for Immich only says that a URL and an
API key are stored, and the status route, which actually reaches the
server. Whichever landed last set the badge.

usePhotoProviderConnections had a statusRouteOwnsBadge option that only the
phone turned on. The option is gone: whenever a provider declares a status
route, only that route sets the badge, on both views. A provider without a
status route still takes the badge from its settings read.

Regression test FE-COMP-PHOTOPROVIDERS-028 (PhotoProvidersSection.test.tsx)
was red on the old code: "Unable to find an element with the text: Not
connected" (the late settings read had turned the badge to Connected).

Changed assertions:
- usePhotoProviderConnections.test.ts FE-COMP-PHOTOCONN-005: it expected the
  later settings read (connected: false) to override the status route's
  connected: true; it now checks that a provider without a status route
  takes the badge from the settings read.
- usePhotoProviderConnections.test.ts: render() no longer takes the option,
  so every call drops its true or false argument.
2026-10-09 19:00:46 +02:00
Maurice 0b90fc8cf5 fix(settings): a failed photo provider test reads "Immich: reason" on the desktop
On the desktop, a failed photo provider connection test toasted "Could not
connect to Immich : Auth failed" with a stray space before the colon, and
"Could not connect to Immich " with a trailing space when the server gave
no reason. The phone already read "Could not connect to Immich: Auth
failed".

usePhotoProviderConnections took a testErrorGap option that put a space
between the sentence and ": reason", and the desktop passed ' '. The option
is gone, so both views join the reason directly.

Regression test FE-COMP-PHOTOPROVIDERS-027 (PhotoProvidersSection.test.tsx)
was red on the old code: "Unable to find an element with the text: Could
not connect to Immich: Auth failed" (the toast read "Could not connect to
Immich : Auth failed").

Changed assertions:
- usePhotoProviderConnections.test.ts FE-COMP-PHOTOCONN-010: the desktop
  toast was 'memories.connectionError(Immich) : bad key', now
  'memories.connectionError(Immich): bad key'; it also checks that a
  failure without a reason toasts the bare sentence.
2026-10-09 19:00:46 +02:00
Maurice 12962480d5 fix(settings): notification settings show an error when they cannot load
When the notification preferences failed to load, the matrix on both the
desktop tab and the phone screen stayed on its loading line for good and
nothing told the user anything went wrong. A failed load of the webhook and
ntfy settings was just as silent and left empty fields that looked like
nothing was saved.

Both loads in useNotificationSettings ended in .catch(() => {}) and had no
cancelled flag. Each failure now shows the common error toast, a failed
preferences load sets loadFailed so both views replace the loading line
with the error text, and both loads ignore a response that arrives after
the screen is gone.

Regression test FE-COMP-NOTIFSETTINGS-010 (useNotificationSettings.test.ts)
was red on the old code: "expected "vi.fn()" to be called 2 times, but got
0 times". FE-COMP-NOTIFSETTINGS-011 covers the cancelled flag and
FE-COMP-NOTIFICATIONS-032 the desktop view.

Changed assertions:
- MSettingsNotifications.test.tsx FE-MOB-SETNOTIF-002: a failed load used
  to expect the "Loading..." line to stay; it now expects "Error" and no
  loading line.
2026-10-09 19:00:46 +02:00
Maurice aa759ba54a fix(settings): phone notification toggles save only the switch that was tapped
On the phone, every notification toggle sent the whole preference matrix as
it looked when the switch was tapped. With two toggles in flight the later
request could overwrite the earlier one on the server, and when the first
request failed the screen rolled that switch back while the second request
had already saved its value anyway.

The cause was the sendWholeMatrix option of useNotificationSettings, which
only the phone turned on. The server merges whatever cells it receives, so
the desktop already sent just the toggled cell. The option is gone and both
views send only the cell that changed.

Regression test FE-MOB-SETNOTIF-035 (MSettingsNotifications.test.tsx) was
red on the old code: "expected [ { trip_invite: { inapp: false,
webhook: false } }, { trip_invite: { inapp: false, webhook: true } } ] to
deeply equal [ { trip_invite: { inapp: false } }, { trip_invite:
{ webhook: true } } ]".

Changed assertions:
- useNotificationSettings.test.ts FE-COMP-NOTIFSETTINGS-004: the phone
  toggle body was { trip_invite: { inapp: false, email: false } }, now
  { trip_invite: { inapp: false } } (the hook is built with the phone's
  remaining option, skipMaskedToken).
- MSettingsNotifications.test.tsx FE-MOB-SETNOTIF-026: the PUT body was
  { trip_invite: { inapp: false, webhook: false } }, now
  { trip_invite: { inapp: false } }.
- MSettingsNotifications.test.tsx FE-MOB-SETNOTIF-032: the mock told the
  in-app write apart by body.trip_invite.webhook === false, which only the
  whole matrix carried; it now checks for the inapp key.
2026-10-09 19:00:46 +02:00
Maurice df7bb9a5e1 fix(storage): Test on a mirrored phone storage row probes the backends on the form
On the phone storage panel, Test on a mirrored backend sent the mirror
itself to POST /api/admin/storage/test. The server expands a mirror by
looking its primary and replicas up by name in the SAVED storage
config, so the phone tested the saved settings instead of what the
operator had just edited, and a replica added but not saved yet made
the whole test fail with "references unknown backend". The desktop
probes the primary and each replica with their draft options and
merges the results.

Cause: the phone called admin.test with the mirror candidate, while the
desktop called admin.testMirror with mirrorProbeTargets. The choice
lived in each shell's markup.

Fix: useStoragePanel now offers loaded.testRow(row), which tests a
plain row as it is and a mirrored row through admin.testMirror with
mirrorProbeTargets from the draft. Both shells call it.

Red test: FE-MOB-MSTOR-018 (new, MAdminStoragePanel.test.tsx) failed
with "AssertionError: expected [ 'mirror' ] to deeply equal
[ 'backups-local', 'off-box' ]". FE-ADMIN-STOR-PANEL-017 (new,
useStoragePanel.test.ts) covers testRow itself.

No existing assertion changed.
2026-10-09 19:00:46 +02:00
Maurice 8dd8635b87 fix(plugins): "Update all" on the phone updates the plugins one after another
On the phone plugins panel, "Update all" fired every pending update at
the same time. Only one row can show as busy, so the other rows stayed
clickable while their own install was running, and the list reloaded
once per plugin as each update finished. The desktop runs the updates
one after another, and the hook's own comment calls the parallel way
wrong.

Cause: usePluginsAdmin kept the phone's parallel loop behind the
updateAllInParallel option, which only the phone set.

Fix: the option is gone; updateAll always waits for each update before
starting the next, on both shells.

Red test: FE-ADMIN-PLUGINS-HOOK-009 (usePluginsAdmin.test.ts) failed
with "AssertionError: expected [ [ 'a', undefined ], …(1) ] to deeply
equal [ [ 'a', undefined ] ]".

Changed assertions in src/components/Admin/plugins/usePluginsAdmin.test.ts:
- FE-ADMIN-PLUGINS-HOOK-009 (renamed from "with updateAllInParallel
  every update starts at once, without a version" to "Update all starts
  only the first update, without a version"): pluginUpdate calls were
  [['a', undefined], ['b', undefined]], now [['a', undefined]]. It and
  the mounted() helper no longer pass the removed option.
- FE-ADMIN-PLUGINS-HOOK-008: only its name lost "by default".
2026-10-09 19:00:46 +02:00
Maurice a09d71ed12 fix(storage): a phone migration that fails to start drops the rest of the queue and says so
On the phone storage panel, when the operator accepted several category
moves and the first migration failed to start (for example the server
was busy), only that error was toasted. The other accepted moves stayed
in a hidden queue with no hint, and started on their own later whenever
the panel state happened to refresh. The desktop drops the rest of the
queue, names it in a storage.migrate.queueDropped toast and refreshes
the state, so the operator sees what did not move and saves again.

Cause: useStoragePanel only dropped the queue when the
dropQueueOnFailure option was set, and only the desktop set it.

Fix: the option is gone and useStoragePanel always drops the rest of
the queue on a failed start, toasts it and refreshes, for both shells.

Red test: FE-ADMIN-STOR-PANEL-011 (useStoragePanel.test.ts) failed with
"AssertionError: expected "vi.fn()" to be called 2 times, but got 1
times".

Changed assertions in src/components/Admin/storage/useStoragePanel.test.ts:
- FE-ADMIN-STOR-PANEL-011 (renamed from "by default a failed migration
  start only toasts and keeps the rest queued"): toast.error was called
  once, now twice; migrationQueue had length 1, now 0; refreshState was
  not called, now it is.
- FE-ADMIN-STOR-PANEL-012: no assertion changed; it no longer passes the
  removed dropQueueOnFailure option.
2026-10-09 19:00:46 +02:00
Maurice abbd2a94ce fix(admin): a failed "Load more" in the desktop release history keeps the list and can be retried
On the desktop admin GitHub panel, when "Load more" failed (for example
GitHub rate limiting), the whole release history was replaced by the
error card for the rest of the session: no list, no Load more button,
no way to retry. Had the user found a way to load again, the next load
would also have skipped the page that failed.

Cause: in its one page per load mode useGithubReleases advanced the
page counter whether the request worked or not and never cleared the
error, and GitHubPanel showed the error card whenever an error was set.
The phone (fillPages mode) already held the page, cleared the error on
the next good load and drew the error inline under its timeline.

Fix: the one page mode now moves the page on only after a good load and
clears the error then, like fillPages. GitHubPanel shows the error card
only when nothing has loaded yet; after that it keeps the timeline,
shows the error as a line above Load more and leaves the button there.
The page walk difference behind fillPages stays as it was.

Red tests:
- FE-COMP-GHREL-HOOK-012 (new, useGithubReleases.test.ts) failed with
  "AssertionError: expected [ 1, 2, 3 ] to deeply equal [ 1, 2, 2 ]".
- FE-ADMIN-GH-017 (new, GitHubPanel.test.tsx) failed with "Unable to
  find an element with the text: /v1\.\d\.0/" after the failed load.

Changed assertions in src/components/Admin/useGithubReleases.test.ts:
- FE-COMP-GHREL-HOOK-004 (renamed from "sets the error and still moves
  the page on" to "sets the error and holds the page"): requested pages
  were [1, 2, 3], now [1, 2, 2].
2026-10-09 19:00:46 +02:00
Maurice 6c916cc257 fix(collab): a note category shows the same colour on the phone as on desktop
When the notes of one category carried two colours (a category recolour
that failed halfway, or a note written over the API), the desktop panel
painted the category in the colour of the last such note in the list
while the phone notes tab used the first one, so the same category had
different colours on the two devices.

The phone's buildCategoryColorMap took the first colour on the stated
assumption that every note in a category has the same colour, so first
seen equals all seen; it was meant to match the desktop and only
differed where that assumption breaks. The desktop rule (since v2.6.0)
is the original. buildCategoryColorMap now keeps the last colour seen
and the desktop panel builds its map from the same function, merged
over its empty categories as before.

Red before the fix: tests/unit/mobile/trip/collabModel.test.ts
FE-MOB-CLBM-023 failed with "expected { Ideas: '#ef4444' } to deeply
equal { Ideas: '#10b981' }".

Changed assertion in tests/unit/mobile/trip/collabModel.test.ts
FE-MOB-CLBM-011 (renamed from "keeps the first colour seen per
category" to "keeps the last colour seen per category"): the expected
map was { Ideas: '#ef4444' } and is now { Ideas: '#10b981' }.
2026-10-09 19:00:45 +02:00
Maurice d118423570 fix(journey): the desktop gallery Upload button shows the files counting up again
Uploading photos into a journey gallery on desktop gave no sign that
anything was happening until the toast at the end; the Upload button
stayed clickable the whole time. Up to v4.0.0 the button sat inside the
gallery and showed a spinner with "Uploading done/total" while it was
disabled. When the button moved up into the page header (onRegisterUpload)
that indicator was dropped, and the count kept being tracked with no
reader.

useGalleryUpload now also returns the progress it tracks, the gallery
hands it up through onUploadProgress into useJourneyDetail, and the
page's Upload button is disabled and shows the spinner with
journey.editor.uploadingProgress while the files go, as it did before.

Red before the fix: src/pages/JourneyDetailPage.wiring.test.tsx
FE-JRN-DETWIRE-043 failed with "expected undefined to be [Function
Mock]": the gallery was never given an onUploadProgress callback and
the button never showed a count. FE-JRN-GALLERY-021 and
FE-JRN-GALLERYUPLOAD-007 cover the gallery and the hook. No existing
assertions changed.
2026-10-09 19:00:45 +02:00
Maurice 65448ce186 fix(journey): a failed entry save on the phone says so instead of failing silently
Saving a journey entry on the phone sheet while the server refused it
(a locked journey, a dropped connection) left the sheet sitting there
with no message, and the rejection went unhandled. The desktop dialog
toasted journey.settings.saveFailed or the server's reason.

The cause was the toastSaveError option of the shared useJourneyEntryForm
hook: only the desktop set it, and without it handleSave rethrew into a
click handler that nothing awaited. The option only kept the phone on
the broken path, so it is gone and both views toast and keep the form
open.

Red before the fix: src/components/Journey/useJourneyEntryForm.test.ts
FE-JRN-ENTRYFORM-021 failed because handleSave rejected with
"{ response: { data: { error: 'Locked' } } }" instead of toasting.

Changed assertions in FE-JRN-ENTRYFORM-007 (renamed to "a failed save
toasts for the desktop dialog and the sheet alike"): the sheet's save
was expected to reject (rejected true) with one toast in total; it now
resolves (rejected false) and the toast count is 2.
2026-10-09 19:00:45 +02:00
Maurice f19b52fb20 fix(collab): note attachments added or removed on the phone show in the Files tab at once
On the phone, attaching a file to a note, detaching one or deleting a
note with attachments left the trip's Files tab on the old list until a
reload. The desktop panel fires the local collab-files-changed event
that useTripWebSocket listens for, because the server leaves the
writer's own socket out of its broadcast; the phone notes tab never
fired it.

The event now lives in the shared useCollabNotesData hook:
uploadNoteFiles announces it once after a batch, and
announceNoteFilesChanged is what both shells call after deleting a note
or a note file. The desktop no longer fires it by hand after an upload.

Red before the fix: tests/unit/mobile/trip/MCollabNotes.test.tsx
FE-MOB-CNOTE-034 failed with "expected 0 to be 1" on the event count
after detaching a file. FE-COLLAB-NOTESDATA-009 covers the hook.
No existing assertions changed.
2026-10-09 19:00:45 +02:00
Maurice 0246e5c317 fix(map): a distance between near antipodal points is half the globe instead of NaN
haversineKm in src/utils/geo.ts answered NaN for some near antipodal pairs,
for example 47.3812, -10.8827 and -47.3812001, 169.1173. Rounding pushed the
haversine term a hair past 1, and asin of a value above 1 is NaN, which then
spread into route sums and range checks.

Cause: unlike the Studio legKm and the roadtrip haversineKm in
@trek/shared, the shared client helper had no clamp before Math.asin.

Fix: clamp with Math.min(1, ...) before Math.asin. With the clamp the helper
runs exactly the operations of Studio legKm in the same order (2 * 6371 is
12742), so Studio now uses haversineKm and legKm goes, with its cases carried
into the new geo tests. The roadtrip variant multiplies its terms in another
order and gives different last bits, so it stays as it is.

Red test: src/utils/geo.test.ts FE-UTIL-GEO-002 failed on the unfixed code
with "AssertionError: expected NaN to be close to 20015.086796020572,
received difference is NaN, but expected 0.0005".

Removed tests: src/components/Studio/legKm.test.ts went with legKm. Its four
cases (zero for one point, a degree of latitude about 111.19 km, the same
both ways between 8700 and 8900 km, 0,0 to 0,180 equal to 12742 * PI / 2) now
run against haversineKm as FE-UTIL-GEO-001, 003 and 004.
2026-10-09 19:00:45 +02:00
Maurice 65a657347e fix(budget): a phone ticket split keeps the share of everyone a receipt line names (#1382)
On the phone, a ticket split where a receipt line named someone who was not
ticked as a participant saved only the ticked people as members. Their share of
the line was dropped, the member amounts no longer added up to total_price, and
the settlement kept a difference nobody could clear. The desktop editor had
been fixed for this in #1382.

Cause: useExpenseForm only added the people named on ticket lines when the
caller passed ticketShareMembers, and only the desktop CostsPanel did. The
phone MCostSheet kept the old behaviour.

Fix: the hook always sends everyone a ticket line names, and the
ticketShareMembers option is gone, so both editors save the same members.

Red test: src/components/Budget/useExpenseForm.test.ts
FE-BUDGET-EXPFORM-021 failed on the unfixed code with
"AssertionError: expected { name: 'Lunch', ... } to match object { ... }":
member_ids was [1] and members held only user 1, without user 2's 6.

Changed assertions: useExpenseForm.test.ts FE-BUDGET-EXPFORM-017 used to
expect the phone run (ticketShareMembers false) to send
members [{ user_id: 1, amount: 6 }] and member_ids [1]; that phone half is
removed and the test now expects every editor to send members for users 1 and
2 with 6 each and member_ids [1, 2].
2026-10-09 19:00:45 +02:00
Maurice 836a845b61 fix(planner): moving a booking onto another day clears the leftover drag data
Dropping a booking on the body of another day moved it but left the
shared drag data (window.__dragData) of an earlier place or map drag in
place, while dropping it on the end zone of the day cleared both. A later
drop with nothing of its own could then read that leftover place id.

The cross day booking branch of the day body drop now clears
window.__dragData along with dragDataRef, the same as the end zone. The
two clears are one chained assignment, so DayPlanSidebar.tsx stays at
the line count lint:size holds it to.

Red test: FE-PLANNER-DAYPLAN-236 in
client/src/components/Planner/DayPlanSidebar.dragData.test.tsx failed
with "expected { placeId: '77' } to be null".

No existing assertions changed.
2026-10-09 18:58:57 +02:00
Maurice 55e6b55f4d fix(roadtrip): the stop popup shows the answers of the stop it is handed, not the previous one
The desktop road trip stop popup read its kind, length, night and
check-in answers only when it mounted. When the planner handed the open
popup another stop draft, the popup showed the new stop's name with the
previous stop's kind, length, night switch and check-in, and saving wrote
those stale answers onto the new stop.

The phone sheet already seeds the shared useStopDraftForm again for
every draft. The popup now does the same in an effect keyed on the draft,
and seeds the night switch and the check-in time from the same draft.

Red test: FE-ROADTRIP-STOPPOPUP-034 in
client/src/components/Roadtrip/RoadtripStopPopup.test.tsx failed with
expect(element).toHaveAttribute("aria-pressed", "true") on the Charging
chip after the draft was swapped.

No existing assertions changed.
2026-10-09 17:56:27 +02:00