Commit Graph

3609 Commits

Author SHA1 Message Date
Maurice aba2ed1ee3 refactor(server): places, unsplash, vacay, collections and file downloads raise DomainError, so REST and MCP stop translating results 2026-10-10 20:16:06 +02:00
Maurice e81efcbc57 refactor(auth): auth, passkey, profile, token and admin services refuse with DomainError instead of returning error envelopes 2026-10-10 20:16:05 +02:00
Maurice 66042ae1c7 feat(server): services refuse with a DomainError that REST and MCP answer in their existing shapes 2026-10-10 20:16:05 +02:00
Maurice 064fba5c0c test(server): the provider-orchestrator boundary case gets an id of its own
Both boundary rules arrived as BOUND-012; the provider rule's case is now
BOUND-014.
2026-10-10 20:16:04 +02:00
Maurice 2c3fb15263 style(server): format the files the health lanes added or moved
Prettier on every new or moved file that was not formatted, so lint:format
holds them; the files taken off the format baseline are the ones that moved
or were formatted on the way.
2026-10-10 20:16:04 +02:00
Maurice a28044dbd4 chore(server): lint:boundaries holds the user-connection repositories and the journey read at their new count
The module comment no longer spells an @InjectRepository call the scanner took
for a foreign Users injection in nest/database, and journey's Users entry is
gone now that it reads the Immich flag through UserImmichRepository.
2026-10-10 20:16:04 +02:00
Maurice 912b8ff96b refactor(place-import): the Naver pager declares its page result, so the moved reader passes the strict ratchet
The reader left places.service with four strictNullChecks errors that a new
file may not carry. Declaring fetchPage's result as a page or a list import
error lets the 'error' check narrow it; the places and maps entries drop off
the strict baseline.
2026-10-10 20:16:04 +02:00
Maurice c859cf3624 test(server): the new demo-gate, timestamp, JSON-column and maps tests add no ESLint warnings
Non-null assertions become explicit guards, the two demo-gate loops collect
their answers with Promise.all, and the MapsService imports the maps tests no
longer use are gone. The src counts the refactors lowered are taken off the
warnings baseline.
2026-10-10 20:16:03 +02:00
Maurice 1a1257ccac docs(maps): the places-provider seam names both keyed providers it now carries 2026-10-10 20:16:03 +02:00
Maurice 04f38f9876 chore(server): eslint keeps fast-xml-parser inside place-import and the WebDAV client 2026-10-10 20:16:03 +02:00
Maurice 1389fe6984 refactor(place-import): file and shared-list readers move into their own domain over one gpx and one kml codec 2026-10-10 20:16:03 +02:00
Maurice dc42fe9a5a chore(server): lint:boundaries refuses a provider that imports the orchestrator above it 2026-10-10 20:16:02 +02:00
Maurice 80e64b4f4d refactor(maps): key selection, place details, marker photos and link resolving leave the maps orchestrator 2026-10-10 20:16:02 +02:00
Maurice 0cedff94a0 refactor(maps): wikimedia lookups and brand logos move into their own client 2026-10-10 20:16:02 +02:00
Maurice 79ba003bfe refactor(maps): nominatim and overpass calls move into an OpenStreetMap client 2026-10-10 20:16:02 +02:00
Maurice 863b02a549 refactor(maps): google place lookups go through the places provider 2026-10-10 20:16:01 +02:00
Maurice 3618a4ab9f refactor(maps): the searchText field mask lives beside the Google places provider 2026-10-10 20:16:01 +02:00
Maurice 526b2c105a fix(mcp): McpSharedModule imports the realtime module its guards inject
McpToolGuardsService injects RealtimeService, and McpSharedModule counted on
some other module of the app having imported the global RealtimeModule.
AuthModule used to do that for every domain that imported it for the demo
check; with those imports gone, a test app assembling ShareModule alone no
longer resolved the guards. The module now names the dependency itself.
2026-10-10 20:16:01 +02:00
Maurice cd0c27921c build(server): lint:boundaries holds foreign repository injections to a baseline
A domain injecting the repository of a table another domain owns couples
the two below the service layer, and nothing counted it. The new
foreignRepositories rule reads every @InjectRepository under src/nest,
looks the entity up in scripts/repository-owners.json (an injected entity
without an owner stops the run) and holds each domain/entity pair to the
baseline, which only shrinks: 209 pairs today.
2026-10-10 20:16:00 +02:00
Maurice fe35e26313 refactor(trip-membership): domains ask TripAccessService who may see a trip
Thirty services, controllers, MCP tools and RPC handlers outside the trips
domain injected TripsRepository for one call, findAccessible. They now
inject TripAccessService from the trip-membership leaf, in the same
constructor slot under the same field name; it answers with the same
access row (or undefined) and binds the id exactly as given, so each
caller keeps its own coercion and its own 404, 403 or tool error.
2026-10-10 20:16:00 +02:00
Maurice 734cbc2a94 refactor(vacay): the invite and share tools read the caller's name from UsersRepository
VacayMcp injected AuthService only to call getCurrentUser for the caller's
username and email before sending an invite or sharing the calendar, which
kept VacayModule importing AuthModule. UsersRepository.findUsernameEmail
answers the same two fields (and undefined for an unknown id, which the
tools already refuse as 'User not found.').
2026-10-10 20:16:00 +02:00
Maurice 806f3c8e62 refactor(addons): the transit-provider setting and photo-provider routes move into the shared kernel
AddonsService read the transit backend through transit/transit-provider.ts
and built photo-provider endpoint paths with memories.helpers, so the addons
domain, which nearly every domain imports for its gates, depended on
transit and memories. Both are free functions over a repository or plain
strings; they move unchanged to common/, where the transit and admin sides
read them too.
2026-10-10 20:15:59 +02:00
Maurice 846d8d4952 refactor(realtime): the gateway asks a journey-access port instead of importing the journey domain
The websocket gateway injected JourneyDomainService to decide who may join
a journey's book room, so the realtime domain, which nearly every domain
imports for its broadcasts, depended on journey and closed a cycle through
all of them. It now injects a JOURNEY_ACCESS port declared in realtime;
JourneyAccessModule (global, imported by AppModule beside the gateway
module) binds it to the same JourneyDomainService method.
2026-10-10 20:15:59 +02:00
Maurice 0c2226c5f0 refactor(realtime): the websocket welcome frame reads the running version from app-config
The gateway imported hostVersion from the plugin install code to put the
version in its welcome frame, which made the realtime domain, imported by
nearly every other, depend on plugins. The running version (image VERSION
or APP_VERSION, else package.json) is app config; runningVersion() holds it
and the plugin host-compat checks call the same function.
2026-10-10 20:15:59 +02:00
Maurice 01fd4e3739 refactor(admin): the admin routes other domains serve keep their DTOs in those domains
NotificationsController, SettingsController, OidcController and the packing
template controller each imported their body DTO from admin/admin.dto.ts,
and AdminController imported the same four without using them. Those
imports tied the notifications, settings, packing and oidc domains to the
admin domain and through it to everything admin depends on. The classes
move unchanged (same names, same @trek/shared schemas) into each domain's
own dto file.
2026-10-10 20:15:59 +02:00
Maurice e40bc71e9f refactor(plugins): the plugin RPC kit moves out of the plugins domain into src/nest-rpc
Every domain's *.rpc.ts was written against rpc-kit, rpc-params, rpc-errors
and PluginGuards, and every domain module imported PluginGuardsModule, all
of it inside src/nest/plugins, while PluginsRuntimeModule imports those same
domains so their handlers exist at boot. That made the plugins domain part
of every cycle. The kit imports no domain but the protocol tables and
permissions/addons for the guards, so it moves to src/nest-rpc beside
src/nest-mcp, with its tests, its README and its extraction rule; the
runtime module keeps its domain imports, which the boot-time coverage check
of the wire surface needs in any subset app.
2026-10-10 20:15:58 +02:00
Maurice cc79f561e5 refactor(auth): guards, auth decorators and ephemeral tokens move into an auth-core kernel
JwtAuthGuard, OptionalJwtAuthGuard, CookieAuthGuard, AdminGuard, the global
and MFA guards, @CurrentUser, @Public, JWT verification and the ephemeral
token service lived in the auth domain, so every controller that checked a
session depended on the whole account module and its imports. They import
no domain themselves; under src/nest/auth-core they are shared kernel like
common/, every importer points there directly, and AtlasModule and
NotificationsModule stop importing AuthModule, which they only did for the
demo gate. lint:boundaries counts auth-core as kernel: 57 fewer cyclic
domain edges, the largest cycle shrinks from 46 to 40 domains.
2026-10-10 20:15:58 +02:00
Maurice 1a9855cf70 refactor(mcp): the demo-mode write block runs once, in the registry's tool gate
Every write tool used to open with its own demo check, 196 copies across 30
tool files, most of them injecting AuthService for nothing else. The
registry now takes a toolGate (built from the container) that runs before
each registered tool handler, and TREK's gate refuses a demo account on
every tool not annotated read-only with the same canned result. The
per-tool checks, the AuthService injections, AuthService.isDemoUser and the
AuthModule imports that only fed them are gone; ESLint refuses a demo check
in a tool file, and a registry-wide test calls every write tool as the
demo account.
2026-10-10 20:15:57 +02:00
Maurice 8350d04a63 refactor(db): the Immich, Synology and AirTrail columns of users get repositories of their own
UserImmichRepository, UserSynologyRepository and UserAirtrailRepository hold the eighteen connection statements UsersRepository carried, on the same users table and with their statements unchanged, provided through a small module as factories over the EntityManager. ImmichService, SynologyService, AirtrailService and JourneyService inject them instead of the account repository. UserRow drops the dead immich_access_token column (the table keeps it), and a shape test pins the account repository's size.
2026-10-10 20:15:57 +02:00
Maurice 3b58694eb9 refactor(settings): instance settings are named through one typed register
app-settings.registry.ts lists every app_settings key the server reads by name with its shape, default, environment override and flags. The admin form's keys, its masked and encrypted handling and the public config's reads derive from it, isOidcConfigured() replaces the three issuer/client-id checks in AuthService, and OidcService resolves its values through the same rule while still requiring the client secret. Every literal getValue() read goes through readAppSetting()/resolveAppSetting(), and ESLint refuses a new one.
2026-10-10 20:15:56 +02:00
Maurice 42d75a2b7e test(auth): pin how the four OIDC checks read the environment and the settings
The login toggles' legacy fallback, the public config, the lockout guard of the settings save and the provider config each decide on their own whether OIDC is configured. These cases hold what each answers, including that only the provider config also wants the client secret, before the checks are shared.
2026-10-10 20:15:56 +02:00
Maurice e316ccf3bb perf(db): the calendar export, the journey reads and the stop reorders read and write in batches
The ICS export reads the stops and notes of all dated days in two statements instead of two per day, the journey's full read and entry list take the source trip titles in one join instead of one read per entry, and reordering a day's stops or sorting it by time writes every position in one CASE update. Results are unchanged: each day keeps its order (the id settles a tie), a missing trip still names null, and an id listed twice keeps its last position.
2026-10-10 20:15:56 +02:00
Maurice 4a3c97e500 docs(server): name the query API per statement shape and hold QueryBuilder calls to a baseline
server/CLAUDE.md now says which API new repository code uses: Kysely for join, aggregate and set-based statements, the entity methods for single-table CRUD, no new QueryBuilder calls. The TrekRepository docstring no longer calls Kysely an escape hatch. lint:query-api counts this.qb()/createQueryBuilder() per repository file against a baseline that may only shrink and runs in the server CI job.

The same file documents the JSON column and timestamp rules from the two changes before.
2026-10-10 20:15:56 +02:00
Maurice 91deedae8b refactor(db): JSON columns are decoded through one declared schema per column
decodeJson/decodeJsonResult/encodeJson read and write a JSON-in-TEXT column against the schema and fallback declared for it in db/json-columns.ts, and log a row that does not decode. reservations.metadata, the OAuth scope and redirect lists, consents, API token scopes, MFA backup codes, journey entry tags and pros/cons and poll options go through it. Readers that used to throw on a broken row (a hotel update, the ICS export, the price mirror, the OAuth token and session reads, a consent, a journey entry, a poll) now take the tolerant fallback the other readers of the same column already used.
2026-10-10 20:15:55 +02:00
Maurice a926990e4a fix(db): timestamp columns are written and stored in one canonical spelling
A DbTimestamp brand, produced only by dbNow(), types the expiry writes of OAuth tokens and password reset links, and collection edits stamp updated_at the same way. parseDbTimestamp() reads the stored text as UTC and replaces the oauth-local parser; utcSuffix lives once beside it. A data migration rewrites the ISO values those writers left behind, and the OAuth session lists keep answering the expiries in ISO.

Two outputs change on purpose. collections.updated_at used to come back in two spellings: a collection never edited answered the column default (2026-01-02 03:04:05), an edited one the ISO text its writer stored (2026-01-02T03:04:05.678Z). It now answers the canonical text in every case, in the PUT and cover responses and, once the migration has run, in every read; the shared schema types it as a plain string and the client does not read it. The ISO expiries of the OAuth session lists are rebuilt from the stored second-precision text, so they end in .000Z instead of carrying the milliseconds of the write.
2026-10-10 20:15:55 +02:00
Maurice b1da50466d ci(server): strict type errors per file may only shrink
tsconfig.strict.json is the build config with strictNullChecks and
noImplicitAny on. lint:strict runs tsc over it and holds the errors per file
to scripts/strict-baseline.json (204 in 109 files today): a file may only
lose errors, a new file starts at none, and an entry above its count fails
until --update lowers it. An error outside any file or an unreadable tsc run
fails closed. It runs in the server job; the build itself stays on
strict: false.
2026-10-10 20:15:55 +02:00
Maurice 1ea435d730 ci: server and shared lint stop rewriting files, and Prettier is checked per file
shared's lint ran eslint --fix with prettier/prettier as an error, so in
lint-prettier.yml the format:check after it only ever saw files it had just
rewritten, and the server's format scripts pointed at a test/ directory that
does not exist. Now lint is check-only in both (lint:fix applies fixes), the
server scripts cover src/ and tests/, and a lint:format ratchet on the shared
library holds every file outside scripts/format-baseline.json to Prettier in
both workspaces: in the server job of test.yml and in lint-prettier.yml,
which keeps providing the lint check. Formatting leaves ESLint in shared, and
the unused eslint-plugin-prettier goes. Once the tree is reformatted in one
commit, --update empties the baselines and the check is a full prettier
--check.
2026-10-10 20:15:54 +02:00
Maurice c9f66156d3 ci(server): lint:warnings holds the ESLint warnings per rule to a baseline
Server lint ran without --max-warnings or a baseline, so the rules left on
warn could grow without limit. lint:warnings runs ESLint with the server's
own config through the shared tally, fails on any error or inline rule
config, and holds the warnings per rule for src, tests and eslint-disable'd
messages to scripts/eslint-baseline.json, which only falls. It replaces
lint:check in CI. no-await-in-loop, no-non-null-assertion and no-console come
in as warnings held at today's counts. npm run lint no longer rewrites files;
lint:fix does.
2026-10-10 12:08:37 +02:00
Maurice cfef8742f4 docs(server): the Nest README is a blueprint again, and guides may only name paths that exist
server/src/nest/README.md drops from 988 lines to under 200: the module
shape with weather as the reference, the cross-cutting pieces, the guards,
parity and the test layout, without the bridge playbook and the services
recipe that contradicted the code. The migration log moves verbatim to
server/docs/nest-migration-history.md, marked historical.

scripts/ci/doc-paths.mjs resolves every backtick path in the CLAUDE.md files,
the root README, the blueprint and the wiki against the tracked files. The
paths already missing are listed in a baseline that only shrinks, and the
changes job runs it so docs-only changes are checked too. The wiki no longer
says the Playwright flows stay out of CI.
2026-10-10 12:01:07 +02:00
Maurice a4d0fc65f0 chore(i18n): drop the photos, register and stats locale files
None of their keys is read anywhere in client, server, shared or plugin-sdk,
neither literally nor through a key built from a prefix: the photo, sign-up
and stats screens they were written for are gone. All 27 locales lose the
same three files, so parity holds.
2026-10-10 11:54:10 +02:00
Maurice 7ee8333aeb refactor(server): types.ts keeps only imported types and derives Trip from the schema
Thirteen interfaces nobody imported are gone. Trip is now the selectable row
of the generated TripsTable, so currency, is_archived and reminder_days read
as the nullable columns they are and feed_token and reminder_sent_for exist.
2026-10-10 11:52:07 +02:00
Maurice 9a8191d367 chore: remove root scripts nothing runs any more
The two React profiler readers were one-off analysis helpers, and the i18n
migration read client/src/i18n/translations, which has been gone since the
locales moved to shared/.
2026-10-10 11:49:00 +02:00
Maurice db8babffc7 chore(server): remove the MCP config re-export and the dormant row-shape helper
Only their own tests imported them. The MCP knobs are parsed in
app-config/parsers.ts, which tests them directly, and toCamelCase was never
wired into a plugin RPC handler.
2026-10-10 11:48:59 +02:00
Maurice 2050e796eb chore(server): drop nodemon, @vitest/coverage-v8 and @types/uuid
Nothing runs nodemon (dev goes through scripts/dev.mjs), the coverage provider
is istanbul, and uuid 14 ships its own types. uuid itself stays: the OIDC and
Unsplash services import it.
2026-10-10 11:47:45 +02:00
Maurice 889afeae92 build(scripts): server ratchets share one baseline library at the repository root
scripts/lib/ratchet.mjs carries the tree walk, the CRLF-safe reader and the
baseline handling the client ratchets already use, next to a warning tally and
a format comparison that take ESLint and Prettier from the calling workspace.
server lint:size now runs on it with the same messages and exit codes, and the
libraries have node:test suites that CI runs in the server job.
2026-10-10 11:45:27 +02:00
Maurice 5644e3d77e fix(vacay): the desktop calendar no longer requests the trips again on every vacay refresh
Every vacay refresh on desktop (opening the page, a vacay:settings or
vacay:invite update from another member) sent a second GET /trips just to
redraw the same trip dots, even when the leave-year window had not changed.

Cause: useVacayCalendarLogic keyed the trip dot reload on the year settings
object when tripsFollowWindowShape was false (the desktop grid). loadAll()
replaces that object with a deep-equal copy on every refresh, so the effect
ran again. The phone screen already keyed it on the window shape.

Fix: the trip dots always reload on the window shape (year type, start month,
start day, hire date), which covers every field the dot filter reads. The
option only existed to keep the desktop behaviour, so it is removed from the
hook and from both callers.

Red test: FE-COMP-VACAYCAL-010 (useVacayCalendarLogic.test.ts) failed on the
unfixed code with "AssertionError: expected "list" to be called 1 times, but
got 2 times".

Changed assertions:
- useVacayCalendarLogic.test.ts FE-COMP-VACAYCAL-010: was "the desktop mode
  reloads trips on every new settings object" expecting tripsApi.list to be
  called 2 times after a deep-equal settings replacement; now expects 1.
- FE-COMP-VACAYCAL-011 no longer passes tripsFollowWindowShape and no longer
  names the phone mode; its assertions are unchanged.
2026-10-09 19:00:47 +02:00
Maurice 694e11c117 fix(vacay): a failed user load in the desktop invite and share dialogs no longer shows a stale list
When loading the users that can be invited to a fusion or given a calendar
share failed on desktop, the dialog kept showing the list from the last load,
so a user could pick someone who is no longer available and only learn it
from the failed send. The phone sheets already emptied the list and showed
their "no users available" state.

Cause: useVacayUserPicker only emptied the list on a failed load when its
clearOnLoadError option was true; the desktop VacayPersons and
VacaySharedCalendars dialogs passed false, a leftover of the old silent
catch in the desktop dialogs.

Fix: the hook always empties the list when the load fails. The option only
existed to keep the desktop behaviour, so it is removed from the hook and
from all four callers.

Red test: FE-COMP-VACAYPICK-002 (useVacayUserPicker.test.ts) failed on the
unfixed code with "AssertionError: expected [ { id: 2, username: 'bob' },
...(1) ] to deeply equal []".

Changed assertions:
- useVacayUserPicker.test.ts FE-COMP-VACAYPICK-002: was "a failed load keeps
  the last list on desktop" expecting available to equal USERS; now expects
  an empty list.
- FE-COMP-VACAYPICK-003: was "a failed load empties the list on the phone"
  through load() with clearOnLoadError true; now drives the same expectation
  (empty list) through a phone sheet reopening, since the option is gone.
2026-10-09 19:00:47 +02:00
Maurice 33db4ea7cb fix(dashboard): the desktop currency widget always shows the rate for the currency picked last
Switching the from currency of the desktop dashboard currency widget quickly
could show the rate of the previous currency: when the older rate request
answered after the newer one, its answer overwrote the newer rates.

Cause: useCurrencyConverter only aborted a request still in flight in its
phone mode (abortStale true); the desktop dashboard passed false and fetched
without any stale guard.

Fix: the hook now always aborts the previous request when a new one starts
and when the widget goes away, and ignores answers of an aborted request. The
abortStale option only existed to keep the desktop without the guard, so it
is removed and both dashboards call useCurrencyConverter().

Red test: FE-COMP-FXHOOK-008 (useCurrencyConverter.test.ts) failed on the
unfixed code with "AssertionError: expected 1.1 to be 1.3".

Changed assertions:
- useCurrencyConverter.test.ts FE-COMP-FXHOOK-002: was "the desktop mode
  fetches without a signal, the phone mode with one" expecting the desktop
  call to be [url] only; now expects [url, { signal: AbortSignal }].
- FE-COMP-FXHOOK-003 no longer names the phone mode; its assertions are
  unchanged. All tests call the hook without the removed option.
2026-10-09 19:00:47 +02:00
Maurice 1ad0ef6d78 refactor(settings): photo provider cards read their state from the shared hook
With the badge and toast options gone from usePhotoProviderConnections,
the desktop and phone cards called the hook identically and then derived
the same seven per card values (fields, config, values, badge, testing,
whether save and test have a route) line for line, which lint:dup counts
as a copied block of 25 lines in MPhotoProvidersSection.tsx.

The hook now hands each card those values through providerCard, and both
views keep only their markup. Nothing the user sees changes.

New test FE-COMP-PHOTOCONN-011 covers providerCard. No existing assertion
changed.
2026-10-09 19:00:47 +02:00
Maurice 24461ecf76 fix(admin): the default map template offers the Amap presets users can pick
In the admin's default user settings, the map template dropdown listed
seven tile presets, while every user's own map settings listed nine: the
same seven plus Amap and Amap Satellite. An admin running an instance for
people in mainland China could not pick the one basemap that works well
there as the default, short of pasting its URL by hand.

The Amap presets (57eac32f9) were added to the two user preset copies and
not to the admin's separate copy; nothing in that change or later gives a
reason, and the server stores any template as a default. The later
refactor kept the gap as DEFAULT_MAP_PRESETS beside MAP_PRESETS. There is
now one MAP_PRESETS list, read by the user settings and the admin defaults
on both desktop and phone.

Regression test FE-ADMIN-DUS-029 (DefaultUserSettingsTab.test.tsx) was red
on the old code: "Unable to find role="button" and name "高德地图 (Amap)"".

Changed assertions:
- useMapSettingsForm.test.ts FE-COMP-MAPFORM-001: it expected the admin list
  DEFAULT_MAP_PRESETS to hold 7 presets and the user list to add Amap on
  top; it now expects the one list to hold 9 presets including both Amap
  templates.
2026-10-09 19:00:47 +02:00