Commit Graph

185 Commits

Author SHA1 Message Date
jubnl 6ff936c613 chore: move auditLog backend to proper DI 2026-07-27 22:09:08 +02:00
jubnl 1d56de3897 chore: move permissions backend to proper DI 2026-07-27 22:08:37 +02:00
jubnl 55cb2148d4 fix(server): fix the quirks preserved by the day DI migration 2026-07-27 20:58:03 +02:00
jubnl 2d1eec76c3 chore: move day backend to proper DI 2026-07-27 20:57:01 +02:00
jubnl d952eaf4d4 fix(server): fix the quirks preserved by the reservations DI migration
Two verified defects carried through the parity fold:

- The create-path accommodation metadata sync gated on the raw
  accommodation_id instead of the resolved one, so a hotel whose
  accommodation was just auto-created never received its metadata
  check-in/out times or confirmation. Now keyed off resolvedAccommodationId
  (RESV-SVC-006 re-pins the fixed behavior).
- The multi-statement writes ran outside transactions: create (accommodation
  insert + reservation insert + endpoint save + metadata sync), update
  (accommodation upsert + update + endpoint replace + sync), remove (the
  3-delete cascade), setReservationTravelers (delete + inserts) and
  resyncReservationDays now run in db.transaction() — a mid-write failure no
  longer leaves partial state (RESV-FIX-001/002 pin the rollbacks).

Left as-is on purpose: the truthy updatePositions dayId check, the
empty-string COALESCE keeps on title/status/type, the TEXT accommodation_id
normalization, the optional day_plan_position wire tolerance and the
swallowed notifyBookingChange catches — all contract or intentional
behavior, not defects.
2026-07-27 19:52:00 +02:00
jubnl 203d67c45f chore: move reservations backend to proper DI
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.
2026-07-27 19:47:01 +02:00
jubnl c27fc99220 feat(server): adopt zod DTOs for the reservation body contracts
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.
2026-07-27 19:45:41 +02:00
jubnl fbc773386f fix(server): fix the quirks preserved by the vacay DI migration 2026-07-27 18:39:53 +02:00
jubnl 588457710c chore: move vacay backend to proper DI 2026-07-27 18:39:52 +02:00
jubnl 29e89a5774 feat(server): adopt zod DTOs for the vacay body contracts 2026-07-27 18:39:47 +02:00
jubnl 07b8f1234c fix(server): fix the quirks preserved by the collab DI migration 2026-07-27 00:27:12 +02:00
jubnl 49197c5370 feat(server): adopt zod DTOs for the collab body contracts 2026-07-27 00:27:11 +02:00
jubnl c08c595b7e chore: move collab backend to proper DI 2026-07-27 00:27:11 +02:00
jubnl f3e5be78c9 chore: move files backend to proper DI
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.
2026-07-26 23:19:26 +02:00
jubnl e0530680cb feat(server): adopt zod DTOs for the settings body contracts
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.
2026-07-26 22:24:14 +02:00
jubnl cece6509b9 chore: move settings backend to proper DI
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.
2026-07-26 22:23:57 +02:00
jubnl c9a9df5310 fix(server): harden share links — transactional writes, TTL renewal on update, scoped shared queries 2026-07-26 21:25:47 +02:00
jubnl 72c3edda1a chore: move share backend to proper DI 2026-07-26 21:24:57 +02:00
jubnl 41b51849b7 feat(server): adopt zod DTOs for the assignment body contracts 2026-07-26 20:34:06 +02:00
jubnl 164f5152be chore: move assignments backend to proper DI and harden its writes 2026-07-26 20:33:31 +02:00
jubnl cb0cd3300d chore: move trip-invite backend to proper DI
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).
2026-07-26 18:21:04 +02:00
jubnl 3762d3306c feat(server): adopt zod DTOs for the day-note body contracts 2026-07-26 17:44:25 +02:00
jubnl 4e49e9c21e chore: move day-notes backend to proper DI 2026-07-26 17:44:12 +02:00
jubnl 7242812b27 fix(trips): scope the offline bundle's packing items to the viewer (#858) 2026-07-26 16:57:02 +02:00
jubnl 934848e114 fix(packing): correct legacy quirks kept during the DI migration 2026-07-26 16:56:18 +02:00
jubnl 5da2abf4f9 feat(server): adopt zod DTOs for the packing body contracts 2026-07-26 16:55:53 +02:00
jubnl f1960dc364 chore: move packing backend to proper DI 2026-07-26 16:55:20 +02:00
jubnl 811230fa5d refactor(plugins): wire PluginHostDepsFactory through DI, drop tags/categories bridges 2026-07-26 15:28:55 +02:00
jubnl 167db04d69 refactor(plugins): split create-rpc-host into an injectable factory + host-state module 2026-07-26 15:28:39 +02:00
jubnl 6c583c2f27 feat(server): adopt nestjs-zod with a fail-closed body-contract boot gate
- 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.
2026-07-26 14:55:13 +02:00
jubnl 79946cc711 chore: move todo backend to proper DI 2026-07-26 14:09:54 +02:00
jubnl 2ce40724d7 chore: move category backend to proper DI 2026-07-26 12:36:30 +02:00
jubnl 8f3ee53841 feat(mcp): Create nest wrapper for MCP for proper DI (#1708) 2026-07-26 11:42:47 +02:00
jubnl 53e884ad55 refactor(nest): migrate tagService into TagsService DI (pilot)
Move the tag SQL from src/services/tagService.ts into TagsService over an
injected DatabaseService (identical statements, #10b981 default, COALESCE
semantics). Non-Nest consumers (MCP tools, plugin RPC host) go through the
new tags.bridge.ts, which exports the legacy function names 1:1 from a
module-level instance over the shared db Proxy — the settled pattern for
the remaining legacy-service migrations (recipe documented in
src/nest/README.md). Tests are DI-native: direct service construction in
unit (with bridge delegation cases for the coverage gate) and
DatabaseModule + real SQL in e2e.
2026-07-25 18:36:17 +02:00
jubnl dd2c2d6668 refactor(nest): inject DatabaseService in db-using controllers 2026-07-25 18:15:15 +02:00
jubnl fbe200424d refactor(nest): inject DatabaseService in db-using services 2026-07-25 18:15:04 +02:00
jubnl def6b84938 refactor(nest): inject DatabaseService in access-check services and controllers 2026-07-25 18:14:53 +02:00
jubnl 718022c7ba feat(db): provide better-sqlite3 connection via DI token and complete DatabaseService surface 2026-07-25 18:14:37 +02:00
jubnl e162da6dbe fix(server): resolve zh-TW default language, close lint gap on bracket env access 2026-07-25 16:19:15 +02:00
jubnl 3b31f4256b refactor(server): move language/session parsing into app-config, config.ts becomes keys module 2026-07-25 16:01:42 +02:00
jubnl a34ab288f5 refactor(server): bootstrap and middleware read config via app-config 2026-07-25 15:17:33 +02:00
jubnl e17f165e44 feat(server): add nest AppConfigModule bound to @nestjs/config 2026-07-25 15:12:38 +02:00
jubnl 48ba9092c3 feat(server): add app-config core with fail-fast env validation 2026-07-25 15:03:27 +02:00
Maurice 3c28c7eaeb test(vacay): cover the leave-year window and the comp exclusion
Windows for all three year types, the fallbacks (anniversary without a hire
date, a day the month cannot have), usage counted across the calendar-year
boundary, carry-over chained over periods rather than years, deleteYear over a
shifted period, and the reads with and without a viewer.

For comp: excluded from usage, a half comp day still costing nothing,
converting a vacation day back, and comp_used reported on its own.
2026-07-25 12:02:16 +02:00
Maurice 9d93ed95bf feat(vacay): read and write the leave year over its own window (#737)
Adds GET/PUT /addons/vacay/year-settings and makes the reads window-aware, so
the backend from the previous commit is actually reachable.

Entries and shared calendars now load over the caller's window instead of a
date prefix — a shifted year spans two calendar years and the prefix silently
dropped the second half. Reads are month-aligned (the grid draws month cards),
while entitlement stays day-exact; the two coincide for every window starting
on the 1st, which is all of calendar. deleteYear clears entries per author over
that author's period, and company holidays only over the range every member
shares, so one member deleting a period cannot reach into another's live one.
getStats reports the window it counted, and getOwnPlan seeds the period today
falls into rather than the calendar year.

A start day the month cannot have (Feb 29 from a hire date, a stored Feb 31)
is clamped to one every year has — otherwise the boundary string matches no
date and the whole window slides. Anniversary before a hire date is entered
resolves to the calendar year instead of borrowing a month a previous fiscal
setting left behind.

MCP reads pass their user through; without one the range stays the plain
calendar year, so plan-scoped callers are unchanged.
2026-07-25 12:02:16 +02:00
Maurice 2672a7a741 test(vacay): catch the suites up with the comp/flex column
The kind column changed four things the tests still asserted the old way:
toggleEntry gained a parameter, the toolbar gained the comp modifier, shared
calendar rows carry kind, and a multi-person cell is now segment overlays
instead of one split gradient. splitBackground went with it — nothing but the
test still referenced it.

The toolbar tests picked the half-day button by index, which the new button
shifted; they select by name now so the next modifier does not break them.
2026-07-25 12:02:16 +02:00
Maurice bc06125d6e test(reservations): server + client coverage for traveler assignment 2026-07-24 16:50:41 +02:00
fbnlrz 9ad3507e57 fix(llm-parse): tolerate non-strict JSON from Gemini and similar models
Gemini reached through the OpenAI-compatible endpoint returns
JavaScript-object-literal output — single-quoted strings, unquoted keys,
trailing commas — which strict JSON.parse rejects. The reservation list
then came back empty and the booking-import UI showed nothing (#1638).

Add a shared parseLenientJson() that tries strict JSON first and falls
back to JSON5 (which accepts exactly that relaxed superset), and route
both the OpenAI-compatible and Ollama-native parse paths through it,
replacing the two identical strict parsers.
2026-07-24 12:42:38 +02:00
Maurice 68b0c6bceb fix(settings): mask inherited encrypted admin defaults so llm_api_key never reaches the client in cleartext 2026-07-24 12:26:58 +02:00
fbnlrz 4b73fa80a9 fix(settings): let an empty user value fall back to the admin default
The admin "user defaults" merge kept any user value that was merely
present, so an empty string shadowed the system-wide default. The
client's Settings save always writes every field — a blank Mapbox token
included — so a user who ever opened Map Settings got '' instead of the
admin token, and clearing their own token could never restore the
fallback (#1634).

Treat an empty/null user value for a defaultable key as "not set" when an
admin default exists, so it falls through. Non-defaultable keys and keys
without an admin default keep their exact stored value.
2026-07-24 12:26:58 +02:00