- nest/README.md: reservations joins the DI-native list; recipe history gains
the residue-fold paragraph; next up is dayService per the dependency-honest
order.
- migrate.md: wave-4 entry ticked, with the migration-graph correction borne
out — reservationService imported neither budgetService nor dayService.
- migration-graph.md: regenerated for the post-fold import graph (row/node
removed, frontier and order updated, dayService is the next pick).
- plugins/DI-MIGRATION.md: the factory's last plain-function reservation
import is drained.
Folds the 626-line services/reservationService.ts into the DI-native
ReservationsService — byte-identical SQL, statuses, bodies, error strings
and broadcasts; the legacy file is deleted.
- reservations.mcp.ts: the 5 legacy tools (create/update/delete_reservation,
reorder_reservations, link_hotel_accommodation) + the trip-reservations
resource move to the decorator registry (identical names, schemas,
annotations, error strings, broadcasts); the canWrite/canRead registration
gates become declarative access markers. Legacy registrar and resource
entries removed.
- reservations.bridge.ts: 9 legacy-named exports + 3 type re-exports for the
outside-container consumers (tripService, airtrail import/sync, the
still-legacy transit + transports registrars, transitItineraryService's
type-only import).
- notifyBookingChange folds to the legacy (tripId, actorId) signature; the
controller passes user.id and the plugin-host factory drops its last
plain-function reservation import for a private method over the injected
service (constructor unchanged).
- TripsService and BookingImportService inject ReservationsService instead of
importing legacy functions.
- Tests: reservations.service.test.ts rewritten as a real-SQL suite
(RESV-SVC-001..030, RES-TRAV-001..005 moved verbatim,
RESV-BRIDGE-001..009 pinning the bridge); both e2e suites converted to the
DI-native pattern; path mocks repointed to the bridge or constructor stubs;
tools-reservations kept and extended with the moved resource cases.
Quirks preserved on purpose (parity first — see the follow-up fix commit):
the raw-accommodation_id metadata-sync gate, the un-transactioned
multi-statement writes, the truthy updatePositions dayId check, the
empty-string COALESCE keeps, and the TEXT accommodation_id normalization.
ReservationsController's 4 grandfathered @Body() params now validate through
the global ZodValidationPipe (createZodDto over the existing @trek/shared
schemas); their allow-list entries are removed (the boot gate rejects stale
entries). The bespoke 400 bodies (Title is required / positions must be an
array / user_ids must be an array) become the pipe's field envelope, and
non-numeric traveler ids now 400 instead of being silently dropped.
reservationPositionsRequestSchema is loosened to the real wire contract:
day_plan_position is optional (the legacy route never validated position
items and an absent value binds NULL) — pinned by RESV-006.
AccommodationsController stays grandfathered until the dayService migration.
Folds the legacy services/fileService.ts into the DI-native FilesService
(injected DatabaseService, byte-identical SQL). Load-time constants move to
files.constants.ts; files.bridge.ts survives with a single export for the
request-time getAllowedExtensions read the module-scope multer configs need
outside DI. TripsService and PluginHostDepsFactory inject FilesService.
Also rides the nestjs-zod ratchet (FileUploadDto/FileUpdateDto/FileLinkDto
over the @trek/shared contracts, FilesController entries dropped from the
body-contract allow-list) and fixes the verified legacy quirks: file-link
insert failures now surface instead of being swallowed, updateFile coerces
an empty-string description to NULL like createFile, and formatFile's
misleading optional trip_id annotation is gone.
PluginRuntimeService's operatorEgressHosts/pruneErrorLog and PluginsService's
egressHostCount were module-level helpers on the raw db proxy; they fold into
their classes as methods over the injected service. (PluginHostDepsFactory's
equivalent conversion rode the settings commit — same files.) No injectable
class under src/nest imports the db global anymore; the remaining importers
are the documented module-level seams (bridges, platform shell,
plugin-host-state, signature-status, the bare plugin user-setting readers).
SettingUpsertDto/SettingsBulkDto wrap the existing @trek/shared schemas; both
SettingsController entries drop off the body-contract allow-list (boot
ratchet). Per the todo precedent the dead bespoke 400 guards go: missing/empty
key and a non-record settings body now answer with the pipe envelope
({ error: 'key: …' } / { error: 'settings: …' }) instead of 'Key is required' /
'Settings object is required', and arrays for settings are rejected. The local
masked-value literal now imports MASKED_SETTING_VALUE from @trek/shared.
The settings e2e registers ZodValidationPipe and pins the DTO 400s plus the
bulk masked-sentinel skip.
Folds the legacy services/settingsService SQL into the Nest SettingsService
(byte-identical statements; BEGIN/COMMIT blocks become db.transaction()).
AdminService and ShareService inject it; llm-config.resolver becomes the
injectable LlmConfigResolver (SettingsService + DatabaseService), injected by
LlmParseService and PluginHostDepsFactory. No MCP surface, no bridge.
Whole-file commit, so this also carries the post-migration quirk fixes that
live in the same files: null serializes as '' (a stored "null" leaked out of
getDecryptedUserSetting as the literal string), bulk upserts skip the masked
sentinel (mirroring the single-upsert no-op) and return the written count.
Unit suite moved to tests/unit/nest with SET-SVC-001..026 preserved,
027..030 pin the fixes; settings/admin e2e converted to DI-native.
New tripInviteLinkCreateRequestSchema in @trek/shared (single source of
truth), a createZodDto wrapper on the controller's raw @Body(), and the
TripInviteLinkController.create entry dropped from the body-contract
allow-list (the boot ratchet enforces it).
expires_in_days accepts number | digits-only string | null | absent — the
string branch is digits-only so garbage like "7abc" is a 400 instead of
silently parseInt-ing to 7; the empty string (blank form input, meaning no
expiry) stays accepted. The e2e registers ZodValidationPipe and pins both
the bounded-expiry and 400 paths.
Fold the legacy tripInviteService SQL into the Nest TripInviteService over
the injected DatabaseService (byte-identical statements; the JS-side expiry
check and non-positive-means-no-expiry coercion preserved) and delete the
legacy module. No bridge, no MCP port and no plugin-host change needed —
nothing outside the container consumed this domain.
Also wraps the rotation's probe + write + re-select in db.transaction()
per the repo rule that multi-statement writes are never bare.
Unit suite moved to tests/unit/nest preserving TRIP-INVITE-001..006 and
adding 007 (expiry pinning); the e2e converted to the DI-native pattern
(real SQL against temp trips/trip_invite_tokens tables).
- One global ZodValidationPipe (APP_PIPE, createZodValidationPipe) validates
any @Body() typed with a createZodDto class; the custom exception factory
keeps TREK's { error: 'field: message; ...' } envelope byte-identical.
- The 7 piped controllers (collections, places, vacay, todo, health,
airtrail x2) now type bodies with colocated <domain>.dto.ts wrappers —
the Zod schemas in @trek/shared stay the single source of truth.
- OpenAPI: the hand-rolled api-zod.ts enricher is replaced by nestjs-zod's
cleanupOpenApiDoc; request bodies surface as $refs to named DTO schemas.
- validateBodyContracts() runs in buildApp() after init and refuses to boot
when any POST/PUT/PATCH @Body() (including @Body('field') sub-reads) lacks
a ZodDto metatype. The 175 pre-existing legacy handlers are grandfathered
in body-contract-allow-list.ts; the list is ratchet-only — the gate also
throws on stale entries, so migrating a domain forces their removal.
The optional metadata fields (category, due_date, description,
assigned_user_id — plus priority on update) now accept explicit null: the
client clears fields by sending null, which the schemas rejected. checked
accepts the legacy 0/1 numeric form alongside boolean (the integration net
pins it). Drops the client-side casts that papered over the too-narrow
types.
updateCategoryAssignees ran its DELETE + INSERT OR IGNORE loop unwrapped, so
a mid-loop failure could leave a category half-cleared. Also widens the item
params to the nullable shapes the wire contract actually sends (prep for the
todo body validation).
The hero picks the trip that is running, else the next one coming up, else
just trips[0] — and that pick was then removed from the grid below. The first
two cases are deliberate: the trip is already on screen, showing it again
right underneath would be noise. The trips[0] fallback is a different thing.
It fires precisely when nothing qualifies, so it borrows a trip that belongs
in the grid, and removing it there leaves the user with a hero and an empty
list. Someone whose trips are all finished sees "No trips yet" under a card
of the trip they just came back from.
Split the two: only a trip that earned the hero is taken out of the grid.
Undated trips were caught by this too, and worse — getTripStatus returns
null rather than 'past' for them, so a lone dateless trip appeared in no
tab at all.
The empty state was the second half of the report. It equated "planned is
empty" with "you have no trips" and told people to create their first one.
Gate it on there being no trip at all, which is the condition the mobile
dashboard already uses.
Mobile shares useDashboard, so it is fixed along with the desktop.
Closes#1706
sessionStorage outlives a logout inside the same tab, so the keys stay readable
after the next user signs in. They are namespaced by user id, which stops the new
user's plugins from reading them, but the data is still sitting in the tab — the
same reason logout already drops the appearance snapshot, the SW caches and the
user-scoped offline DB. Purge them there too.
Three things around it, from reading the handlers next to the new ones:
- The inactive-plugin purge ran before the store was updated, so a sessionStorage
failure cost us the plugin list we had just fetched. Commit first, purge after.
- The session handlers answered without checking requestId, unlike confirm and
geolocation — an answer the frame can never match is worth skipping.
- The mock host threw plain Errors while the real bridge rejects with .code set,
and checked the key before the trip scope where the host does the reverse, so a
plugin's error handling could pass its tests and still break in the app.
The context table listed the new field without saying what a plugin is meant to
do with it. It is a display hint — booking codes still arrive in full over
trek:invoke — and reading it as redaction is the one mistake worth preventing.
The dev preview hardcoded it to false, so the blurred path was the only host flag
an author could not try out; it gets a toggle like reduce motion and the rest.
The widget's new healthApi.features() call left BackgroundTasksWidget.test.tsx
red — the api/client mock never exported healthApi, so both existing tests threw
on mount. The mock now carries it, pending by default like the poll backstop, and
the retry itself finally has tests.
Writing them turned up two gaps. The retry never called saveImportFiles, so the
files only lived in memory: reloading during the retry parse left the review with
no source document to attach to the bookings it creates, unlike every import
started from the modal. And the button stayed live while the request was in
flight, so a second click started a second force-ai job on the same files.