Auto layout wrote journey data straight into the document without holding
it to the caps the contract sets, and the save path sends the document
unvalidated - so the route refused the whole body with a 400 and the
editor reported "couldn't save" for the rest of the session, because
autosave kept offering the same rejected book.
The everyday way in was the itinerary list: thirteen stops a page over
two pages is eighty elements on a spread capped at sixty, so a run of
twenty bare stops was unsavable and a longer one put two spreads over.
Sixty was below what this app's own layouts draw, which makes it a bug
with a constant in front of it rather than a limit, so the cap goes to
ninety and the page keeps the design somebody chose. It stays a
serialisation limit: the largest document the contract now admits is
under four megabytes against the book route's eight.
A long journal entry, a long place name, a journey past eighty countries
and a long title each did the same thing on their own, and a bound story
did it on open without anyone touching Auto layout. The caps become named
exports on the contract and every producer builds against them, so the
two cannot drift apart again - which is also why the stations chunk is
derived from the cap and the salvage fixture counts off it.
Fixes#2085
Guests are trip-scoped accounts. Trip Members excludes them from its picker and
refuses them on the write path, and Collections does the same, but Vacay's two
directories did not: a guest stayed selectable in a plan's Invite User dialog
and in the shared-calendar picker, including after being removed from the trip
it was created for.
The queries are server-side, so the filter belongs there rather than in the
client, which never received the flag to filter on. Both write paths get it too:
the picker only hands out a list, the id comes back from the client, and the
Vacay MCP tools reach the same two service methods.
Fixes#2112
The Atlas hero counted more countries than the continent bars below it drew,
and more than the dashboard widget. The extra ones were invented: a place
without coordinates fell back to parsing its address, and any two-letter
uppercase segment at the end became a country code. "New York, NY" produced NY,
"Toronto, ON" produced ON.
Those phantoms only ever showed up in the one number. The continent strip files
an unknown code under 'Other' and never draws it, the map skips codes it cannot
resolve, and the dashboard builds its set from stored regions instead, which is
why it stayed right.
Filtering against a list of valid codes would not have been enough, and would
have hidden the worse half of this: 26 US state abbreviations are real ISO
country codes. CA is California far more often than Canada, DE is Delaware, IN
is Indiana. The existing comment above resolveCountryCode already describes that
collision and solves it by trusting coordinates first and sanity-checking the
address against the country's own bounding box. The gap it names in its last
line is this bug: with no coordinates there is nothing to check against.
So the bare-code branch is now only offered to callers that have coordinates. A
spelled-out country name still resolves either way. A lone "NY" or "CA" resolves
to nothing, which is the honest answer.
Fixes#2111
Pointing the instance at a different OIDC provider left two things from the old
one on every account. The avatar kept a URL on a host this instance no longer
talks to, so it rendered as a broken image and stayed that way: the sync only
ran when the new provider sent a picture claim, and having no picture for a user
is a perfectly normal answer.
The account also stayed pinned to the old provider. Login still worked, because
the verified-email lookup finds the row, but the relink sat behind a check for a
missing sub, so oidc_sub and oidc_issuer never moved. The SSO note in settings
went on naming a provider that was gone.
An uploaded avatar is a bare filename rather than a URL and is left alone on
both paths, exactly as before.
Fixes#2110
The handler had eight cases, its two doubles had none, which Sonar counted as
new code without coverage. The mock is the more useful of the two to pin
anyway: an importer developing against it should hit the same refusals it will
hit in production, so the SVG, the missing extension, the empty payload and the
size cap are all asserted against the double rather than only against the host.
A journal importer can create the journey, create its entries and write their
stories, and then it stops: there was no way to give an entry a picture. The
seven journal.* methods cover text and nothing else, so an importer reading an
export archive could only upload its photos as trip files, which live on a trip
and never reach the journey's gallery.
journal.addEntryPhoto takes the bytes. Deliberately not a link to an existing
gallery photo and not a provider asset: an importer holding an archive has
neither, it has a file it just unpacked.
The access decision stays where it already was. addPhoto answers null unless the
acting user may edit the journey the entry belongs to, and null becomes
RESOURCE_FORBIDDEN here, so a plugin reaches exactly the entries its acting user
does. Everything the REST upload refuses, this refuses too: images only and no
SVG, the operator's allowed-file-types setting, 10MB decoded, demo mode. The
stored filename is the host's, never the plugin's, because the gallery dedupes
on file_path. The object is written before any row points at it and removed
again when the link is refused.
JournalRpc moves into its own module for this. It now needs StorageService and
the EXIF backfill out of MemoriesModule, and JourneyDomainModule exists
precisely so places, assignments and the plugin host can reach a few journey
functions without dragging both photo providers into their graphs.
Discussion #1365
Homepage's customapi widget and everything shaped like it want a handful
of numbers from one request; /api/v1/trips hands out the rows, but a
widget maps fields, it cannot aggregate a list.
The figures come from AtlasService.getTravelStats, the same call behind
the dashboard's passport card, rather than from a second count. Visited
countries carries years of rules — countries reached only by a flight,
layovers that do not count, countries hidden by hand — and a second
implementation would disagree with the card next to it within a release.
That is also why the controller sits in atlas/ and not in public-api/:
PublicApiModule stays a leaf, so the route comes to the data instead.
TravelStatsController next door already does this for
/api/auth/travel-stats. ApiTokenGuard is provided locally rather than
imported with PublicApiModule — @UseGuards instantiates a guard in the
declaring controller's module, so an export would still leave its
TokenService unresolvable here.
last_trip means the most recently started trip, not the most recently
created one, and is null while every trip is still ahead.
Closes#1367
The three api-tokens endpoints landed without tests, which took
src/nest/auth/** under its coverage floor. Same four paths the
mcp-tokens block above already pins, plus the rate limit: unlike MCP
tokens these are not refused on a managed instance, so the limiter is
the only thing bounding a scripted caller.
Reading the surface back against real data turned up three things it dropped
without saying so.
Places with no day were invisible, because the payload only reached places
through day_assignments. On a live instance that is half of them, and they
are the ones carrying a coordinate and no claim about when — exactly what an
integration can act on. They come back under `unplanned_places`, minus the
hotel, which is already reported as an accommodation.
Bookings with no day were dropped by the day filter. reservations.day_id is
nullable and deleting a day detaches its bookings rather than removing them,
so this is not a hypothetical.
The bucket list had no endpoint at all. It hangs off the user rather than a
trip, so it gets its own, and it answers whether or not the Atlas addon is
switched on: the addon decides what TREK shows, not what a key can read.
Also fixes ?include=notes returning the bare trip: places, notes and
reservations are reported on days, so asking for one now implies days
instead of quietly answering with nothing.
The module stays a leaf. Reaching for AtlasService pulled AuthModule and the
storage registry in behind it and broke the surface's own boot, so the one
SELECT lives here like every other query in the file.
Minting a key for other software meant finding it under MCP, behind a
heading about AI assistants and gated on an addon it does not need. It now
sits on its own in Settings, above the MCP block, and works on an instance
with MCP switched off.
Deliberately separate state rather than another branch of the tab's shared
hook: the section has to stand on its own, and that hook is already carrying
forty fields through three levels of props.
Translated into all 23 languages.
An MCP token drives every assistant tool; an API key reads trips over HTTP.
Handing the same credential to a third-party integration gives it the whole
MCP surface, so the two are now separate kinds on `mcp_tokens` and the kind
sits in the WHERE clause — a token of the wrong kind is indistinguishable
from one that does not exist.
Also adds `travellers` to the trip payload: display name and an owner flag,
no ids and no email addresses. An integration key reads its owner's
itinerary; it is not a way to enumerate the people around them.
Documents the surface in the wiki, since integrators need something to read
that is not the source.
Two gaps from the first pass, both about a client that polls.
Rate limiting is keyed by user rather than by IP. A self-hosted integration and
its owner's browser routinely leave the same address, and an IP bucket would let
one starve the other; 120 requests a minute is generous for a sync surface and
still bounded, so a runaway poll degrades its own integration instead of the
instance.
updated_at now ships with every trip, and its docblock says plainly what it is
not: a content timestamp. It moves when the trip's own fields change — title,
dates, currency, cover — and stays put when a place, a day or a note is edited,
because the child tables carry no updated_at of their own. That also rules out
the ?since= filter this was meant to enable: it would silently hide every trip
whose itinerary changed, which is worse than no filter at all. Making it honest
needs a schema change, not an API field, so the field is documented for what it
is and the filter is left out.
TREK's REST surface is internal: it answers a session cookie, carries no
version, and changes with the client. That is fine for our own frontend and
useless for anyone building against us — the Dawarich integration people have
been asking for since April needs something it can rely on not moving.
So this adds a small one rather than opening the existing 549 endpoints:
GET /api/v1/trips the trips the caller can reach
GET /api/v1/trips/:id one trip, ?include= picks the sections
Auth reuses the trek_ tokens users already manage under Settings, via Bearer
or X-API-Key. No new credential type, no new table — the guard calls the same
hash lookup the MCP transport does. It deliberately refuses a session JWT and,
for now, OAuth bearers: a credential whose scopes this surface does not yet
interpret is worse than one it rejects outright.
Access is decided per trip against canAccessTrip, the same owner-or-member
predicate as everywhere else, and a trip the caller may not read answers 404
identically to one that does not exist — a 403 would confirm the id is real and
turn the endpoint into a way to count someone else's trips.
The payload is not the rows. Ids, foreign keys and order_index stay inside;
accommodation day ids resolve to ISO dates, places arrive in planned order, and
every dated thing carries its date so a consumer can join on days rather than
on ids it has no way to look up.
fixArrivalDate normalized the arrival and then looked for the time behind a
leading date. The model usually sends the arrival as a bare clock, "13:00" with
no date on it, and slicing ten characters off that leaves nothing to match, so
the function returned early and stopped deriving the arrival date at all.
Try the bare 12-hour form first, then fall back to the normalized string, which
covers both shapes: "13:00", "01:11 pm", and a full date-time.
When neither the keyword table nor the model produced a type, the router wrote
"hotel" and cast whatever string it got to FlatType without checking it. A hotel
is a booking type, so a ferry voucher imported from the Transport tab opened the
booking form, which offers six chips and not one transport among them, and
"other" was the only honest pick left. An unrecognised string got through the
cast too and then vanished silently in nuExtractToKiReservations.
The type is validated against FLAT_TYPES now. When it cannot be read the item is
still extracted with the lodging shape so it survives mapping, but it says so,
and the tie-breaker #2080 added finally has something to fire on: isUnplaceableItem
only ever matched a type outside both form's vocabularies, and the server can
only emit its own eight, so on a real document it never matched at all.
The tab travels with the job instead of sitting in component state. The parse
deliberately outlives navigation and reload and the review is triggered by the
global widget on any page, so by the time it ran the state had remounted back to
its default and the context was gone.
splitIso reached the time by taking characters 11..16, so "2026-06-11T01:11 PM"
became 01:11. The hour survived and the half of the day did not, which is why
AM and 24-hour times in the same document imported correctly. The arrival then
sorted before the departure and the router rolled it onto the next day, so a
three-hour hop was rendered as an overnight.
The client has done this properly since #1725; this is the server half, which
was never pulled across. normalizeLocalDateTime rewrites a value only when it
actually finds a meridiem, so a date-only value, a value carrying an offset and
anything the parser does not know come back byte for byte.
The mapper has only two exits for a date, toIsoString and splitIso, so this
covers all eight types and every emitted field at once. It also stops
sameConnection comparing a NaN timestamp, which silently merged unrelated legs
into one booking.
The ICS feed is public with a token that never expires and prints the
confirmation number into the DESCRIPTION, and the shared-trip payload does a
SELECT * over reservations with share_bookings on by default. Neither filters
anything, so whatever lands on a trip is published the moment it is written.
That is fine while a person types every booking in, and it stops being fine as
soon as something writes on their behalf.
reservations.ingest_state is that gate. It defaults to live, SQLite backfills
every existing row on the ALTER, and no current writer names the column, so
every booking visible today stays visible. Nothing writes staged yet.
The predicate takes its table alias because share.service.ts queried
reservations without one. A stay is withheld only when bookings point at it and
all of them are staged: a hand-added hotel has no booking at all and has to keep
showing up.
Ten new controllers in the test registry, and one line in the session
instructions: without it nothing tells an assistant to read the display
settings before rendering a temperature, which is what makes that tool worth
having.
A plugin warning is a plugin telling the user something is wrong with their
trip, and get_trip_summary omitted them, so an assistant reviewed a trip
from a strictly smaller picture than the user was looking at.
The whole memories domain had no tools, so the Immich and Synology
libraries were invisible even though the attach body is plain JSON and the
photos never pass through the call. Search, album listing, album contents,
and the attach.
The routes that stream photo bytes stay out: those are img targets.
TREK's own bulk travel-history importer, and the closest thing in the
product to what this whole branch is about. Both halves are server-side
JSON, so neither was ever browser-bound; they simply had no tools. The list
tool's output carries what the import tool needs, so a model can run the
pair without a detour through the UI.
export_trip_ics is a snapshot of one trip, so subscribing was out of reach
and the all-trips feed had no counterpart at all. Both feed tokens can now
be read, minted, rotated and revoked.
The invite link is the one that grants membership, which create_share_link
is not, and the descriptions say so in both directions so a model does not
reach for the wrong one. The two consumer routes stay out: the ics URLs are
what gets subscribed to, and accepting an invite is a human act.
test-db's reset list was missing trip_invite_tokens, so a leftover invite
re-attached itself to the next test's trip once ids restarted.
131 of 201 tools carry an addon gate, and the registry drops an entry when
either the addon gate or the scope gate fails: one filter, one outcome. The
session instructions tell the model to check what is available before
assuming a tool exists while giving it nothing to check with. list_addons is
that something, and it is the difference between a confused apology and
'ask your admin to enable Budget'.
The help routes were already public, so the two reader tools add no scope
surface at all. The asset route stays out: it serves bytes to an img tag.
An assistant rendering a temperature, a distance or a clock time was
guessing: the session instructions named only the trip currency, and nothing
could read what the user had picked.
The write works off an allow-list rather than off the settings row, because
that row also holds the webhook URL, the ntfy token and the Mapbox, Carto
and LLM keys, and getUserSettings hands some of those back in cleartext.
Everything outside the list is refused, including a key nobody has heard of
yet. The guard test asserts against the service's own encrypted and masked
sets rather than a copy of them, so a sixth key added there fails it.
Often the travel history is the uploaded PDFs, and an assistant could not
learn that a trip had any: there was no files.mcp.ts, no files scope, and no
files section in the trip summary.
Six tools: list, read contents, set description and primary link, add a
link, remove one, list them. Reading contents rides files:content rather
than files:read, because listing what a trip carries and reading what is
inside a booking PDF are different privileges.
The content read is not a second copy of the plugin path. readContent moved
out of files.rpc.ts into FilesService and both callers use it, so the 10 MB
cap, the storage-layer read and the off-thread decode stay in one place. The
RPC keeps its own error wording, which its tests still pin.
The admin gate on the three new category tools reached for AuthService, which
drags the storage registry in behind it, and the categories e2e builds a
container around one module against a two-table database. It stopped booting.
budget/ already solved this: isDemoUserId reads RuntimeEnvService and the
users table directly, and its module comments say why AuthModule is absent.
Categories now does the same, which also takes eight hand-built collaborators
out of its unit test.
The e2e gained RealtimeModule, which is global in the app graph but not in a
partial container, the way days.e2e.test.ts already imports it.
Two domains an assistant cannot reach at all today need somewhere to hang,
so this is the credential half on its own, ahead of the tools.
files:content is a third mode rather than a second group. Listing the
documents on a trip and reading the bytes inside a booking PDF are different
privileges, and the plugin host already draws that line between
db:read:files and db:read:files:content. It is not implied by files:write,
the same way journey:share is not implied by journey:write; the policy's
fallback branch covers both.
settings:write is scoped to display preferences. Settings also holds the
webhook URL, the ntfy token and the Mapbox, Carto and LLM keys, so the tools
that ride this scope work off an allow-list rather than off the settings
row.
34 scopes across 16 groups, translated into all 23 languages.
A dateless trip made by a tool silently got seven days, and reminder_days
was stuck on the default of three with no way to turn it off. Cover images
had no search at all.
add_trip_member and remove_trip_member hardcoded an owner check while REST
consults the configurable permission matrix and the plugin RPC does too:
three surfaces, two behaviours. They now use the same permission call
update_trip already made, so an instance that lowered member_manage behaves
consistently. The guest tools stay owner-only, because their REST routes are
owner-only by design and document it.
Asking whether a place is already in one of your lists took a walk over
every collection and lost the coordinate tolerance the server matches on.
Marking a trip's places visited across every list holding them was one call
per place.
The dashboard's next-booking view is a three way union that folds in
synthetic check-in and check-out rows, so rebuilding it from per-trip
summaries silently drops the accommodations.
A day note could not be coloured or positioned, so the service default of
9999 pinned every note an assistant wrote to the bottom of the timeline. A
collab note could not carry the website the card renders.
Categories could be assigned by bulk_update_places but never created: the
whole file was one read tool and a resource. The three new tools are
admin-only, matching the REST routes exactly.
image_url was in no places tool although the service persists it and the
picker in the UI sets a URL, which is not a file transport question.
search_place passed two of the service's four arguments, so an unbiased
query resolved to the wrong continent.
get_place_details gained expand for reviews and the editorial summary. It
defaults to off because that field mask bills as a Google Enterprise SKU,
and it now honours the admin kill switch the REST route checks: an instance
owner who turns Place Details off to stop the billing was still billable
through this one surface.
location_name was settable when creating an entry and never afterwards,
which is the sharpest create-versus-update asymmetry in the surface.
Coordinates, weather, tags and the pros and cons verdict were persisted by
the insert but had no way in, so an entry written by an assistant never
placed on the journey map.
mark_region_visited wanted a code like US-CA that nothing in the tool
surface could produce. The visited-region list also read the raw manual
marks while the UI reads the list derived from trip places, so the two
disagreed about where somebody had been. Travel stats gained the cities and
the distance the dashboard shows.
The toggle took a date and nothing else, then hardcoded a full vacation day
for the calling user, so a half day, a comp day or a day logged for another
plan member had no expression.
School holidays needed the plan flags that switch them on, and the region
list that makes them usable. That list needs care: a group region is stored
as COUNTRY|group:CODE and the reader treats a bare code as a subdivision, so
the tool now returns the string add_holiday_calendar actually wants. Belgium
and the Netherlands are the two countries this affects.
Bag assignment was reachable only while bulk-importing a list, and by bag
name, never for an item that already exists. Quantity, weight and the
private flag were not reachable at all, and the whole three-tier sharing
model was invisible: an item made by a tool was always Common.
Historical costs are rarely split evenly, and every budget tool took only a
list of member ids, which the service stores as a null amount, meaning an
equal share. members[] carries what each person owes instead.
A custom split has to reconcile before it is persisted, so the tool refuses
one whose shares do not add up in whole cents. Two ways round that check
needed closing with it: the write path drops a payer who is not on the trip
and then derives the total from what survived, and an empty payer list means
a total of zero rather than the previous one. Both would have certified a
split the stored row does not satisfy.
Also on update, which had none of them: currency (so the FX freeze is no
longer a no-op), expense_date, and a settlement currency, plus place_id on
create so a museum ticket can point at the museum.
An imported itinerary landed in whatever order the assistant appended it,
with no way to fix it afterwards: create_day only ever appended, and
days.service.insert was reachable from exactly one place in the server, the
REST controller. Because both halves were missing, inserting a day into the
middle of a trip could not be expressed at all.
url is persisted on create and update and rendered as a link by the
planner, and reservation_end_time is what makes a dinner run 19:00-21:30
rather than just starting. Neither was in a tool schema, so a booking
written by an assistant lost both - which for an imported itinerary is
every confirmation link it came with.
url validates through reservationUrlSchema from @trek/shared, the same
refinement the REST route applies, rather than a second hand-written one.
The transport form's picker has nine entries, not ten: a transit booking
carries a provider itinerary in metadata.transit and create_transit_journey
is what writes one, so letting a caller hand-make a bare transit row
produces a shape the transit UI does not expect.
Splits the one list in two - what may be asked for, and what counts as a
transport for the update gate - so a stored transit booking stays editable
through update_transport, which is where it was already reachable.
Also reports the ids set_reservation_travelers dropped instead of
returning a plain success with a shorter list.
Reads have hydrated a booking's travellers since #1517; there was no way
to write them, so an imported booking could not record who was on it -
and creating a guest is only half an answer if the guest cannot then be
attached to the flight they were on.
Mirrors PUT /api/trips/:tripId/reservations/:id/travelers. The roster
filter stays in the service, so an off-trip id is dropped rather than
attached and the tool cannot be used to enumerate users.
add_trip_member resolves an existing account by username or email, so a
travelling companion who never signed up could be put on a trip in the
planner and not through an assistant. A guest is assignable to budget
splits, packing, to-dos and day participants like any member, so leaving
them out of an import loses who was actually there.
Mirrors the owner-only /api/trips/:id/guests routes, including the
trip-scoped guard that keeps one trip's owner away from another trip's
guest and off a real member's user row.
create_transport and update_transport took flight, train, car and cruise.
The picker in TransportModal has ten modes and ReservationsPanel renders
all of them, so a bus, a ferry or a taxi could be planned in the UI and
not through an assistant - which is where importing a travel history from
another service runs aground.
Both lists now come from one constant in the picker's order rather than
four literals repeated across two schemas, which is how they drifted.
legs[] stays on flight and train: the form writes metadata.legs for those
two and for nothing else.
fix(import): stop reading tickets as lodging, and stop scroll escaping a dropdown
Two v4.0.0 reports.
The file importer classified nearly every transport document as a hotel. Its
keyword table tested lodging second, on a group that included "check-in" and
"check-out" — words printed on almost every ferry, train and bus ticket there
is — so a ferry ticket came back a hotel, and the review then handed the user
the booking form, whose six chips contain no transport type at all. That is
where "everything ends up as other" comes from: it was the only honest pick
left. Transport is tested first now, the lodging pattern asks for a word that
names lodging, and the rental pattern asks for a rental rather than for a
pick-up time that ferries and shuttles also print.
Two more in the same function. The flight path forces its type on everything it
returns, and it claimed any document with two letters followed by digits — which
is what a German rail number looks like, so an IC ticket was imported as a
flight without the keyword table ever running. It now stands down when the
document already names another kind of transport. And a type neither form can
express opened the booking form regardless; it now follows the tab the import
was started from, while a type either form knows still wins over the tab, since
one PDF routinely holds a flight and a hotel. An unreadable type reaching the
transport form arrives as transport_other rather than as a flight: a wrong
flight looks right enough to be saved unnoticed.
The other report: a dropdown inside a sheet scrolled the page instead of its own
list. The panel is portaled to document.body and positioned fixed, so its scroll
chain runs to the viewport rather than to the sheet it looks like it belongs to,
and nothing stopped it. Three portaled lists carry overscroll-behavior: contain
now — the select, the navigation menu (which reaches the phone through the place
sheet, not only the desktop inspector) and the phone day picker.
@
test(calendar): cover the window paths the split added
The domain branch ratchet went from 93 to 92.3: the new hand-over code brought
branches with no case behind them. Three cases and one simplification, no
threshold touched.
Covered: a split rental whose endpoints carry no IANA zone and fall back to
their coordinates; a bare end clock, which resolves against the start date and
therefore does NOT split; and an unsplit rental with a day but no clock, whose
hand-over has nothing to place and correctly emits nothing.
The metadata parse now happens once, inside describeReservation, rather than
three times. The two extra copies were only there to hand `meta` to it, and one
of their arms is unreachable from SQLite, where the column is always text.
@
fix(export): print the reader clock format, and split window bookings in the feed
Three v4.0.0 reports that all come down to a surface printing what is stored
instead of what the reader asked for.
The PDF (#2066) is assembled as an HTML string outside React, so no hook could
reach it and no time format was ever passed in. It printed the column: four
clock sites, all 24h whatever the personal setting or the admin default said,
and a place time typed by a 12h user came out as "3:00 PM" for a 24h reader.
Both export entry points now hand the format over the way they already hand
over the locale, and the four sites go through formatClockTime.
The journey editors (#2067) used a native <input type="time">, which paints
12h or 24h from the browser locale and cannot be told otherwise by any
attribute. Every other time field in TREK has used CustomTimePicker for exactly
that reason; the journey editor, its phone sheet and the phone place sheet were
never migrated, so the same place form obeyed the setting on desktop and
ignored it on a phone. The picker gained a disabled prop so the read-only
journey sheet keeps the affordance the native input gave it.
In the subscribed calendar (#2068), a booking that is a window rather than an
activity read as one block across its whole range: a week of airport parking
sat on all seven days. Both hand-overs now stand on their own — an hour each,
carrying what the block used to say — and the block goes. Only when the two
ends fall on different days: a same-day booking is one sitting, and a rental
with only its pickup geocoded keeps the block, which is the only thing holding
the return time and its zone at all. Rental cars were missing outright, because
the planner writes reservation_time = NULL when its optional time pickers are
left blank while still setting day_id, and nothing in the exporter ever read
the linked days. It reads them now, as the last resort, for every type — so a
hand-typed bus or train shows up as well.
@
test(server): unpin three fixtures from the 3.x version window
The 4.0.0 bump turned three suites red on dev, all for the same reason: they
described a world where the host was still 3.x.
The plugin install fixtures declared trek ">=3.2.0 <4.0.0" and ">=3.0.0
<4.0.0", so every default artifact became incompatible with its own host and
23 tests failed on the version gate instead of on what they were testing. The
upper bound bought nothing — the cases that exercise the gate pass their own
range — so it is gone rather than moved to 5.0.0.
The system-notice suite named thank-you-support twice. It is capped at 4.0.0
and handed over to release-4-0-0 there, so the established-user case saw a
notice it had never been told about, and the per-version recurrence case rode
on a notice that no longer applies at all. The first now reads the always-on
set out of the registry instead of naming it, and the second drives the
mechanism through a notice it pushes itself. Neither ages out at the next
release.
@
fix(dashboard): fix the upcoming widget dropping bookings, and give it the stays back
Three things, all in the same widget.
reservation_time holds two shapes. The booking form writes the datetime-local
YYYY-MM-DDTHH:MM; day-anchored rows, the demo seed among them, hold a bare
HH:MM. The query compared the column against an ISO timestamp as a string,
where ":" sorts above "2": every booking from 20:00 on passed the filter and
everything before it silently vanished, and the order ran off the same column.
On the demo that is why the Broadway show shows and the ferry does not. The
query now separates the date from the time before it filters, so a value with
no date falls back to the day it hangs off instead of being read as one, and
the order is by day and clock. The demo seed writes full timestamps, which is
the shape every real write path already produces.
Hotels came out of this list because a stay covers a range and a week in one
would hold a slot for the whole week. Arriving and leaving do not: they are
moments, and they are the two worth reminding someone of. Check-in and
check-out now appear as their own entries, named after the stay place and
falling back to the linked booking. The stay itself stays out. The two carry
the accommodation id, so the list key had to take the type with it.
An unconfirmed booking looked exactly like a confirmed one. The status was
already in the payload and is now a badge, on both the desktop tool and the
phone widget.
@