A foreign expense without a frozen rate is converted with today's rate.
When the server could not fetch rates itself, it counted the amount 1:1
in the trip currency, although the browser had the rates all along. A
VND bill on an AUD trip was settled as millions of AUD.
Such a row is now left out of the balances, flows, final budgets, trip
totals and per-person summaries. The settlement reports it under
`unconverted` and says which currency its figures are in; the trip
summary lists the ids and the budget prompt says how many are missing.
The browser lends the rates it already holds (`fallback_fx`) when it
saves an expense or a settle-up transfer, and to heal existing rows
through the new POST .../budget/freeze-rates route, which an editor's
Costs view calls for the rows the settlement left out. The server still
prefers its own rate, uses a lent one only while it is quoted against
the trip currency, and never replaces a frozen or explicit rate. The
settlement also takes the browser's display rate (`base_rate`) in place
of a display quote the server cannot fetch; the browser leaves out a
figure the query would refuse. The browser's own fetch gives up after
10 s.
Changing a row's currency while no rate is available no longer keeps
the old currency's rate; the row goes back to unfrozen.
MCP gets the same through the new freeze_budget_rates tool, which only
ever pins the server's own rate.
A euro trip read in dollars books a bill entered in dollars at the rate
of the day, from the euro's quote: 12,345.67 USD at 1.1398 is 10,831.44
EUR. The Costs screen and the settlement then turned those euros back
into dollars with the dollar's own quote, 1 / 0.87732. Frankfurter
rounds the two quotes separately, so they are not exact inverses, and
the bill read $12,346.05 the moment it was saved, a $250.00 one $250.01.
The error grows with the amount, which is why the large rows showed it
first. Paying back what settle-up offered for such a bill left the trip
open with the debt flipped the other way.
Both sides now convert trip money to the display currency with the trip
currency's own quote, the one every entry rate is frozen from. On the
server, settlement() fetches that quote for the REST route and for the
MCP get_settlement_summary tool, which now makes the same call, and a
legacy transfer without a currency reads back through the same factor.
On the client, useExchangeRates takes the trip currency as an anchor
and converts through its cached quote, falling back to the display
currency's while it is missing, so it works offline from cache and
needs no change to the settlement response. The final budget builds
each row from the unrounded trip amount, because a euro cent is worth
more than a dollar cent and 10,831.44 EUR came back as 12,345.68; the
balances and settle-up still net whole trip cents.
A same-day bill in the display currency now reads exactly as typed on
the row, the payer chip, the day header, the totals, Outstanding and
the final budget, on both shells and on the shared trip page, and
paying the amount settle-up offers leaves both balances at zero. When
the rate really moved, the row keeps the value the totals count, with
what was entered and the euros it was booked at underneath, "$801.76 ->
685,26 EUR". One helper builds that line for the desktop list, the
phone list, payments, the final budget breakdown and the shared page;
it prints nothing for an amount of 0 or a bill that reads as typed.
The expense editors share one module for their preview. The split
summary puts the share the row lends or borrows next to the entered
one, "$400.88 -> $390.53 each", and an expense saved without a currency
still opens in the trip currency. Both CSV exports add the amount in
the trip currency when it differs from the display currency.
Kept from before: the desktop totals recompute when rates arrive, a
booking's linked cost is labelled in the trip currency, a legacy
transfer is read in the display currency, the shared page reads the
frozen rate in whole cents, a booking import keeps the currency it was
quoted in at a rate frozen before the write (REST and plugin RPC), and
the MCP trip summary, the budget-overview prompt and the per-person
summary add every row up in trip cents at its frozen rate. The
packing-list prompt reads the trip title directly instead of building
the whole summary, which could wait on a rates fetch.
The phone timeline only put a travel-time connector between two places.
The drive from the arrival airport to the first stop after a flight is
part of the day's route and drawn on the map, and the desktop day plan
shows it under the flight, but on the phone the two rows sat on top of
each other. The drive from the last stop to the departure airport was
dropped the same way, and so was every drive past a booking saved
without a location, like a taxi to the airport or a concert between two
stops.
buildPlanRows now walks the rows the way the route calculation walks
the day: a located booking ends the drive before it at its departure
point and starts the next one at its arrival point, and each leg goes
under the row it leaves from. The route rides straight past a booking
it has no location for, so the drive through it goes under that
booking, as on the desktop, and keeps the mode menu of the stop it
left. Only transport bookings count as located, as in the route
calculation. Two bookings back to back still get no road between them.
A booking whose station is the stop itself, as the transit planner
saves a journey between two of the day's places, leaves a leg of no
length, and that one gets no connector.
On a day that opens with a flight, where the traveller also put the
hotel on the day as a stop of their own, the phone timeline started the
day with a hotel row and the drive from that stop to the next place,
above the flight, and then showed the same drive again in its own spot.
hotelLegsForDay guessed the bookends from coordinates: the first
segment leaving the hotel's spot was the morning leg. The route had
correctly drawn no morning leg on that day, but the stop on the
hotel's spot starts a leg there too, so that hop was taken for it.
The route calculation now tags the two legs it draws from and to the
accommodation, and the timeline reads that tag instead. The hotel end
of a tagged leg still has to be the day's own stay, because the
calculation keeps the previous day's legs until the new day has
routed. The stop to stop connectors leave tagged legs alone, so one
leg can no longer show up in both places.
The flight block in the road trip rail printed each fact two or three times: the booking
title in capitals with its duration cut off, the airport code three times per side, an
unlabelled 14:15 on the right with "Departure 15:15" under it, 17:20 twice, and a warning
"+9 h 39 min" measured against a check-in nobody could see, for a flight that was missed.
It now reads in four lines: the booking as typed; the two codes with their timetable clocks
and the ride between them; the places without the code in brackets; and the check-in as a
badge that carries its own state, the hour when the drive is on time, by how much when it
is late for the check-in, and "Missed, there at 23:54" when it gets there after the
departure, with the duration beside it. A missed ride edges the block and says so in the
day header, also when the day is folded. Trains and buses say boarding. The phone shows
the same, and its rows now read the lateness and missed-leave findings at all.
The stays at the edge of a day are named by the hotel, with the edge in a badge: Check-out
with the latest hour, Day start, Overnight, or Check-in with the hour the room is ready. A
plan that leaves after the check-out turns that badge instead of adding a line. They wear
the hotel stop's own disc, the stay's own stop shows its check-in the same way, and the
driving settings button shows when the stays are switched on.
The two-part badge the rail used in six hand-typed copies is one FigureBadge now, and the
airport code is stripped by one shared helper instead of two private copies.
The day dialog hangs from a fixed top edge, so switching to the delete
question only moves its bottom edge, and the question opens at its top
with the focus on Cancel. Cancel, Close and the quiet add share one
outline; the dated add is drawn in the accent instead of filled with it.
Dates in the dialog, its hints and the new-day toast read the way the day
list writes them, and captions use the muted text colour for contrast. A
stay booked twice is named once in the warning. On the phone the dated
pill shows only its date, so both pills fit side by side.
When the trip dialog asks both how bookings follow a moved start and
which days the new dates remove, the two now stand side by side in a wide
dialog instead of one long column.
Deleting a day no longer stacks a second dialog over the reorder dialog. The
dialog swaps its list for the question at the same size and in the same place;
Cancel and Escape bring the list back with the focus on the row asked about.
The foot is one row of normal sized buttons with the hint line above it, the
undated day is labelled "Without date" in every language, and the dialog is a
step wider so no label breaks. The list of what goes with a day is quieter: a
cancelled stay tints its whole row, a day with nothing on it is one plain line.
On the phone the add buttons are two pills under the scrolling list, and the
confirm sheet moves a long label to its own row instead of breaking it.
The question before a day goes now also says which day without a date takes over the last date, which stay running across the day loses a night and when it checks out, and what the cancelled booking costs, or that it has no expense at all; further bookings on the same stay are counted. Road trip boundaries follow their own days when the numbering still has a hole from the old delete, and ones left on no day are dropped.
A long list scrolls inside the confirm card on both shells with the buttons always in reach, Escape closes only the sheet on top and takes the trip dialog back from its review step to the form, and the panel of a deleted day closes, also when a fellow traveller deletes it. Undo steps on a deleted day are dropped, a restored place skips days that are gone, and an undo that fails says so. The day cache is written without holding up the answer once the server has it, the tip under a new start date points at the dated add button, and two Collab tests find the confirm button by its role instead of an old colour class.
The trip dialog on the desktop and the trip sheet on a phone now ask before a save whose new dates remove days with something on them. The question names those days the way the day list does and lists what goes with them in the same rows the delete day question uses: planned places stay in the place list, notes and day texts are deleted, bookings stay under Bookings, and a stay that checks in or out on a removed day goes as a whole while its booking and expense remain, shown in the danger tone. A moved start gets both questions in one step, with the booking hint following the chosen shift mode, and a save that only drops empty days asks nothing.
The rule that lays the days out moved into shared as planDayGrid, with resolveDayGridRange deciding whether the rows are rebuilt at all. generateDays on the server now carries that plan out in one transaction, so the warning names exactly the days the save takes, and a seeded fuzz test holds the new rebuild to the old one over 300 random day grids. The update_trip tool says in its description what shortening takes and lists removed_days in its answer.
Alongside, the dialog sends a day count only when it changed, so a stale count no longer trims a day just added, stays are reloaded after every date change including the one made on a phone, and the date shift tip now points to adding a day under Reorder days.
On a trip with dates the reorder dialog and the days sheet on a phone now offer two ways to add a day: a day without a date at the end as before, or the calendar day after the last date, named on its button, which extends the trip by one day. The new day goes right behind the last dated day, so the days without a date move one place back with their plans, and no existing day or booking changes its date.
The server does it in one transaction behind a new dated flag on the day create contract, used by REST, the create_day tool and the plugin RPC alike. It moves the road trip boundaries of the days behind the new one, and announces the day with day:reordered and the grown trip with trip:updated; an insert at a position announces its trip now as well. The next date and the day limit come from one shared rule, so the button never promises a date the server would not give.
On the client the add is online only and runs one at a time, so a double click cannot grow the trip by two days. A wiring test that picked the stay question by its position now finds it by its handler, since the delete question sits in front of it.
Every row of the reorder dialog, and of the days sheet on a phone, now has a delete button. It first asks, and the question lists what goes with the day: planned places stay in the place list, notes and day texts are deleted, bookings stay without a day, a stay that checks in or out on the day is cancelled with its booking and expense (shown in the danger tone), and on a dated trip the later days and their bookings move one date earlier or the trip ends a day earlier.
The server side moved from a bare DELETE into DayRemovalService, which REST, the delete_day tool and the plugin RPC all use. It closes the numbering, keeps the dates on their positions like a reorder, re-stamps the moved bookings, shifts the road trip boundaries, cancels touching stays through the accommodations domain, refuses the last day of a trip, and fans out day:deleted, day:reordered and trip:updated so collaborators renumber and see the new day count and end date. The trip query moved into a leaf file so the days domain can read it without a module cycle.
On the client the delete is online only, runs optimistically through the days slice with the same reducer collaborators apply, and the ConfirmDialog gained a slot for extra content, an Escape that only closes itself, and the danger and accent tokens.
Since 4.3.0 every lodging booking types its place as 'hotel', and 'hotel'
is one of the road trip stop types. With "Show in Days too" switched off,
Days dropped every such place from the trip's place list, so a hotel
vanished from the sidebar, from the booking's own place picker and from
the Days map the moment it was booked, and a click on it in the day list
found nothing. Nothing was deleted: road trip mode kept showing it.
Days now leaves out only road trip stops nobody booked. A lodging place
stays, and so does any place a stay or a booking points at, on the
desktop and on the phone. Trips planned on 4.3.1 show their hotels again
without any change to their data.
An imported GPX or KML track is no longer offered as the place of a stay,
in the booking form, the day's hotel picker and the phone's sheets. A
stay that already names a track keeps showing it.
Takes back the editable price, currency, website and phone on collection
places from 0df82185b and its review follow-up in a910c2a83. Costs are kept
inside a trip only; a collection does not carry them, and #2471 is a
feature request rather than a bug. The collection files are back to how
they were before, including the dimmed save button from f16698bdb, which
only mattered for the price field.
Saved place price (#2471)
- A price above zero that would be left without a currency is refused
with a 400, over REST and through update_collection_place alike. A list
has no base currency, so such an amount was read in each viewer's own
default currency and went into a trip as the trip's. A price sent alone
keeps the stored currency, and a zero price or clearing both fields
still goes through. The check sits in the service because the MCP tool
spreads the contract's shape, where a refine would never arrive.
- An older row that holds an amount without a currency shows it as a
bare number instead of in the viewer's own currency.
- The Hungarian hint uses the informal register of its file, which is
now prettier clean.
Immich certificate switch (#2475)
- The switch trusts one server. Saving a different URL without sending
it turns it off; saving the same URL keeps it as before. The wiki says
so.
Moving day (#2476)
- Google Maps and CoMaps are left out only when there is nothing to hand
over. A day with a single stop opens it as a pin again, on the desktop
and on the phone, as it did before.
- Desktop: a moving day whose flight or train has no stations shows no
route tools, since there is nothing left to draw. With stations they
stay.
- Phone: the drive between two stays on a day without stops or bookings
shows once at the top instead of again at the bottom.
- The export helpers take the carrier flags as one object instead of two
booleans in a row.
Import card (#2477)
- The success and error marks use the theme tokens.
- The e2e test names its upload with a made-up booking number.
Tests: COLLECTIONS-SVC-132 and 133, COLLECTIONS-E2E-088,
update_collection_place with a price and no currency, FE-COMP-COLEXTRAS-013
(011 adjusted), IMMICH-TLS-010, IMMICH-103 extended,
FE-PLANNER-DAYPLAN-222 adjusted and 224, FE-MOB-PLTL-051, FE-MOB-PTLM-047,
FE-W4BGT-023 and 024 check the mark colours.
A saved place always had price, currency, website and phone columns. Saving
from a trip, the file import and the copy into a trip all carried them, but
neither editor showed them and the update contract did not know them, so the
validation pipe stripped them from a PATCH and the MCP tool refused them as
unknown keys (#2471).
- shared: collectionPlaceUpdateRequestSchema takes price (not negative,
nullable), currency (three letters, trimmed and upper-cased), website
(http or https, as on save) and phone (trimmed, capped at 60 like the file
import). The save schema is unchanged.
- server: updatePlace writes the four fields inside the existing
transaction. update_collection_place spreads the same shape, so REST and
MCP take and refuse the same values; its description now names the price
and asks for the currency with it.
- client: the desktop detail sheet and the phone sheet edit price with a
currency picker, website and phone, and show them in read mode (price only
above zero, phone as a call link, website as a link chip through
safeHttpUrl). Both shells share one hook, useCollectionPlaceExtras, which
sends only the fields the user changed and always pairs a price with its
currency, since a list has no base currency. An amount that does not parse
marks the field and holds the save.
- i18n: collections.price, collections.priceHint and collections.phone in
all 23 locales; the website label reuses places.formWebsite.
- wiki: Collections and MCP-Addon-Tools name the new fields.
Tests: schema spec, COLLECTIONS-SVC-128 to 131, controller pipe parity,
update_collection_place with the new fields, COLLECTIONS-E2E-085 to 087,
FE-COMP-COLEXTRAS-001 to 012, FE-COMP-COLDETAIL-049 to 054 and
FE-MOB-CPLSH-042 to 046.
The share dialog stored the pick as String(value) while its options are
keyed by numeric user ids. CustomSelect looked the pick up with a strict
comparison, found nothing and left the trigger on its placeholder, although
the Invite button was already enabled and the invite went through. The
phone opens the same dialog from the Mehr sheet, so it showed the same.
CustomSelect now compares the held value and the option values as text, so
a caller holding "5" finds the option keyed 5 and the other way round. A
held null or undefined still matches nothing. onChange keeps handing back
the option value unchanged. No other caller mixes the two kinds today; the
place form had worked around the same trap by keying its options as
strings, and its comment now says why the string stays.
Tests: CustomSelect matches across both kinds and marks the pick in the
list, the desktop dialog shows the picked name and clears after the invite,
and the phone share sheet does the same through the real dialog.
Refs #2478
A day that checks out of one hotel and into another, with nothing else
planned, falls back to a drive from the first hotel to the second
(#1297). A flight, train, ferry or coach booked on that day is the move
itself, but one saved without its stations leaves no waypoint behind, so
the day map, the trip overview and the phone plan all drew the full road
between the two hotels, and the phone showed it above and below the
flight (#2476). With its stations the day was already right.
- The fallback drive between the two hotels is no longer drawn when any
such booking is on the day, located or not. A day without a booking
keeps it. No line beats a wrong one.
- The phone plan takes the evening hotel leg from the last drive that
reaches the hotel, not the first, so an earlier drive to the same spot
no longer stands in for the one that closes the day.
- Google Maps and CoMaps exports of such a day, which could only ever
describe that same drive, return nothing, and on the desktop and the
phone the two buttons are left out whenever fewer than two stops
remain, instead of doing nothing on a tap.
A hotel stop planned by hand ahead of the flight already sorts behind
it through the ride seating of #2474; a test now pins that for the days
view as well.
The check-out row showed the room's last hour and, beside it, the time the
plan leaves the hotel, which can be hours later when a place further on is
pinned. The row now says "Leaves after check-out" when it does, on the
desktop and on the phone.
A click on a road the day drives twice, out of the hotel in the morning and
again between two of its own places, went to the first pass, the drive out
of the hotel, and was refused. The second pass now takes the via, and where
yesterday's drive into the hotel and today's leg share a road, today's leg
does. A road only the hotel's drives use still takes none.
The wiki says both.
A hotel row now takes its position and name from the trip's place instead of the stay
row, which the planner fetches again only when a stay is edited. A moved pin no longer
seats the hotel at its old spot while the server plans from the new one. The drive out
of the morning hotel goes the way the first place is reached from it, the mode Days
draws that leg in, and it is set in the shared rule so the browser and
calculate_roadtrip agree.
On the rail, a hotel row that took in the morning marker starts the line. A day that
only drives from one stay to the next shows no stop count and takes a dropped stop as
its first. The hotel keeps its pin on the desktop map, and the switch explains itself
under its label and to screen readers. On the phone, the note about figures set on the
desktop now sits above the switch it does not cover. The hint and the refusal on the
hotel's drive say what they mean in all 23 languages. The via tools and the wiki say
that a via into a booked night is kept but not used, the wiki states the overlap rule
Days applies, and the arrived-from branch of the via anchor has tests.
A trip can now switch on roadtrip_hotel_bookends, a shared driving preference that is off
until somebody sets it, so every trip drives exactly as before. Switched on, the planner in
the browser and calculate_roadtrip on the server seat the hotel slept in at the start of the
next day and tonight's hotel at the end of the day, both through seatNightBookends over the
trip's stays in id order, with the earliest booking linked to each. A linked booking changes
the hotel row and asks the router nothing.
The switch sits in the desktop driving settings under the route line, and the phone's
driving figures carry the same switch as the one control they offer; both write through the
planner's own preference save, so a flip offline queues like any other. get_roadtrip_context
reports the stays, calculate_roadtrip previews the switch through its settings, and the
settings, corridor and day boundary tools say what it does and that missing means off.
The drive learns what a booked night at the edge of a day is, while nothing seats one
yet. One rule in shared, seatNightBookends, picks the stay a day wakes up in and the one
it sleeps in the way Days does, over real nights and in id order, and puts each hotel at
the edge of the day as a stop of its own that belongs to no assignment.
The schedule, the day window, the spill into the next morning, the sums and the connected
days read a night spent at one hotel as a leg going nowhere, never cut or postpone the
drive to tonight's hotel, and pass the hotel over wherever a stop is looked up by its
stored index. That lookup now also puts a place added behind a terminal where it fell.
The rail and the phone chain draw the hotel as a row of its own that opens the booking or
the stay, offers no other ways and takes no via, in every language. Until a trip switches
it on, every plan is the one stored.
The trip's places went into the field's search results as they were. A place
without a pin was picked as 0,0, because Number(null) is a finite 0, and a
highlight left over from a longer search could point past the shorter list
and throw on Enter. The phone's form, the train stations and the car stops
were not offered the places at all.
The places are now prepared once per form (named, with a real coordinate,
each once) and handed to every location field of the manual tab, on the
desktop and on the phone. The field shows them while it is empty or holds
fewer than three characters, narrowed by what was typed, and derives that
list rather than writing it into the search results, so a late search answer
cannot replace it and a place already picked does not reopen it.
The slot the desktop stores for a ride within one day was worked out over the rows the day
list shows, so a hotel it hides on the far shore was left out, the ferry was filed behind
it and the road trip drove there overland before the crossing. It is now worked out over
every stop the drive reads, timed the way the drive times it. An overnight ride no longer
seats its arrival by the slot seeded on the day it left, and opens the day it lands on
unless somebody placed it there. A via behind a day's last stop goes once that stop is no
longer last, for the rail's drags and removals, the list's reorder and the server's sorts
and seated nights alike; some of them used to bend a leg of the day through it instead.
A leg OSRM drew while the avoidance router did not answer is now known as such: the picker
no longer reads it as that router's road, has the leg routed again when its own road is
chosen and says so, and a join that fell back is asked for again and flagged on its card.
A refused way that leaves a class out names the setting that drives it, the ferry hint
covers a crossing that lands the next day, a road another engine timed names that engine,
and the desk bar keeps the keyboard on the chip while a choice is checked and announces
the outcome. A ride on none of the trip's days is no longer said to have no date.
A ferry between two stops without a clock was seated by the clock alone, which files a
ride with nothing timed before it at the end of the day. The desktop day list stored that
slot, and the road trip drove overland to the far shore and back to the pier before the
crossing. A ride that leaves and lands on the same day, with both terminals located, now
takes the seat between the clocks that adds the fewest straight-line kilometres. One rule
in shared seats it for the day list, the slot the desktop stores, the phone and PDF lists
and the road trip in the browser and on the server. Slots already stored are left alone.
A flight, train, ferry, cruise or bus on no day made no seam and said nothing, while the
map still drew its arc. The rail and the phone tab now name such a booking with a way to
open it, and calculate_roadtrip lists it under undatedRides.
The drive from where one day ended to where the next begins was the one stretch of a
connected road trip that offered no other ways. The band above a card's first stop had no
button on the desk, and the phone did not show that drive at all. A via behind a day's last
stop, where the map already files a point dropped on that drive, was deleted when the day
was reordered under Days, and was left on no stop when that last stop was removed or
dragged up the day. Switching the avoided road classes or the day's profile kept the old
road and minutes on it until a reload.
The drive in now opens the same picker as any leg, on the desk band and on a new row at
the head of the phone's chain that names where it leaves. A choice is proven with the
seam's own router and written behind the last stop of the day before, also when a day with
a single stop lies between. The rule for that via lives once in shared (carriedSeam): it
stays with the last stop while that stop stays last, and is read like any other once it is
not, for the planner's drags and removals as for the server's sorts and seated nights. The
cached answer for a seam now records its mode and avoided classes, so changing either asks
for it again.
The picker of other ways asked OSRM with nothing avoided while the rail drives a trip
that avoids a road class through Valhalla, so the list had no current road, called a road
the rail was not on the fastest, and choosing it changed nothing. A choice was stored as
one via where the offer strayed furthest, without asking whether the router then drove
it: a point on a ferry was pulled to the pier and the drive went overland, and a way
weighed away from motorways kept the motorway after its one point. An OSRM answer standing
in for a Valhalla that did not respond was cached as the avoided road, unmarked.
The rail now hands out a router per leg with the leg's own mode, avoided classes and
engine, and its days carry the line of every leg. Offers come from that engine, and the
rail's own leg always heads the list as Current. A choice is pinned and proven against
that router with at most three pins before anything is written; a way it will not follow
is not saved and says why, with a hint when it crosses by ferry, and a leg that changed
meanwhile or a device offline writes nothing. The OSRM stand-in is filed under the plain
key only and flags the day as not avoided.
With "Blur booking codes" on, the bookings list and the transport details
hid a code, but opening the same booking to edit it printed the code in
the clear: the reservation and transport forms with every per-segment
code of a flight or train, the hotel editor in the day panel and the
three phone sheets. The stay's own code in the day panel, the booking
strip in the place inspector and the booking lines under a stay and a
stop on the phone showed it plainly as well.
One shared BookingCodeInput and BlurredCode now carry the setting to all
of them. A filled code field stays blurred until it is hovered or
focused, so clicking, tapping or tabbing into it still reveals the code
for editing, and the value is saved exactly as typed. With the setting
off nothing changes, and the PDF export and the share pages stay as
they were.
A booking switched on with "On map" stayed drawn on every day of the
phone's plan map, so a flight from the first day crossed the map of the
third, although a day chip frames one day and drops the other days' pins.
The plan map now draws such a booking only on the days it runs on, the
arrival day of an overnight one included, and the all-days view still
shows every booking that is switched on. The switch itself stays
trip-wide, and the desktop, collections and the road trip stage draw
exactly as before.
"On map" in a booking's sheet now reads as on only where the plan map
draws the route. From any other day a tap takes the map to the booking's
day, switching the route on when it is off, instead of hiding a route
that is not drawn there anyway.
The phone settings also gain the "Always show booking routes" switch the
desktop already had. As on the desktop it sets the default for trips not
yet touched on this device, so a trip where a route was switched on or
off by hand keeps that choice.
Ride minutes and late marks follow an edited timetable instead of the routing
that fetched them; a landing day's join band reads as the ride it is rather
than a nine hour drive of no length; the refuel offer is there on the road
out of a terminal or a pick-up desk; the phone's stage map draws the ride;
the share page numbers its stops the way the planner does; the PDF lists a
day the way the day plan lists it, from both shells; a poster-less clip draws
its play badge everywhere, not only in the editor; the hero title has room
for its descenders and the list card keeps its badge clear; the release
notice keeps its close control in reach when it stacks; a link chip's actions
are thumb-sized and its delete asks first; the Google retry line and the
Google-only switch appear only where Google answers.
Registering an OAuth client by hand and getting only "Failed to register
OAuth client" left nothing to act on: the server names the rule it broke (a
scope, the ten-client cap, a redirect URI it will not take) and the toast
swallowed it in a bare catch. Both shells show the server's message now and
keep the generic text for a failure that carries none.
With places set to "Trip owner" a member who may edit days still saw Edit and
Delete on a place in the day plan's context menu, the inspector's action row,
its inline rename and the phone timeline's edit circle, and every one of them
ended in the server's 403. The day plan gates the two entries on place_edit
(taking a place off the day stays a day right), the inspector drops its edit,
delete and update handlers without the right the way the places pool already
does, the phone timeline offers the pencil only with it, and openPlaceEditor
and handleDeletePlace guard like their siblings.
Closes#2446
A plugin declaring the reservation-detail slot mounted at the foot of every
card in the desktop ReservationsPanel and nowhere else: the phone's transport
and booking cards never rendered the slot, so on a phone in portrait the
reservation was there and the widget was not, and turning the phone sideways
made it appear. ReservationPluginSlots mounts one PluginFrame per widget under
each card, scoped to that reservation with surface detail-slot, outside the
card's body button and gone in compact mode.
Closes#2440
Every file the release touches was read once more, and what survived an
independent check against the code is fixed here, with a test for each.
Data and credentials
- A document sync connection edited to a new address no longer carries the
stored token along: a blank form under another origin is refused, the same
rule the probe route already had. Dawarich and AirTrail testConnection send
the stored key only to the host it was stored for.
- The key rotation script now covers document_connections.secrets and
trip_document_links.webhook_secret, which it had skipped.
- Turning a booked night into a pause in the road trip popup asks first and
says that the booking and its expense go with it. Deleting a place says the
same when a night is booked there, on the desktop and on the phone.
- A moved version marker over unchanged bytes no longer replaces the TREK file
and its attachments; a real revision carries the attachments over.
Planner
- A drop in the day plan names the row it lands ahead of in the full day, so a
hidden hotel stop no longer shifts it one slot early; the store sorts before
it splices. Via re-anchoring, the booked-night guard and the sidebar's day
membership read the stored day, the index space the server uses.
- A hotel stop placed mid-day re-pins the road trip vias of that day, and the
booking session receives the new stop order too. The Public API and the iCal
feed list a booked hotel once. A plugin can no longer write a stay against
another trip's day.
Road trip and map
- The route overview paces its requests to the public routers and retries a
rate limit, and it says which legs it could not route instead of quietly
reporting a shorter trip. A click on a via handle no longer drops a second
via; handles hide when zooming out. Read-only members cannot edit the driving
limits. The phone shows a way back when no day is picked and prints the night
drive departure in the chosen clock format.
Integrations and search
- Google transit keeps conventional trains when the train chip is on.
TREK_PLACES_ENABLED=false stops the details and enrichment calls too, and
accepts the whole boolean family. A caller-supplied description no longer
lands in the instance-wide enrichment cache. Nominatim searches carry a
timeout. An OSM place id is checked for shape before it reaches Overpass.
- Dawarich puts a stay on the trip whose dates hold it, the Atlas hand-off
knows what the Atlas already shows, and a host change with a new key clears
the old instance's state.
- Document sync: a webhook address pasted by hand works on the token alone,
deleting a connection unregisters its subscriptions, Nextcloud under a
sub-path connects, Synology answers a name collision before uploading and
verifies a move into the bin, Paperless titles are capped at 128 characters,
a DNS failure reads as unreachable rather than as a policy verdict, videos
from the store are accepted like uploads, and remote_missing has a way out.
Conflict buttons only for the owner, a refused choice is reported, a refused
credential gets a Reconnect button, disconnecting asks first on the desktop
as it does on the phone, and the folder picker no longer spins forever.
Release
- The server holds the release notice back from a bundle that cannot draw it,
so the previous shell served from the service worker cache after an update
no longer shows bare keys and no longer lets a click use the notice up.
- Import into an existing list gets the same body limit as a new list. Chat
images say when the limit is reached and the new controls are translated.
The transit backend switch reports a failed save. The Helm chart carries the
Amap credentials in its Secret; the shipped templates document the Places
API and Amap variables and the Overpass default. The wiki follows.
A trip longer than a year stopped at day 365: generateDays capped the day
rows at MAX_TRIP_DAYS while the trip kept its full end date, so the last
days had a date but no day to plan on, in the day list, the transport and
booking dialogs and the phone's date pickers (#2403).
The limit is MAX_TRIP_DAYS in @trek/shared now, 999 days, shared by the
server, both trip forms and the MCP tools, and a range past it is refused
rather than cut short: 400 on REST, an error result on MCP, bad params on
the plugin RPC, and a message in the forms before anything is sent. The
check runs before the trip row and the budget are touched, also catches a
start date moved past the stored end on its own, and holds a day_count
rebuild to the same bound. A trip that already carries a longer range can
still be renamed. The MCP-only 90-day ceiling on create_trip goes with it;
it clipped dated trips the same way.
Closes#2403
The connector menu on the phone listed only the road profiles, so the
transit search could not be opened for a single leg there. It now carries
the Public transit entry whenever the trip has dates, seeded with the leg's
two ends and the departure of the stop it leaves, and the transport sheet
passes that departure on to the search.
The prefill is built by one module the desktop day plan now uses as well.
Closes#2398
A visit's End could be edited and was shown in road trip mode, but the
schedule never read it: the drive always left a stop after its stay, so
a traveller who knew when they had to leave but not how long they would
stand there had no way to say so.
The End is now the time the drive leaves. RoadtripStop.leaveAt is
filled from the visit's End (or its place's) on the client by
useRoadtripRoutes and on the server by roadtrip-plan.service, so
calculate_roadtrip and get_roadtrip_context agree with the planner, and
computeSchedule takes it as the departure it already knew how to hold.
With Start and End both set the stay is the time between them. With
daily travel times on, the stay is the time until then, and an End
before the window start starts the day early the way a Start does.
Before the first fixed time the chain works backwards through an End as
it does through a pinned Start.
An End is read against the drive: the last passing of that clock time
counts as missed when the drive went past it on the road or earlier
the same day, anything else is the next one, so a drive across
midnight neither waits a whole day nor pretends to be on time. A drive
that gets there after the End leaves on arrival and files a
missedLeave finding, shown beside the late badge in the rail and on the
phone.
The rail's STAY badge, the inspector's Stay tile, the phone stop sheet
and the phone chain show the stay the End makes, with an "until 14:00"
hint. It is read off the schedule and never written to
duration_minutes: the stay belongs to the place, the End to one visit,
and a stored copy would go stale as soon as the arrival moved. For such
a stop the stay dialog, desktop and phone, shows that stay read only
and offers to remove the End, through the visit's time route with an
offline path (assignmentRepo.setTimes, setAssignmentTimes). While the
road trip is on, the End field in both place forms says the drive
leaves at that time.
The dead checkoutAt in the plan service is gone. New strings in all 23
locales.
The custom image control on a saved place's cover sat in the top left
corner, on top of the category chip, and hid half of it. It also looked
like nothing else on the cover: a square dark button next to a round
translucent close button.
The camera and the remove image button now sit in the top right corner
next to the close button, camera directly left of the X, remove further
left when there is a custom image, all three in the close button's
round translucent look with its hover, phone size and focus ring. The
phone sheet had the same overlap and gets the same row.
The category chip and the controls share one header, so neither can
run under the other: the controls hold the end and never shrink, and
the chip ellipsizes a long category instead of sliding under the
buttons in the narrow docked detail. On the phone the header is in
flow, so a place without a category starts its name below the buttons.
Picker, tooltip, labels, the spinner while an upload runs and the
removal control behave as before. The trip planner inspector keeps its
avatar overlay; it never had the overlap.
The Documents addon put a "Document sync" button on every trip's Files
tab, and the seed ships every provider switched off, so after the
upgrade each of those buttons led into "No document providers are
available". The button now shows on a trip that is already bound, to
every member, and otherwise only to somebody who may bind the trip on
an instance that offers a provider. Without it the Files toolbar is
the one from before the feature, trash flush right, on both shells.
/api/addons carries no document providers for a normal user, so the
answer comes from the docsync routes through a new online-only
docSyncRepo and is kept per session in docSyncOfferStore. A revisit
renders the last answer at once and checks it again; offline nothing
is asked and the last answer stands. Creating a binding, removing one
and removing a connection now ping the trip with docsync:changed and
zero counts, so a member already on the Files tab sees the button come
and go without leaving it.
Both shells keep listing a binding whose provider the admin has since
switched off instead of claiming there are no providers, and name its
store through one storeName() helper. A load that answers 403 or 404
clears the bindings as well, so they do not linger once the Documents
addon is gone.
The phone admin screen gets the Documents shelf the desktop one has:
the document providers as sub rows under the addon, toggled through the
same admin route and re-read after Documents itself is toggled, because
the server switches the providers off with it.
isTripOwner and the isOwner prop become canManageSync and canManage,
which is what canManageDocSync answers. Both folder pickers take
loadScopes out of the sync object before their effect, so
exhaustive-deps is satisfied without the effect running on every
render.
The Up next card stopped printing a fixed key when the audit replaced the
nonexistent places.navigate with the single navigation target's own name, so
the assertion went looking for text that no longer exists and took the 4.3.0
branch red. The link is found by aria-disabled now, which is what the test is
actually about.
**"Today" was read off UTC while the clock beside it is local.** `localDate.ts` exists for
exactly this and says so in its own comment: between local midnight and the UTC rollover the
two disagree, so east of Greenwich the up-next card hung on yesterday's stage for the first
hours of every night, which is when somebody on a long stage is actually driving. Both the
stage hook and the corridor hook now ask the same helper the rest of the app asks.
**A refused day end left the switch showing a state the trip never reached.** The writer
catches its own failures and shows a toast, so it never throws and the sheet's catch could
not fire, which left the optimistic value standing. It answers whether the day end moved
now, and the switch comes back when it did not. The desktop control ignores that answer, as
it reads its state back from the trip.
**A timed-out corridor search reported more unsearched boxes than it had.** Every worker
still alive ran the deadline branch and each added the whole remainder, so three workers
reported three times the boxes left, which can exceed the number of boxes there ever were.
The remainder is claimed before it is counted, which also ends the other workers' loops.
**A night that fell mid-leg had its morning drive dropped from the stage map.** The join
mark went on the first leg of every day opening on an automatic night, but only a night
standing ON a stop is a connection between two days. `dayWindow` records that difference as
a whole `position` against a fractional one, and where it is fractional the morning drive is
the last stretch to this day's own first stop. It was being filtered off the stage while its
distance stayed in the day's total.
`mobileTrip.rtTruncated` interpolated a count into a sentence naming the stretch once, so at
one truncated area it read "1 stretches". Two keys now, picked by the count, the shape this
codebase already uses where a number changes its noun.
Writing the singular turned up three plurals that were wrong at counts the old one-form key
could never reach. Polish and Czech both had the genitive plural with a singular verb, which
is right at five and wrong at two; Arabic had the 3 to 10 form, wrong at two and again from
eleven. All three now put the count after a colon, where no form has to agree with it.
Russian and Ukrainian were already correct at both, since their phrase leads with a
locative, and were left alone.
The eight languages that keep the singular noun after a numeral differ only in the numeral,
which is what they should do.
**A saved stay left an invisible sheet standing, and the day swipe stayed dead behind it.**
The stay sheet reopened 'rtstop' by spreading its own payload, which carries a place id.
That sheet is located by day and assignment, so it resolved to nothing and drew nothing,
while `shell.sheet` still pointed at it and `menuOpen: !!shell.sheet` kept the day swipe
blocked until something else closed it. The row rides along now, both ways, and with no row
to go back to the sheet closes rather than opening one that resolves to nothing.
**"End the day here" showed off for a day that already ends there, and switching it on
deleted that day end.** Two things close a day: the stop's own flag and a manual boundary
filed against it. The writer has always known about both; the phone switch read only the
flag, so the tap ran through the writer's boundary branch and cleared the boundary. The
question is asked once now, in the planner beside that writer, and the desktop reads the
same answer it always computed inline.
**The road trip chain printed 24-hour clocks at a reader on a twelve-hour one.** The model
hands clocks over as HH:MM, which is what the schedule computes in, and the chain, the head
card and the up-next card printed it straight. The desktop rail, the day timeline and this
tab's own sheets all honour the setting; these go through the same formatter, carried in the
row chrome so no two rows can disagree.
**The up-next card's navigate button read "places.navigate".** No locale has that key. It is
`inspector.navigation`, which is what the stop sheet beside it, the place inspector and the
place sheet all use, with the single-target label when there is only one.
**Offered roads were invisible on a satellite basemap.** The imagery is anchored under the
first layer TREK draws, found by prefix, and the roads offered for a leg are added before
the route source so they sit under the current route. That also puts them before the first
`trip-` layer, so the raster went straight over them. The prefix list knows them now.
**A journal grew a day section for every day Dawarich recorded, all year.** The hook reads
every open stay the account has, and the timeline took all of their dates. A week in May
therefore sprouted sections from September. Bounded by the journal's own span now, its
entries together with the dates of the trips it links, and with no span at all it offers
nothing rather than everything.
The moon pill marking where a travel day ends is back on the phone, filtered to the day
on screen. The markers are built from the whole drive, which is right for the desktop
rail and for the all-days view, but a stage draws one day: unfiltered, day 1 carried day
2's pill as well, sitting in the middle of a map that is otherwise entirely day 1. Two
ends on a map that draws one is a question rather than a label.
Off a stage the list is left alone, because in the all-days view every night does belong
to a line that is drawn. The plan tab is unaffected either way: its vias come from the
route calculation, which has no night pauses in it.
This is what the map lost when the pills were dropped altogether: on a day whose drive
ends somewhere other than its last stop, the pill is the only thing that says where.
The desktop rail turns a place on the drive into a service stop by its own disc: click
the 3, pick the pump, and the 3 becomes an orange disc while everything below it
renumbers. The phone chain drew the same disc but it did nothing, so the only way to
mark a petrol stop was to open the trip on a computer.
The disc is now the control here too, for the same reason it is there: it is exactly
what changes, so it is its own preview. It opens 'rtkind', a sheet rather than the
desktop's popover under the disc, because a palette aimed at an anchor would land under
the thumb that opened it. The kinds stay discs rather than seven rows of words, each
named for a screen reader. A second tap on the kind a stop already is takes it back to
a destination, and once there is something to undo the sheet says so in words too.
It writes through planner.setRoadtripStopKind, the very call the rail makes, so the
chain redraws off the store with no second code path. Without place_edit there is no
handler and the disc is a plain disc, not a disabled-looking control.
A leg pill with no figures in it is fixed on the way past: a segment that reaches the
chain before its routing round has landed carries metres and seconds but empty texts,
and reading only the texts printed the template with both slots empty, so the pill read
" in " and said nothing. The numbers are formatted when the texts are missing, which is
what the rail does with the same segment, and with neither the leg says it is pending.
It named the day's destination with its picture, its stop count, its arrival and the
stage distance, and it stood there for the whole of the road trip tab. The day and
the distance are in the shell's stage header already, and on a map that fills the
screen a permanent card over the bottom edge costs more than the three facts it
added. Tapping it opened the destination's stop sheet; the chain opens the same sheet
from the row that names it.
The slot stays for the one bar that earns it: while other ways of driving a leg are
on offer, the picker's bar stands there. That wrapper is now mounted with the bar
rather than lying empty over the map's own buttons the rest of the time, and the map
floor lifts only while it is up, so on the stage the round controls stand on the dock
like everywhere else.
A final pass over the feedback commits found two gaps in the phone map.
The planner keeps a point it was asked to show until its next routing
round, and once the fuel offers or the other roads for a leg close it hands
that very array back. MMapArea read it as a new focus, so closing either one
flew the camera to the stop "Show on map" had pointed at, on another day's
stage too. holdFocus now remembers the arrays it already held and only
takes a new one as a focus, with the open picker's own roads as the one
exception.
The all days view still took its pins from the planner's planned list,
which on a phone leaves out booked nights and hidden service stops, so the
drive ended at a hotel with no pin to open it from. It now reads the places
of the stored visits, like the stage does.
The desk offers alternative routes from the drive band between two stops;
the phone had nothing. A leg in the phone chain now carries one round
button that asks the router for other ways and switches to the map, where a
bar takes the stage bar's place: the ways as chips with their colour, time,
distance and what sets them apart, and a confirm button. On glass there is
no hover, so a tap on a chip or on a line only previews that road; confirm
writes it. The button shows only where the desk offers it (a routed leg
between chosen stops), for someone who may edit the day, and not offline,
since vias are written online only.
The picker could not have stayed open on a phone at all: the planner closed
it whenever road trip mode was off, and that mode is off on a phone by
design, so the next render shut it. The close now follows where the picker
can be seen (road trip mode on the desk, the road trip tab on a phone), and
useRouteAlternatives returns a memoised object.
The rules both shells need are shared: legReroutable in the row model,
alternativesPhase and alternativeSubline next to the overlays, and the desk
bar and sidebar read them too with no visible change. Both map engines take
an optional fitPadding, so the phone frames the leg between the day chips
and the bar instead of under them.
The credit button sat in the middle of the round controls band, beside the
locate button. On the trip map it now has the bottom right corner to
itself, a gap above the dock (or above the stage bar on the road trip
tab), and the band floats one credit row higher, so the switcher, the
compass and the locate button stay on one line.
MMapArea marks its layer as a credit corner and defines the floor the map
ends at; mobile.css places both GL engines' compact control there, keeps
an opened GL credit's button in the corner, moves the Mapbox wordmark into
the same row instead of under the compass, and unfolds the Leaflet credit
to the left of its button. MapAttributionToggle reads the same two
variables with today's values as the fallback, so the other phone maps and
the desktop keep their placement.