The stay dialog and the phone stay sheet take their presets, the five minute step, the full day limit, the draft and the arrival and departure preview from useStayDraft. Each still seeds the draft when it opens and saves it its own way; the phone keeps whole minutes when a step moves a stored fraction, the desktop does not.
The export dialog and the phone export sheet keep their rows and run the PDF, the calendar file and the GPX download through useTripExport, which also reads the print settings and whether my plan is offered. The desktop still runs one export at a time and closes after each one; the phone still lets a calendar or GPX file run beside a PDF and stays open after the PDF.
The transport detail dialog and the phone transport sheet name the booking's day and each segment's own booking code through transportDetailModel, next to the files filter they already share. The rest of the two views reads the booking differently on purpose and stays as it is.
The booking dialog and the phone booking sheet keep their markup and run on useReservationForm: how a saved booking or an import fills the form, the end before start check, the start date and time, the place and hotel picks, the stay range and the request body with the stay a hotel booking creates come from reservationFormModel. The desktop's check-in window, its link to a stop inside the form and its imported price stay its own, behind options where the code is shared.
The transport dialog and the phone transport sheet keep their markup and run on useTransportForm: the route rows, how a saved booking or an import fills them, the day options and the request body come from transportEndpoints. Where the two shells differ today (unknown type fallback, stopover days, which rows are reset, train anchors, empty metadata, the link field) the shared code takes an option. The phone's upload helper moves to bookingFormModel next to the traveller checks both booking forms use.
costsModel moves from the phone tabs to components/Budget.
useCostsLedger holds the settlement, the totals, the filters, the CSV
export and the payments, and useSettlementForm holds the payment form.
CostsPanel and MCostsTab both run on them.
The old differences stay behind flags. The desktop also loads the
settlement on a trip change, leaves expenses nobody paid out of my share
(#2225) and writes each currency as stored in the CSV. The phone sheet
starts blank, refills when it opens and ignores a second save while one
is running.
useExpenseForm holds the payers, the splits, the ticket items and the
receipts of the expense editor in CostsPanel and MCostSheet.
Flags keep the old differences. Only the desktop sends the people named
on a ticket line as members (#1382). The phone ignores a second save
while one is running and stays busy after a successful save. The
rebalance effect uses useEffectEvent instead of an eslint-disable.
useBagCardEditor holds the editing of a bag's name and weight limit for
the desktop bag card and MBagsSheet, and bagLoadSummary computes the
weight totals for the bag sidebar, the bag modal and the phone sheet.
The desktop card follows the bag when it changes elsewhere, the phone
row resets a name it did not save.
usePackingItemActions, usePackingTemplates and usePackingImport take the
item, template and bulk import logic out of usePackingListPanel and
MPackingListTab, and the phone listsModel keeps only its priority
tokens.
When adding to a category the desktop looks for the placeholder in the
store and the phone in its rendered items. The desktop toasts on an
empty import, the phone runs one import at a time. The effects run in
the same order as before.
The to-do filters, counts, sort order and categories move from the phone
listsModel into todoListModel. useTaskForm holds the name, the fields,
the load and the save of a task for the desktop detail and new task
panes and for MTaskSheet. Each side keeps its own guard and error
wording.
useListsSubTab remembers whether the packing or the to-do sub tab was
open, for ListsContainer and MTripShell. The desktop restores any stored
value, the phone restores todo and otherwise falls back to packing.
usePlaceForm and usePlaceSearch take the form state, the prefill, the
opening hours selection, the duplicate name check, the payload and the
maps search out of PlaceFormModal. MPlaceEditSheet, PlPlaceSearch and
PlCategoryPicker run on the same hooks, and PlTimeFields uses the shared
timeCollisions.
Where the two shells behaved differently the hooks take a dialog or a
sheet variant: the coordinate pair rule, abort and console handling,
when the billing session ends, whether a pick clears the suggestions,
the empty search cue, whether a map link clears the results, and how a
new category is trimmed and guarded. The open effect and the submit flow
stay per shell, because service stops, expense intents and delete arming
differ too much to share.
useAddPlaceToCollection holds the maps search, the pick and the save
that AddPlaceToCollectionModal and MCollAddSheet each carried, with the
pure helpers in addPlaceModel.
The differences stay as variants. The desktop resets and stays open
after an add, sends links and typed coordinates and shows the error's
own text. The phone closes, takes coordinates only from the picked hit,
ignores a second search or save while one is running and shows the
fallback text.
usePlacesPool holds the category filter, the search, the multi select
and the bulk mark visited of the places pool, for usePlacesSidebar and
for MPlacesBrowser with its filter controls and sheet. When selected
places are deleted, the desktop leaves select mode once none of them is
left and the phone drops only the missing ids. The hook keeps both.
usePlacesFileImport holds the GPX, KML and KMZ import that
FileImportModal and ImpFileStep each carried. The desktop keeps one
toast and one undo label and counts only the summary's skips. The phone
keeps a toast per format, the importFiles undo label for a mix and the
top level skipped fallback.
useListImport holds the Google and Naver shared list import that
usePlacesSidebar and ImpListStep each carried. The desktop closes the
dialog and clears the link before the undo step, the phone calls onDone
after it, and the hook keeps both orders.
useSaveTripPlacesToList holds the logic that saves a selection of trip
places to a list, which SaveTripPlacesToListModal and
MPlacesSaveToCollectionSheet each carried as a copy. The two copies did
the same thing, so the hook has no variants.
The bookings model moves from the phone tabs into components/Planner/bookings, so
both shells read the same sections, traveller predicate and type colours. The
phone bookings and transports tabs had the same traveller filter and folded
sections twice; that state now lives once in useReservationListFilter, which only
those two tabs use. The desktop panel keeps its own filters in useBookingsView
because it remembers them for the session.
The status switch and the delete on a booking card stay per shell. The desktop
hook renders its ConfirmDialog and awaits the delete, the phone asks in an
MConfirmSheet and lets the delete run on its own; sharing them would change
when the question closes.
filesFor moves out of the desktop booking parts into utils, and the phone
transport sheet now reads a booking's files through it as the desktop
transport detail and booking cards do.
The desktop note dialog and the phone note sheet fill their editor with
dayNoteDraft and save through writeDayNote, so both trim the title, store an
empty detail as null and default the icon the same way. Both count the detail
line against one shared limit.
The desktop hotel picker and the phone accommodation sheet take their day
options, range changes, checkout default, places on offer and request body
from stayFormModel, and both save a stay through writeStay.
The desktop day panel and the phone day sheet load the day's forecast with
useDayForecast, rename the day with useDayRename, and read the day's
bookings, the stay labels and the display temperature from dayDetailModel.
The day plan's connector menu and the phone timeline's are built by
legModeMenuItems, and both save a leg's mode through useLegModeActions, which
reports a refused write and reloads the days the same way on both.
The desktop day plan and the phone timeline derive the place order, the note
and transport positions and the per leg positions of a reordered day from
mergedOrder, and write the moved bookings back through the same helpers.
dayRoute moves from the phone's lib into components/Planner. The desktop day
plan builds its Google Maps and CoMaps stops with dayExportStops, and both
optimizers put the reordered stops around the fixed ones with fillAroundLocked.
The day plan, the day panel, the phone timeline and the phone day sheet read
the day local weather anchor from dayWeatherAnchor, and every weather icon
comes from one map in weatherIcons.
The place inspector and the phone place sheet now run on one module for
their participants, file uploads, image picks, ratings and place updates,
and both build the save to collection target with placeToSaveTarget.
The lease heartbeat and settle failures, the log sink and rotation
fallbacks, the runtime env getters, the public API summary edges, the
admin-only permission level and the WebSocket handler rejection had no
spec, which took six domains under their coverage floor.
The session gate passes a session lookup to verifyJwtAndLoadUser, the
Immich save takes auto_upload, getAppUrl stays synchronous, the demo
session list marks the current session by the id it is handed, and the
lease no longer needs a transaction of its own, so its spec is gone.
The converted specs wrote accommodation_id as a plain id, while the
services store the legacy '<id>.0' text, and wrote read-only foreign key
twins instead of their relations. The files e2e schema gains the trip
columns the entity fills, and the idempotency spec seeds a real user.
findRow and the default category lookup asked findOne with an empty
where, which MikroORM refuses. insertRows returned a single id because
insertMany on plain data reports only the last insert id; it now inserts
row by row and returns every id. insertRow and updateRows reject instead
of throwing before the caller holds a promise.
TRIP-SVC-072 still pinned the old order, where the trip row committed
before the days were rebuilt. Both now run in one transaction, as
TRIP-SVC-018b already asserts.
All 100 dialect helper cases pass on both engines. Of 1402 repository
methods, 11 send one statement Postgres refuses as SQL and 4 send none
the probe can see; both lists may only shrink from here.
The specs written alongside the session, transaction and lifecycle work
still seeded and read with raw statements. They go through the ORM
factories now; the few that test SQL below the ORM carry a test-sql-allow
reason. The raw SQL baseline holds 177 statements in 6 files.
The session carry-over specs were written against the old close and
reinitialize calls; they now drive the database backup port. The rollback
specs read through the factories and await the helpers that became async
when the seeding moved to the ORM. Two readers that lost their await and a
duplicated import are fixed as well.
acquire() made sure a row existed and then raced on a conditional UPDATE,
two statements the registrar wrapped in a transaction of its own. lint:tx
cannot follow a repository reached through a local variable, so it
reported the method. One INSERT ... ON CONFLICT DO UPDATE ... WHERE now
takes a free lease, renews the holder's and leaves anyone else's alone,
atomically on SQLite and on Postgres, and needs no transaction.
Both tables arrived in migrations written before the generator existed,
so src/db/kysely/ did not describe them and check:db-types would fail.
Regenerated with gen:db-types.
These specs left the raw SQL baseline but still emptied tables with
`db.exec('DELETE FROM ...')` and aged a Dawarich connection with a raw
UPDATE, which the ratchet cannot see because it only counts `.prepare(`.
They now go through deleteRows and updateRows like the rest of the file.
packing_item_recipients and packing_bag_members have no entity; both
cascade from their item and bag, so the packing spec relies on that with
foreign keys on in the snapshot database.
findRow returns null for a missing row, and `row?.deleted_at` is then
undefined, which `.not.toBeNull()` accepts. The converted soft delete,
album sync and passkey counter checks would therefore pass if the row were
removed outright. They now assert the row is there first, as the raw
`.get()` reads they replace did by throwing on a missing row.
The permissions and plugin RPC fallback comments were reworded when
their raw SQL went away; this keeps the new wording in line with the
comment style used elsewhere in the converted specs.
The specs converted to the factories no longer carry raw prepare()
calls, or keep only ones marked with test-sql-allow, so their baseline
entries are dropped. Leaving them in place would let raw SQL grow back
into those files unnoticed.
Converts the raw prepare() fixtures and reads in the reservations service
spec to the ORM factories: expense links, stays, stops, endpoints, day
positions and guest flags are written through insertRow and updateRows,
and the narrow SELECT post-conditions read whole rows through findRow and
findRows, compared with toMatchObject so they still assert exactly the
columns the projection named. stopsOn keeps returning only the three
columns its toEqual assertions compare, and the helpers that now await
become async.
The one write left raw carries a test-sql-allow marker: it plants
dangling references inside the synchronous foreign_keys = OFF window,
which an async ORM write would outlive. The unused canAccessTrip and
isOwner entries are dropped from the database mock.
Converts the raw prepare() fixtures and post-condition reads in the users
repository spec to updateRows, findRow and readUser. The Immich read
helper becomes async and still returns only the three columns its toEqual
assertions compare, and the guest insert check compares its columns with
toMatchObject now that the read returns the whole row.
The raw statements that remain carry a test-sql-allow marker: the legacy
statements run as parity oracles for the repository reads, the
NOT NULL DEFAULT 0 probe on is_guest, which has to bypass the entity
default, and the unflushed-entity check, which must read the table
without an EntityManager that could flush first.
Converts the raw prepare() fixtures and reads in the places service spec
to the ORM factories: tours, waypoints, files, links, expenses, collection
places and ratings are written through insertRow and updateRows, and what
the service left behind is read back through findRow, findRows and
countRows. The track colour cases insert their seeded tracks with
insertRows in place of INSERT ... RETURNING, tracksOf and the file attach
helper become async, and the stale updated_at is computed with dbNow
instead of datetime('now', '-1 hour'). The rating reads keep comparing
only the rating column, as the narrow SELECT did.