mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
3f2b60b00bc6e4e439e90b3132df5f4ccd8bbf50
245 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d88dc17e8e |
ci(release): build the bump commit, add a minor tag, make the bump an explicit choice
docker.yml built and published whatever `main` pointed to when the build and merge jobs started, not the commit it had just bumped and tagged, so anything that landed in between shipped under a version that did not contain it. build, merge and release-helm now check out the bump commit, the way docker-dev.yml already pinned its prerelease builds. The push of that commit fast-forwards main from the commit the run started on, so a race fails the release instead of skipping over it. Nothing tied a release to a green CI run. Both release workflows now refuse a commit unless every ci-ok check run on it has finished green, and docker.yml refuses to run on anything but main. bump=auto fell back to a patch whenever no prerelease was in flight, which would have published the ORM release, the largest change the project has made, as 4.3.4. auto now only finalizes a prerelease above the last stable tag and otherwise stops, asking for patch, minor or major. The inputs are unchanged. A patch that adds migrations gets a warning, not a refusal, since patch releases have carried them before. Stable releases also publish :MAJOR.MINOR, so an install can take the fixes of one minor line without being moved onto the next on day one. The tag tables in the wiki list it and the prerelease tag, and the root CLAUDE.md describes the release and prerelease flow. |
||
|
|
2f5d16ac74 |
build: pin the Node major in .nvmrc and declare the supported range
Nothing said which Node TREK runs on. The image is node:24, part of CI ran 22 under comments saying 22 matched the server runtime, and the dev guide said "22+". A bare-metal or dev install on an unsupported Node installed without a word and then failed at runtime: isomorphic-dompurify needs ^22.22.2 or ^24.15.0, the plugin jail's --permission flag is new in 22.x. .nvmrc now holds the major the Docker image runs, for nvm/fnm and for every workflow to read. engines in the root and each workspace states the range the runtime dependencies accept, the same one isomorphic-dompurify declares. Without engine-strict npm only warns outside it, so no existing install stops installing. |
||
|
|
48b1e879d2 |
fix(offline): an online edit waits behind an older queued write to the same entity
Holding later queued writes behind a parked one only ordered writes that were in the queue. Online, the repos send an edit straight to the server, so an edit made while an older write to the same visit, tour or driving settings was backing off or parked landed first, and Try again (or the next flush) then replayed the older write over it. Those writes carry no version token, so nothing refused the stale replay. The repos of visits, day clears, tours and driving settings now ask the queue before an online write. While an older write to the same entity is pending, in flight, parked or a conflict, the new write is queued too, with the same optimistic Dexie write as offline, and a flush is started so it goes out as soon as the one ahead of it does. When the driving settings left the cache since the older write, they are read in first. Places and packing items keep their direct online path: they send the base token, so a stale replay is refused with a 409 and becomes a conflict. What this does not cover, and the docs now say so: a collaborator's change to a token-less entity is overwritten by the retried write, as by any replayed offline write of those resources, and the planner's two time edits that call the API directly are not held. The wording of the parking commit also named budget items, which never go through the queue. |
||
|
|
b61a55f037 |
fix(offline): archived trips on the device follow the eviction rules again
Keeping archived trips stopped the sync from wiping them, but left nothing that ever took one off the device. The trip sync never refreshes an archived trip, and the 7-day rule and the per-trip switch only looked at the active list, so an archived trip stayed until Clear cache however long ago it ended. The stale and switched-off rules now also run over the archived trips that are stored here, with the same eviction as an active trip: its rows go, its parked changes stay. The offline settings list the stored trips the server's list leaves out, so an archived trip gets a switch the user can turn off; before, the switches came from the active list only and could not reach it. |
||
|
|
b4f705a049 |
fix(offline): a parked write holds back the later writes to the same entity
Parking a write the server keeps failing on unblocked its trip, so a later queued edit of the same budget item, visit, tour or driving settings went through while the older one waited. Try again then replayed the older write last and it silently overwrote the newer one: only places and packing items send a token the server could refuse it with. Before the parking change a 5xx stopped the whole flush, so this order was never at risk. The flush now holds back every pending write whose entity has an older write parked as failed or waiting as a conflict, and a write that parks during the pass holds back the rest of that pass too. Other entities and other trips keep syncing. Try again therefore replays the parked write first and the held ones after it, Keep mine does the same for a conflict, and Discard or Keep theirs flush right away so the held writes go out without waiting for the next trigger. Writes queued against an offline create that got parked now wait for it instead of failing as an unresolved temp id. |
||
|
|
237f82c0b6 |
fix(offline): an archived trip is not a deleted one, and eviction keeps parked changes
The trip sync clears every cached trip the server no longer lists, but it read only GET /trips, which leaves archived trips out. Every sync (login, reconnect, the periodic run) therefore wiped every archived trip from the device, pinned ones included, and with it their parked failed changes. A trip now only counts as gone when it is missing from both the active and the archived list, and when the archived list cannot be read nothing is treated as gone that round. clearTripData no longer deletes 'failed' rows. Stale eviction, a trip switched off in the storage settings and a trip that is really gone all go through it, and none of them is the user deciding to give up a change. A parked change of a deleted trip, or one the user was removed from, now stays in the queue: Settings > Offline counts it under failed changes and offers Try again (the server answers 403/404, so it parks again) or Discard. Dropping it silently would lose the only copy of an edit the user may still want to look at or redo elsewhere, and keeping it costs nothing more than a counter the user can clear in one tap. |
||
|
|
7cb4a09837 |
docs(wiki): say which images refuse a newer database on rollback
The rollback section promised that an older image refuses a migrated database and names the pre-migrate copy. Only images from 4.4 on carry that check. Going back to 4.3.x, the most common rollback today, gives neither the refusal nor the pointer, so the copy has to be restored by hand before the old image starts. |
||
|
|
bb5cc8f795 |
fix(db): one account per email address, whatever its case
Login, password reset and the sign-up checks all match emails case-insensitively, but the only constraint was the column's own UNIQUE, which SQLite compares case-sensitively. Two accounts whose addresses differ only in case (legacy rows, two sign-ups at once) left a login to resolve to whichever row came first, and LOWER(email) lookups had no index to use. A migration adds a unique index on lower(email). An install that already holds such a pair must still start, so the migration then leaves the index out, logs how many addresses are shared and by which user ids (not the addresses), and records email_case_index_pending. Since a migration runs once, db/email-case-index.ts runs on every boot while that flag is set: it repeats the warning until the accounts are told apart, then creates the index and clears the flag. The app-level checks keep guarding meanwhile. A write that passes the up-front check and still loses the race is now refused by the database. The users repository turns that unique violation into UserIdentityTakenError, and sign-up, admin create and edit, and the profile update answer it with the same 409 their checks give, instead of a 500 or an unhandled rejection (ADMIN-SVC-094 pinned the old behaviour and now pins the 409). The entity generator could not see an expression index (PRAGMA index_info names its column NULL), so PARITY-009c found a db index with no entity declaration. Rule 14 carries such an index into the entity as a named entry with its own statement; Users.entity.ts is regenerated. |
||
|
|
ff3615fdf0 |
feat(db): copy the database before a boot migrates it
Migrations only go forward, and since #2610 an older image refuses a database a newer one migrated. Going back after a bad update therefore needs a copy from before it, and the only copies were backups the admin takes by hand or auto-backup, which is off by default. The first boot on 4.4 migrates every 4.3.x install through the legacy baseline and the new migrations without either. Before applying pending migrations to a file database that holds anything, the boot now writes a VACUUM INTO copy next to it, named pre-migrate-<schema>-<time>.db (legacy-<schema_version> for an install the positional runner left, otherwise the last recorded migration), and keeps the newest TREK_DB_PRE_MIGRATE_SNAPSHOT_KEEP (3). The copy is written under a temporary name and renamed, so a crash never leaves a truncated file that looks good. If it cannot be written the boot refuses before anything is migrated; TREK_DB_PRE_MIGRATE_SNAPSHOT=false is the documented way past that. The newer-database refusal names the copy taken right before the newer release migrated past this one. A fresh install, a restart with nothing pending, an in-memory database and a restore's re-bootstrap (the archive is the copy) take none, and NODE_ENV=test leaves it off unless a suite sets the switch. |
||
|
|
bee500880e |
fix(i18n): every language says one file and five files its own way
Finishes the plural work the previous wip commit started. t() picks a count string's form by the language's own Intl.PluralRules, and every locale now spells out the forms its language reaches with whole numbers, about 160 count strings in all: one, few and many in Russian, Ukrainian and Polish, one and few in Czech and Slovak, five forms in Arabic, one in the others, none in Japanese, Korean, Chinese, Thai, Vietnamese and Indonesian. Each language was translated and then reviewed on its own. English no longer reads "1 files", "1 updates available" or "photo(s)". Count labels shown next to a number now take that number too: the atlas and admin statistics, the trip cards, journey and shared trip pills, packing templates, receipts, storage usage, the Studio export note. A heading that shared its key with a counted label keeps a key of its own (the boarding pass heading, the Bookings tab), so languages that put a noun after a number in the singular keep a plural heading. The plugin hosts chip passed its count through .replace() after t() and never got a form; three toasts sent the count as text. Mail and push texts for shared photos and suppressed storage failures pick their form in code through pluralForm. A whole number sent as text counts too, which is how in-app notification params arrive, and the plural rule follows the language of the strings on screen while another one still loads. The parity check knows which forms each language needs and refuses missing and impossible ones. It also reads keys that contain a colon now: it skipped them, which hid 58 missing plugin permission texts in Catalan and 35 in Vietnamese, both translated. The client parity test covers every language of the registry instead of a hand-kept list of 19, and the placeholder spec lets a form spell its number out only where its category is that one number. Brazilian Portuguese error screens were in Breton, and an Arabic dashboard label held Hebrew text. |
||
|
|
0126ef3401 |
fix(client): the version notice asks about a change during the session only
Three gaps in the notice from the previous commit, found in review. A page served from the network and claimed later by its own build's worker counted as already taken over, so Reload went to that worker, got the old build again and spent the session's reload: no notice and no new build until the next start. Only a worker that replaced the one that served the page skips the handover now, as at launch. A reload that came before the new worker could take over (a slow precache, a failed update, an install gone redundant) also spent that guard, so the launch behind it could no longer finish the handover with its own reload. That reload leaves the guard alone now. An install whose server never reports the bundle's version (a hand-set APP_VERSION) showed the notice in every session, and Reload could not clear it. The store keeps the first version a page hears of, from the app config at launch, and the notice only asks when a later report differs: a different build at launch is the launch handover's to fix. The server's wire-envelope test pins the welcome frame's fields and now expects the version beside the socket id. The wiki's update page says what an open tab shows after an update. |
||
|
|
e0efa0314a |
feat(tours): tours read and write through the offline core
A tourRepo replaces the direct toursApi calls: reads fall back to a new Dexie tours table, and a tour saved or edited offline is written there and queued like a place, under one temporary id for the place and its facet. The queue moves both to the real id on replay and drops both when the server refuses the create. Trip sync caches each trip's tours while the addon is on, so a tour no longer reads as a plain place offline. The tour metrics move to shared so the offline copy shows the server's numbers, and useTourPlaceIds keeps one Set per change as intended. |
||
|
|
57df945318 | style: plain punctuation in the new comments and test names | ||
|
|
40bce5554d |
docs: the key check, the restore guard, the new probes, failed offline changes and reminders
Updates the wiki for the changes in this branch: TREK refuses an ENCRYPTION_KEY that disagrees with the key file, the rotation script covers the AI Parsing key and moves the file along, a restore refuses a newer backup and keeps the replaced database, the chart's startup and readiness probes, PORT and extraEnv, retrying or discarding failed offline changes, trip reminders catching up, and why a Portainer recreate used to keep reporting the old version. |
||
|
|
de8caa81a2 |
Feat/orm : mikroorm (#2416)
* base entities
* update deps
* fix date as string
* rename entities
* migrations + seeders
* feat(db): DbTimestampType keeps DATETIME columns as the CURRENT_TIMESTAMP text
* feat(db): discover entities from an explicit list instead of a glob
* test(db): createTestOrm binds an ORM to a suite's own SQLite handle
* feat(db): dialect helper is the one place SQL functions are spelled
* test(db): dialect helper refuses malformed column references and day counts
* feat(db): snake_case entity shape and the first three repositories (Days, DayNotes, Trips)
* fix(db): repositories mirror the statement's column set and guard their row types
* feat(db): request-context and unit-of-work helpers for the ORM, with concurrency tests
* test(server): integration suites boot on the migrated schema snapshot
* test(server): snapshot harness keeps the legacy id semantics
* fix(test): route MikroORM's dynamic imports through vitest from the setup file, not the ORM config
* test(db): coverage ratchet entries for the ORM foundation
* test(server): websocket suites boot on the migrated schema snapshot
* test(server): coverage's uncovered-file pass goes through the SWC transform
* fix(db): dialect helpers dispatch on the platform; lint gate for the repository layer
* test(server): ORM suites open the schema snapshot; RESET_TABLES derived from the schema
* chore(server): promise safety net and sweep tooling for the async migration
* refactor(realtime, systemNotices, addons, airports, audit, categories, permissions, query-helpers, tags, transit, trip-membership): async DB-touching methods; transactions through UnitOfWork
* fix(server): observe every promise the async sweep left unobserved (timer, handshake, plugin-runtime test)
* test(doc-sync): unit suites await the methods the async sweep made async
* refactor(atlas, school-holidays, settings): async DB-touching methods; transactions through UnitOfWork
* chore(server): lint rule refusing a Promise used as a plain value, and the misses it found
* test(server): suites await the methods Task 1's forced edits made async
* refactor(budget, common, day-notes, days): async DB-touching methods; transactions through UnitOfWork
* test(oauth): controller suite awaits the async service
* chore(server): db-call-graph --unawaited lists calls to async methods whose result is used without await
* refactor(llm-parse, mcp-shared, oauth, place-shadow, route-usage, storage, todo, trip-invite): async DB-touching methods; transactions through UnitOfWork
* refactor(plugins): async DB-touching methods; transactions through UnitOfWork
* chore(server): db-call-graph --sync stops its transitive walk at an async callee
* chore(plugins): drop gate-only asyncs; observe the scheduler's rejection; async event-sink stub
* refactor(notifications, place-photos, backup): async DB-touching methods; transactions through UnitOfWork
* refactor(auth, files): async DB-touching methods; transactions through UnitOfWork
* test(server): suites await the methods earlier sweep groups made async
* refactor(tokens): async DB-touching methods; the auth token handlers await them
* test(files): upload filters fail closed on an excluded or unresolvable type
* refactor(collab, collections, maps): async DB-touching methods; transactions through UnitOfWork
* refactor(memories, photos, packing, share): async DB-touching methods; transactions through UnitOfWork
* chore(server): lint rule also refuses a Promise handed to expect() without resolves/rejects
* refactor(vacay, admin, doc-sync): async DB-touching methods; transactions through UnitOfWork
* fix(vacay): type the loop accumulators the async sweep introduced
* refactor(journey, oidc, place-enrichment, tokens, trip-members): async DB-touching methods; transactions through UnitOfWork
* fix(server): the journey hooks are awaited by their callers, not detached
* refactor(assignments, public-api, accommodations, places): async DB-touching methods; transactions through UnitOfWork
* refactor(trips): rewrite AccommodationsService.createAccommodation .bind alias
Recipe R4 trap named in the Task 12 brief: a .bind alias of an
AssignmentsService/AccommodationsService method is typed any and hides a
missing await from tsc and the promise lint rules. createAccommodation
belongs to this task's group, so rewritten as an arrow forwarding to the
method instead of a bound reference.
* refactor(assignments): drop the unused Trip type alias
* refactor(reservations, booking-import, calendar, integrations, roadtrip): async DB-touching methods; transactions through UnitOfWork
* refactor(roadtrip, reservations): return await on transactional; document the single-flight promises
* refactor(trip-read-model, trips, feeds): async DB-touching methods; transactions through UnitOfWork
* refactor(server): remove DatabaseService.transaction — every transaction runs through UnitOfWork
* refactor(trips, feeds): drop added return annotations; no double await on the feed guard
* fix(server): phase 1 review follow-ups — awaited returns, logged upload-filter rejections, covered handshake/notice branches, call-graph gate in CI, dead test imports
* fix(server): upload filters answer multer once on a throwing callback; positive call-graph gate test
* test(db): entity metadata must match the migrated schema; fix the four drifts it found
* feat(db): primary-relation entities proven (BudgetCategoryOrder, VacayUserSettings)
* test(db): parity gate also holds declared pivot entities; per-field column types; missing-set ratchets both ways
* test(db): numeric ids for the primary-relation spike
* feat(db): re-runnable entity generator encoding the snake_case/DbTimestampType rules
* fix(db): generator guard rails — validation cannot mask hidden()/index loss; typed JSON; loud failures
* fix(db): generator keeps NOT NULL boolean defaults; fails if a JSON interface import survives
* refactor(db): every entity in the snake_case shape; document-sync entities added; one repository per entity
* fix(db): no NaN class-field initialisers; entity generator --check gate in CI; parity normaliser keeps literal case
* chore(db): entity --check reports stray files; class-field census is a ratchet; migrate-encryption typechecked
* feat(db): AppSettings and Users repositories; forFeature wiring pattern
* refactor(permissions): permission flags through AppSettingsRepository
* fix(server): boot-time settings read runs inside an explicit ORM request context
* refactor(audit): audit log writes through AuditLogRepository
* fix(db): PK reads refresh past the identity map; typed owned-lookup generics; e2e ORM module mirrors production config
* docs(db): owned-lookup call-site convention; refresh side effect; e2e ORM module spread notes
* fix(audit): audit rows are native inserts, not a unit-of-work flush; closed test ORMs
* fix(server): non-HTTP entrypoints run inside an ORM request context; permission loads fail closed on context errors
* refactor(categories, tags): repositories replace raw SQL
* refactor(addons): addon catalog and photo providers through repositories
* fix(db): generator captures inline UNIQUE constraints
SqliteSchemaHelper.getIndexes() drops every unnamed sqlite_autoindex_
entry before entity-generator ever sees it, so a table-level
UNIQUE(...) constraint with no separate named CREATE UNIQUE INDEX
(settings' UNIQUE(user_id, key) among ~40 others) never reached the
generated entity's uniques: metadata. Adds a general
RULE9_addImplicitUniqueConstraints (skips a column set identical to
the table's own primary key — AppSettings.key/Addons.id noise) fed by
a PRAGMA index_list/index_info scan, and regenerates every entity.
* chore(addons): well-known gate wraps only the addon read in a request context; drop unused test factories
* fix(platform): well-known gate tolerates a synchronous addon read from the context helper
* fix(db): PK lookups inside patch refresh past the identity map; corrected identity-map notes
* refactor(settings): user and instance settings through repositories
SettingsRepository (getForUser/upsertForUser/getOne/deleteForUser) replaces
the per-user settings table's raw SQL; SettingsService drops DatabaseService
entirely. instance-api-keys.ts's readInstanceApiKey/writeInstanceApiKey/
resolveApiKey take an explicit AppSettingsRepository/UsersRepository instead
of a raw connection.
An earlier version resolved those two repositories itself from the active
MikroORM request context, so none of its six callers needed to change. That
worked in production and e2e (always inside a real request) but broke
service construction in plain unit tests across every calling domain
(addons, auth x2, maps, transit, unsplash - 185 failures on a full run),
since none of them wrap a request context. Each of those six services now
injects its own AppSettingsRepository/UsersRepository and passes them
through explicitly, matching how every other converted repository in this
migration is consumed; their own SQL is untouched (outside Plan 3a).
* test(server): harness constructors follow the Task 5 repository signatures; one test ORM per handle in the plugin host
* fix(db): generator names relation properties in captured UNIQUE constraints
* fix(settings): drop dead wiring and the speculative deleteForUser; strict row assertions
* fix(server): plan 3a review follow-ups — fail closed without an ORM at every choke point; seam and D5 ratchet tests; dead method and DDL copies removed
* fix(server): plan 3a re-review follow-ups — plugin boot activation and the airports backfill inside a request context; the supervisor answers the child before failing closed; BOOT-SWEEP-001 drives the real boot hooks; ws adapter seam pinned
* feat(db): generated entities declare their repository type; test casts dropped
* fix(server): plan 3a re-review 2 follow-ups — absent-ORM boot branch pinned; airports boot hook never rejects; BOOT-SWEEP-001 asserts the seven routes; duplicate comment removed
* feat(server): pre-init photo route and WS handshake run in a request context; InviteTokensRepository
* refactor(auth): users table through UsersRepository; JWT and MFA lookups repository-backed
* refactor(tokens): MCP and API tokens through McpTokensRepository
* refactor(auth): passkeys and registration invites through repositories
* fix(db): row-out repository reads bypass the identity map — stale write-back over nativeUpdate at flush; insertUser via em.insert; settings round-trip pinned
* fix(server): non-numeric route ids answer the legacy not-found, never NaN (tokens, categories, tags); mint path back to two statements; identity-map regression pinned
* refactor(oauth): OAuth server tables through repositories
OauthClientsRepository/OauthTokensRepository/OauthConsentsRepository
replace OauthService's raw SQL (OA1-OA33). revokeChain's recursive CTE
and the oauth_clients join (its FK actually targets client_id, not the
row id the generated relation assumes) go through em.getKysely();
OA19's chain revoke is one nativeUpdate($in); OA12's consent grant is
em.upsert on the (client, user) unique key. revokeAllForUser built now
for Task 5. Every row-out read is disableIdentityMap: true; inserts go
through em.insert.
* fix(auth): non-numeric passkey and invite ids answer the legacy 404, never NaN; invite join type pinned
* fix(db): generator emits the referenced column for non-PK foreign keys; OAuth client joins through the relation; FK parity un-skipped
* refactor(db): TrekRepository base — every repository path validates the request context and reads bypass the identity map by default
* refactor(auth): AuthService and account cleanup through repositories
* refactor(oidc): OIDC identities and settings through repositories
* fix(auth): invite validation keeps expires_at null on the wire; password transaction scopes and reset-token burn pinned
* fix(server): plan 3b close-out — case folding on both sides for identifier lookups (non-ASCII login regression); coverage ratchet restored; repository scaffold and instanceof ratchet on TrekRepository; D-shape tests load-bearing
* refactor(server): trip-access primitives async at every caller
* refactor(trips): trip access through TripsRepository; demo seed in a request context; dialect helpers for the trips cluster
* refactor(days): days and day notes through repositories
* refactor(assignments): day assignments through repositories
* refactor(server): trip-membership, place-shadow, place-enrichment, place-photos and query helpers through repositories
* refactor(trip-members): membership, guests and the trip read model through repositories
* refactor(places): places, tags, ratings and place images through repositories
* fix(server): place-shadow source counts ordered in SQL; identity-map ratchets load-bearing; photo-cache race pinned
* refactor(places): import paths through repositories
* refactor(trips): trips through repositories
* fix(server): trip ids parsed once at the gate for places and assignments; NUL-safe value quoting on the SQLite platform; places parity tests completed
* refactor(trips): trip copy through repositories for the owned tables
* fix(server): plan 3c close-out — non-finite ids bound as NULL, trip id parsed once at every places/days/ws gate
Platform: NulSafeSqlitePlatform now renders a bound NaN as NULL and
+/-Infinity as 9e999/-9e999, matching better-sqlite3's own legacy binding
(rule 22 extension) — the program-wide fail-safe for any caller that still
passes an unguarded Number(untrusted) into a converted statement.
PlacesService.verifyTripAccess and RealtimeGateway.handleJoin stop doing
that: verifyTripAccess parses once with toRowId and threads the parsed id
through create/import/bulk/rating/export.gpx (deleting the seven
toRowId(tripId) ?? -1 sites), and handleJoin gets a Number.isFinite guard.
DaysService gets the same treatment across list/create/getDay/reorder/
insert/resyncAccommodationDays, closing the gap where a hex-spelled trip id
could renumber a real trip's days while the raw-bind inverted-accommodation
guard silently matched nothing.
DatabaseService.getTripTitle is deleted; PlacesService injects
TripsRepository directly and calls getTitle itself.
Template/cheap-item cleanup: DayAssignmentsRepository.listForTimeSort uses
$castTo instead of an unknown bridge; the duplicated TagRow interface in
Places.repository.ts is now imported from Tags.repository.ts;
AssignmentWithPlaceRow extends the guarded DayAssignmentRow instead of
restating its columns; PlaceShadowPicksRepository.totals drops its
unreachable ??-fallback branch; Places.repository.ts's updatePlace types
the NOT NULL name column as string; trip-members.service.ts's
transferOwnership threads one parsed trip id through all three writes and
corrects two stale "raw-bind seam" docstrings that were actually Number()
seams; a dead canAccessTrip mock is removed from reservations.e2e.test.ts.
Tests: the 23 new no-explicit-any warnings in TRIP-SVC-069 are typed
(lint back to 2416); HTTP and WS regression coverage for the non-finite
and non-canonical id shapes across places/days routes, mutation-proved;
TRIP-JOIN-005 asserts the exact concurrent-join settlement instead of
>= 1; a new tests/unit/db/transaction-yield.test.ts pins rule 24's
measured transaction-yield/dirty-read/nested-BEGIN behaviour.
* test(server): plan 3c close-out — copy-source full-row parity, Kysely rollback proofs, identity-map relabels, trips e2e on the snapshot DB
* fix(server): plan 3c close-out — days create/insert answer the guard 404 for non-canonical trip ids instead of 500; exact settlement and cross-request yield ratchets
* feat(server): plan 3d groundwork — dialect helpers, visibility predicate, feed and cron context ratchets
* refactor(roadtrip): roadtrip tables through repositories
* refactor(reservations): reservations, endpoints and travelers through repositories
* refactor(feeds): ICS feed tokens and anonymous feeds through repositories
* refactor(accommodations): stays through repositories inside the existing transactions
* test(server): plan 3d — MCP test helper constructors follow the accommodations and days services
* refactor(trips): trip copy through repositories for the roadtrip, accommodation and reservation tables
* refactor(calendar): calendar reads and reservation visibility through one predicate
* feat(server): plan 3e groundwork — packing visibility predicate, memories access-gate audit
* fix(server): plan 3d close-out — legacy text shape for stored accommodation ids restores the date-change restamp; reservation ids parsed once at the gate; parity tests for the reservation repositories
* refactor(photos): trek photos, memories access and cache sweeps through repositories
* refactor(files): trip files and file links through repositories, with transactions added
* test(server): plan 3e — hand-built FilesService, FilesRpc and AllowedFileTypesService calls follow the repository constructors
* refactor(memories): unified, immich and synology photo sync through repositories
* refactor(collab): notes, polls, messages and links through repositories
* refactor(budget): budget items, members, payers and settlements through repositories
* refactor(todo): todo items through repositories
* refactor(packing): packing items, bags and templates through repositories
* feat(server): plan 3f groundwork — systemNotices DI shape, TRAVELER_OWNS predicate, ntfy/storage-health specs, composite-PK pins
* refactor(school-holidays): country and region catalog through repositories
* refactor(notifications): in-app notifications and channel preferences through repositories
* refactor(notifications): reminder crons, mailer, ntfy and webhook transports through repositories
* fix(server): plan 3e close-out — repository registrations restore the boot; packing assignee and bag updates parse and no-op as legacy; coverage and scoping ratchets for the budget, packing and collab repositories
* refactor(vacay): vacation plans, entries, shares and holiday calendars through repositories
* refactor(system-notices): notice dismissals through a Nest provider and repository
* refactor(atlas): visited countries, regions and bucket list through repositories
* feat(server): plan 3g groundwork — GALLERY_CHRONOLOGICAL_ORDER Kysely ordering, unixEpochToIsoKysely helper, composite-PK DDL pins, JB5/R7 confirmations
* refactor(journey): journey and trip access control, CRUD and the trip-sync engine through repositories
* feat(server): plan 3h groundwork — date-offset dialect helpers, entity/repository fitness confirmations, pre-init and encryption pattern confirmations, survivor re-grep
* fix(server): plan 3f close-out — ntfy operator-scope test on the live send path; vacay date-offset and shift-order parity; todo reminder window; coverage and parity ratchets for the atlas and vacay repositories
* refactor(journey): journey stats, entries, photos, contributors and suggestions through repositories
* refactor(journey): public share links, TREK Studio books and the journal facades through repositories
* refactor(collections): access model, hydration, export/import and list CRUD through repositories
* refactor(server): journey-table survivors in trips, photos, memories, auth-cleanup and plugin-contributions through repositories
* refactor(integrations): dawarich connection, sync, tracks and review-flow through repositories
* refactor(collections): saved places, copy-to-trip, labels and invitations through repositories
* refactor(integrations,booking-import,airports,maps,route-usage): airtrail, booking import, airport backfill, maps cache and route-usage counters through repositories
* refactor(doc-sync): connections, reconciler and controller-SQL relocation through repositories
* test(integrations): add AirTrail saveSettings tx-asymmetry ratchet (Plan 3h Task 4 addendum)
* fix(server): plan 3g close-out — transactional book first-save, skeleton reconcile and share-link create; unknown-key entry patches are no-ops again; parity and authorization pins for the journey repositories
* feat(server): plan 3i groundwork — AuditLogRepository's reserved methods, the demo connection-swap spike, repository-fitness confirmations
* feat(server): plan 3j groundwork — scheduler onto CronRegistrarService, SDK-contract confirmation, repository fitness
* refactor(storage): backend/category config and usage stats through AppSettingsRepository; fix the seed-file test race and the shared-replica migration-flip gap
* refactor(share): public share links, the pre-init photo fallback, and collection-places survivors through repositories
* feat(server): plan 3j — the withCamelCase utility, unwired, pending a contract decision
* refactor(admin): user CRUD, stats, audit log, version check and addon config through repositories
* test(admin): pin createUser's pre-existing non-transactional uniqueness-check race (AD2-4)
* refactor(plugins): the trip-write role gate through a repository, in isolation
* refactor(demo,backup): seed and reset through DemoRepository, live-connection backup statements through MaintenanceRepository, isDemoUserId through an injected DemoService
* fix(server): plan 3h close-out — new-value-wins COALESCE restores stay/reservation propagation; doc-sync state filter truthiness; request-context and parity ratchets for share, doc-sync and the integrations
* refactor(plugins): runtime lifecycle, config CRUD and the uninstall cascade through repositories
* fix(server): plan 3i close-out — DemoModule imported where its service is consumed (e2e restored); admin addons list selects the provider relation; saveDemoBaseline awaited; backup tests on the maintenance repository
* refactor(plugins): outbound OAuth tokens and per-user plugin settings through repositories
* refactor(plugins): install-time gates, the capability-audit hash chain and the registry through repositories
* refactor(plugins): contribution-hook controllers, the small plugin controllers and the plugins-table survivors through repositories
* test(server): plan 4 — collapse mcp tool test harnesses onto the ORM snapshot
* test(server): plan 4 — collapse nest service test harnesses onto the ORM snapshot
* fix(server): plan 3j close-out — serialized audit-chain appends, atomic metadata and scheduler limits, rollback and race proofs for the plugin repositories
* refactor(server): plan 4 — convert the program's orphaned raw-SQL callers
* fix(server): plan 4 — un-skip PARITY-009 (FK deleteRule and index-name parity)
* refactor(server): plan 4 — inline canAccessTrip/isOwner off DatabaseService (trip-domain services)
* refactor(server): plan 4 — inline canAccessTrip/isOwner off DatabaseService (plugins + MCP surfaces)
* fix(server): plan 4 — RULE12 paren-count scanner ignores string literals
The entity generator's RULE12_fixGarbledCheckExpressions CHECK-expression
paren-balance scanner counted every `(`/`)` character regardless of
whether it fell inside a single-quoted SQL string literal. A CHECK
expression like `status IN ('confirmed)', 'tentative')` has a literal `)`
inside its first string value; the scanner would hit depth 0 at that
in-string character and truncate the expression, losing its own real
closing paren. The scanner now tracks single-quote string state (handling
the doubled `''` SQLite escape) and skips parens while inside one.
Latent-bug fix — no shipped entity's CHECK expression has hit this shape
yet (3d's ledger: "nothing affected today").
* refactor(server): plan 4 — unify the trip-access predicate's Kysely copies
TripsRepository.findForViewer/listForUser/activeTrip each hand-wrote the
identical trip-access boolean (t.user_id = ? OR m.user_id IS NOT NULL)
through the Kysely expression builder. Task 7's security review (M1)
evaluated a shared helper and set it aside for time, falling back to the
TRIPREPO-041 cross-method parity test as a regression guard instead.
Extracted the boolean into one generic helper, tripAccessExpr
(_shared/trip-access.ts), following the reservation-visibility.ts
publicReservationExpr precedent: fully generic over <DB, TB> since the
predicate has no correlated subquery, so it composes across
TripSelectKyselyDB and ActiveTripKyselyDB without needing a narrower
fixed-alias shape. The LEFT JOIN itself stays hand-written at each call
site — tripSelectQuery's own docstring already explains why a shared join
helper can't be typed across these differently-joined outer queries.
feeds.service.ts and memories-access.service.ts no longer duplicate this
predicate at all (Task 2's facade-inline sweep already converted both onto
TripsRepository.findAccessible/getFeedTokenIfReachable/
listReachableActiveTrips, which read through the canonical MikroORM
QueryBuilder form, accessibleTripsQuery) — the remaining duplication this
task closes is the three Kysely copies inside this file.
TRIPREPO-041 (64/64 in the file) stays green as the unification's own
regression guard.
* fix(server): plan 4 — trek_photo_cache_meta.cache_key NOT NULL
The column was a bare TEXT PRIMARY KEY, which SQLite does not implicitly
make NOT NULL (unlike an INTEGER PRIMARY KEY rowid alias). Every writer
(TrekPhotoCacheMetaRepository.upsertMeta, the table's only INSERT path)
has always bound a real string — 3e's R5 confirmed this. Appends a new
migration rebuilding the table with cache_key NOT NULL (SQLite has no
ALTER COLUMN, so this follows the same rebuild recipe
Migration20200101020000 uses in the opposite direction) and updates the
entity/generated metadata to match. Schema tightening, not a behaviour
change.
* refactor(server): plan 4 — retire the safely-convertible string-form where conditions (rule 23)
Places.repository.ts's existsInTrip/findInTrip/findRaw/reclaimInputs/
listByTripAndIds and Trips.repository.ts's findPublicForShare all took a
raw `.where('col = ?', [v])` string condition even though their id/trip_id
parameters were already plain `number` (or, for findPublicForShare,
narrowed to `number` here after confirming its one caller,
share.service.ts#getSharedTripData, only ever passes a number sourced
from a Kysely-typed share_tokens row, never an unconverted URL param) —
converted all six to MikroORM's typed `.where({...})` filter object.
The remaining raw string-form conditions in src/db/repositories/** are
NOT converted here: they are the `number | string` raw-bind seam
findAccessible/isOwner's own docstrings describe (trip_id/id threaded
through unconverted from req.params), which structurally requires the
deferred guard-level single toRowId parse to land first — narrowing any
of those methods' signatures now would either break the '0x10'/'007' id
parity those docstrings warn about, or duplicate the deferred item's own
work ahead of it landing.
* refactor(server): plan 4 — trek-photos.repository.ts finishes its rename
3e Task 6 renamed the CLASS (TrekPhotosRepository -> TrekPhotoRegistrationService,
since the name collided with the generated ORM TrekPhotos.repository.ts) but
left the FILE and its test file's names unchanged. Renames both to
trek-photo-registration.service.ts / .test.ts.
The old path is kept as a thin re-export stub (the src/websocket.ts shape)
rather than repointed at every one of its ~20 importers in this pass: several
of those files sit in two sibling tasks' exclusively-owned windows on this
shared branch (Task 3's budget/packing/todo/accommodations/places ripple,
reached through tests/helpers/plugin-host.ts and mcp-test-controllers.ts;
Task 5c's held-back tests/unit/services/{trekPhotoMedia,conflictUpdate}.test.ts)
and are off limits here. Flagged for a follow-up once those tasks land: delete
the stub and repoint every remaining importer at ./trek-photo-registration.service.
* fix(server): plan 4 — plugin discovery's delete+reinsert pairs are atomic
install/discovery.ts#upsert refreshed a plugin's actions and settings-field
descriptors as two independent delete-then-reinsert pairs (DI5-DI8) with no
transaction spanning either pair, let alone both: a crash between a delete
and its own reinsert left that plugin with an EMPTY actions or
settings-field set until the next discovery run, instead of its previous
still-valid rows. Wraps both pairs in one uow.transactional, so a discovery
refresh for a plugin's descriptors is now all-or-nothing.
Deliberate behaviour change, named for the user per the task brief.
DiscoveryRepos grows a UnitOfWork; PluginRegistryService (the other
production caller, registry.service.ts) picks it up the same @Optional()
way PluginRuntimeService already does, with the same guard-throw. Updates
the two hand-constructed test call sites that actually exercise discovery
(discovery.test.ts, registry.test.ts) to pass a real UnitOfWork.
* refactor(server): plan 4 — facade inline sweep part B (rosterUserIds/getPlaceWithTags)
Inline DatabaseService.rosterUserIds onto TripMembersRepository.rosterUserIds
directly in budget.service.ts, budget.mcp.ts and todo.service.ts, and
DatabaseService.getPlaceWithTags onto PlacesRepository.findWithTagsAndRatings
in accommodations.service.ts (placesRepo was already injected there).
BudgetService/BudgetMcp/TodoService collapse their constructors, dropping
the now-fully-unused DatabaseService param (budget.module.ts/todo.module.ts
register TripMembers on forFeature). PackingService keeps DatabaseService as
a vestigial param behind an optional TripMembersRepository with a fallback
in tripRosterIds: an in-flight Task 5c file
(tests/unit/services/conflictUpdate.test.ts) hand-constructs PackingService
positionally without the new repository, so its arity cannot change this
task; the fallback keeps that file's DatabaseService.rosterUserIds spy
working until Task 4 drops the param with the class itself.
* test(server): plan 4 — constructor ripple for the rosterUserIds/getPlaceWithTags inline
Hand-built BudgetService/BudgetMcp/TodoService call sites drop the removed
DatabaseService constructor arg (budgetRepoArgs() now appends a
TripMembersRepository so most spread call sites need no other change);
budget.service.test.ts's rosterHas() helper and svc() factory move from a
spied DatabaseService to a stubbed TripMembersRepository. The
AccommodationsService helper (tests/helpers/accommodations-service.ts) and
its two direct callers drop the same arg. PackingService call sites are
untouched (its new param is optional, so every existing hand-built instance
still resolves rosterUserIds through the DatabaseService fallback).
* fix(server): plan 4 — thread the discovery UnitOfWork through PluginRuntimeService too
Complements the just-landed discovery atomicity fix (DiscoveryRepos.uow):
PluginRuntimeService's own discoveryRepos getter still built the bundle
without a uow field. Threads its already-injected, @Optional() uow through
so its own onApplicationBootstrap/reload/dev-link discovery calls also get
the transactional DI5-DI8 refresh, not just PluginRegistryService's.
No throwing guard here, unlike PluginRegistryService's: several hand-built
partial-DI-graph test instances of this class (boot-registry-order.test.ts,
in a sibling task's held file window) construct it with uow omitted and
exercise onApplicationBootstrap's discovery call directly. Discovery must
never refuse outright just because a test built a partial graph, so it
degrades to the pre-existing untransacted sequence when uow is absent
(DiscoveryRepos.uow's own docstring covers this), the same "boot must
never block app init" defensiveness this file already applies elsewhere.
* fix(server): plan 4 — guard-level single parse: verify + correct the overclaiming verifyTripAccess docstring (L-2)
TripAccessGuard/TripOwnerGuard already parse :tripId exactly once
(Number(), guarded by Number.isFinite) and hand the resolved TripAccess
row to the handler via request[TRIP_REQUEST_KEY], TripOwnerGuard reusing
TripAccessGuard's own lookup when both are on the same route rather than
re-querying — verified by reading both guards directly, no change needed
there.
PlacesService.verifyTripAccess's own docstring separately claimed its
returned `tid` gets "returned... for every downstream call to reuse
(never re-parsing)" (3c's Task 9 review, L-2, carried to Plan 4). Reading
every call site (not assumed) shows this is false: `.tid` has exactly one
reader (PlacesController.requireTrip itself), which discards it — none of
requireTrip's 13 callers read `.tid` off the trip it returns; each passes
the original raw tripId string on to the write/read method it calls next,
which toRowId-parses it again itself. That second parse cannot be
eliminated by this gate alone: the same service methods are called
directly by places.mcp.ts too, an entry point with no requireTrip gate at
all (its own numeric tripId is re-stringified, String(tripId), to call
them), so each method owns its own id validation regardless of entry
point. Threading `tid` all the way through would mean giving every one of
~15 methods a second, number-only call shape for the REST path alone to
use — a real refactor, out of this docstring fix's own scope. Corrected
the docstring to say so plainly instead of repeating the false claim.
* refactor(server): plan 4 — relocate JS7/10/13/14 off the journey-share fallback stub
JourneyShareTokensRepository's own JS7/JS10/JS13/JS14 Kysely reads were a
3g Task 3 fallback (Task 2's JourneyPhotosRepository/JourneyEntriesRepository/
JourneyEntryPhotosRepository were still mid-flight). Now that those
repositories are stable, relocate: JS7/JS10 (photo/asset validation joins)
onto JourneyPhotosRepository (already injected into JourneyShareService as
photosRepo, no constructor change); JS13 (public entry list) onto
JourneyEntriesRepository.listPublicEntries; JS14 (public per-entry photo
read) onto JourneyEntryPhotosRepository.listForPublicJourney, deduped
against that repository's own JP_COLUMNS select list instead of a
hand-duplicated copy. JourneyShareService gains two new @InjectRepository
params (both already registered in journey-domain.module.ts's forFeature),
rippled through mcp-test-controllers.ts and journey-share.service.test.ts
(P04/P05 parity tests now pin the new homes).
* refactor(server): plan 4 — document unreachable ?? default arms after single-row aggregates
3f Task 7's fix wave flagged a cluster of `row?.x ?? default` arms after
unqualified, ungrouped aggregate reads (MAX/COUNT/COALESCE(SUM,0)) as
"same class as OauthClients:147" but left them unexamined per-site. Decided
per site: document, don't delete — TypeScript still types
executeTakeFirst()'s result as `| undefined` even though SQL guarantees
exactly one row for these unqualified aggregates, so the optional chain
can't be removed without a non-null assertion (worse, not safer). Six
sites: JourneyEntries.repository.ts (maxSortOrderForDate JG42,
countStatsPlaces JG67), JourneyEntryPhotos.repository.ts
(maxSortOrderForEntry JG91), JourneyPhotos.repository.ts (maxSortOrder
JG88/JG99), VacayEntries.repository.ts (sumFraction VC1, sumCompFraction
VC2). No behavior change — comments only.
* refactor(server): plan 4 — unify the two user-data-erasure enqueue paths
UserCleanupService.erasePluginUserData (the account-deletion transaction's
own UC2/UC3 enqueue) and PluginRuntimeService.enqueueUserErasure (the
emitUserDeleted sink's post-commit enqueue) independently walked the
identical SELECT id, permissions FROM plugins + JSON.parse + hook:user-data
filter + INSERT OR IGNORE — and both genuinely run for every account
deletion, since every emitUserDeleted call site (auth.service.ts,
admin.service.ts, trip-members.service.ts's guest removal) fires
immediately after deleteUserCompletely.
Extracted the shared filter into enqueueHookUserDataErasures
(nest/plugins/user-erasure-enqueue.ts) and pointed both call sites at it.
UC2/UC3 also come off DatabaseService onto PluginsRepository/
PluginUserErasureQueueRepository in the same change (they were the last
raw statements standing in the way of sharing one implementation) — this
narrows the Plan 3b Task 5 "stays raw, owned by Plan 3j" ruling to UC1
only (plugin_user_config/plugin_oauth_tokens/plugin_oauth_state), which
still stays raw, unaffected. UserCleanupService's orphan-data-directory
scan (a plugin uninstalled with data retained) has no equivalent on the
enqueueUserErasure side and stays there, now via the same repository.
Both enqueue calls still run per deletion (INSERT OR IGNORE keeps this
idempotent) — this unifies the IMPLEMENTATION, not the call count; not
touching the emitUserDeleted event-sink wiring itself was a deliberate,
smaller-blast-radius choice for this task.
* refactor(server): plan 4 — narrow TripMembersRepository.remove/TripsRepository.setOwner to number
Both accepted trip_id: number | string per 3c's carry note (their "stays
loose for other callers" reasoning), pending the guard-level single parse
landing first. Re-checked every caller directly: setOwner's one production
caller (trip-members.service.ts#transferOwnership) already passes trip.id,
a real number resolved earlier in the same transaction; remove's callers
are trips.rpc.ts (num(), already a validated number), trips.mcp.ts (Zod-
typed tool input, already a number) and TripMembersController.removeMember
(the one genuine raw-string caller, :id off the route with no class-level
TripAccessGuard on this controller by design).
TripMembersController.removeMember now parses :id once (toRowId, not
Number() — rule 15's NaN-into-SQL trap) and threads the parsed number to
both its own access check and removeMember, instead of passing the raw
string through twice. TripMembersService.removeMember and both repository
methods narrow to number accordingly; setOwner's WHERE also moves off its
raw-bind escape hatch onto nativeUpdate now that trip_id is typed, and
remove's trip_id half moves onto a typed .where({ trip: trip_id }) filter
(user_id keeps its own raw bind — a different, still-live NaN-safety seam,
unrelated to this narrowing).
TRIPREPO-018 and TMEMREPO-023, which pinned the retired string-bind seam
on setOwner/remove, no longer typecheck against the narrowed signatures
and are removed with an explanatory comment in their place.
* refactor(server): plan 4 — U6 gate-level id parsing for todo item ids
Rule 21's carry from 3e Task 8's review: TodoController.update/.remove
passed the raw :id route string all the way to TodoItemsRepository,
relying on SQLite's column-affinity CAST to match it instead of a typed
filter. Parse :id ONCE at the controller (toRowId, matching Task 8a's
TripAccessGuard/removeMember shape), 404 on a non-canonical id, and thread
the number down through TodoService.updateItem/deleteItem into
TodoItemsRepository.findInTrip/existsInTrip/update/deleteById, all
narrowed from number | string to number. todo.rpc.ts's num()-derived
todoId no longer needs its String() wrapper. tripId stays string | number
— a separate, already-accepted carry (
|
||
|
|
b5514024fa |
docs(wiki): describe the map's places filters
Map-Features gets a "Filtering the places on the map" section: the markers follow the places list's Show, category and rating filters, and on a phone the Filters button of the plan map opens the same choices in a sheet. Places-and-Search says the rating floor narrows the map markers too. |
||
|
|
05b53d024e | docs(wiki): bring every page up to the next release; admin settings, user defaults and notifications get pages of their own | ||
|
|
88afdb986c | feat(mcp): upload_trip_file tool for adding documents to a trip (#1566) | ||
|
|
1c34a74498 |
feat(calendar): one feed event per leg of a connecting flight or train
A flight or train with several legs comes out of the calendar feed as one event per leg, titled with its route and carrying the leg's own departure, arrival and segment reference, so the layover shows as the gap between them. A booking with a leg missing its departure time stays one event, as before. (#2389) |
||
|
|
5138358d7b |
feat(net): the SSRF-guarded requests go through HTTP_PROXY / HTTPS_PROXY too
Place photos, link previews, webhooks and the integrations fetch through the guard's own dispatcher and so ignored the outbound proxy, which left a server that can only reach the internet through one without them. They now read HTTP_PROXY, HTTPS_PROXY and NO_PROXY themselves and connect through the proxy, after the guard has vetted the target. The Helm chart passes the three variables through. (#1754) |
||
|
|
885896aba2 |
feat(files): FILE_UPLOAD_LIMIT_MB sets the upload limit for files and notes
The 50 MB limit for files, booking attachments and note attachments becomes FILE_UPLOAD_LIMIT_MB, 50 by default. The server hands it to the client through the app config, so the upload dialogs refuse a file over it up front and say what the limit is; on desktop a file over it no longer vanishes without a word. (#1364) |
||
|
|
1fee38e8f7 |
feat(oidc): OIDC_USERNAME_CLAIM picks the claim a new account's username comes from
Providers like Pocket ID send the full name in 'name', which became 'JaneDoe'. OIDC_USERNAME_CLAIM=preferred_username builds the username from that claim instead. Unset, or with the claim empty, the old order applies (name, preferred_username, the email's local part), and it is only read when an account is created. (#1677) |
||
|
|
ba3dc65926 |
feat(public-api): next_trip in /api/v1/stats counts down to the next trip
The counterpart of last_trip for a homepage widget: the nearest trip that has not started yet, with its countries and the whole days until it starts, or null when nothing is planned. A trip under way stays last_trip, so the two never name the same trip. (#2542) |
||
|
|
f5103d04e0 |
test: follow the qol sweep in the e2e schemas and the assertions it changed
The hand-written e2e schemas gain the new columns and place_regions, and the unit assertions take the new arguments and fields. The Public API page names GET /api/auth/app-config for the running version. |
||
|
|
8fd46c4c18 |
feat: quality of life sweep from the feature discussions
Small requests from the discussions, each built end to end: planner and map (cycling mode, date first headings, clear a day, stay times per day, off-route stops, map lock, compact unplanned markers, pending stays, cable car, transit quick picks), places (location filter, category from OSM, open in maps, geo: link, image from an attachment, enrichment on file import, places near a pin, place name language, Amap first in China), lists and costs (packed counter, inherited sharing, Markdown to-dos, payment notes, bookings CSV, my plan PDF), sharing (unplanned places, travel only, without photos), journey (photo zoom, drafts, photo order, trip suggestion, day jump), atlas (region counter, region bucket list, places per country), vacay half company holidays, studio copy and paste plus the book's own layouts, and RESTORE_FROM_BACKUP for a first start restore. |
||
|
|
bd5a6cc748 |
fix(cors): the instance's own address no longer trips ALLOWED_ORIGINS
With ALLOWED_ORIGINS set, every request carrying an Origin that was not on the list was failed with an unhandled error, which the exception filter answered with a 500. A browser sends Origin on every POST and WebSocket upgrade, including same-origin ones, so opening TREK under any address the list did not name (the LAN IP, a second hostname, http instead of https) broke the login with "Internal Server Error" while another browser that used the listed address worked. A request whose Origin is the host it was sent to is now treated as same-origin and always passes, for REST and the /ws upgrade alike. A foreign origin is still refused, now with a 403 "Not allowed by CORS" and a warning in the log naming the origin to add. Closes #2543 |
||
|
|
7b0333a0c1 |
fix(roadtrip): a booked night no longer shows up mid-day when the day already ends at the stay
Booking a night writes the hotel onto its check-in day as a stop, seated first or at its check-in, so the drive reaches it even without hotel bookends. With "Start and end each day at your stay" on, the evening row already ends that day at the same hotel, so the road trip drove check-out, tonight's hotel, the day's places and back to the hotel again. Days hides that stop and draws the stay at the day's edges only. seatNightBookends now passes over the stop tonight's booking wrote whenever it seats that stay as the day's evening, in the browser and in calculate_roadtrip alike. The stops keep their stored index, a hotel the traveller placed stays, and the booking's stop stays while it is the day's last one or no evening is seated. With the switch off nothing changes. Closes #2546 |
||
|
|
64087512df |
fix(planner): the plan's costs follow the expenses in Costs
The day pills, Total Cost at the foot of the plan and the same figures in the PDF added up the price column of every planned place. No form sets that price since expenses replaced it, and nothing in Costs changes it, so deleting an expense left the plan's total where it was, and a place planned on two days counted twice. With the Costs addon on, the plan now adds up the expenses: each one once, on the day its booking starts, else the first day its place is planned, else its date, and in the total either way. A foreign expense counts at the rate frozen when it was entered and a refund nets against its day, so Total Cost matches Total trip spend in Costs. With Costs off, a place's own price still counts, once per place. Closes #2551 |
||
|
|
bbf8d94f9f |
feat(share): the public trip page in the planner's design
The page a share link opens is rebuilt from top bar to footer: the planner's light top bar with the language picker, the trip as a hero made of its cover, a tab bar that stays in reach, the days as the planner's cards beside a map that stays in view, bookings as the booking cards, packing and costs as cards with their progress and shares, the chat as bubbles, and empty states for sections the owner shared with nothing in them yet. The map picks its day in a select with a step back and forth instead of a row of chips a three-week trip did not fit into. |
||
|
|
3901aecbc0 |
feat(trips): Share and Save to list in the new dialog look
Share opens on the shared dialog frame: the trip's name and the number of people in the head band, members and guests on the left, the public link and the invite link as two cards on the right. Rows carry their actions as icon buttons named by their tooltips, and removing a member, leaving, handing over the trip or removing a guest asks in the app's own dialog instead of the browser's. The invite link can no longer be created twice by a double click. The phone, which opens the same dialog from its More sheet, keeps the sheet it had; both run on one set of hooks. Save to list has the place's name in the head band, Mark visited as a pill beside it, the lists as rows with a tick, and View at its foot. The public link's hint loses its dash in every language, and the help guides and wiki pages of the Share dialog show the new look. |
||
|
|
af9710c618 |
docs(wiki): the pictures that were missing, and the follow-ups of the rewrite
The wiki pictures of the trip are taken again with the fixtures the help guides use, so the Bookings tab shows a booking of every kind, Files has documents and What's Next has something ahead. New pictures: the map's hover card, the Subscribe to calendar dialog, the PDF preview, the To-Do view, and the Transports tab's own timeline and booking detail. Day selection in the screenshot run clicks the day's number, now that the middle of a day header is the booked night. Home, Real-Time Collaboration, Road Trip, Map Features and the bookings page are brought in line with the rewritten pages, and sixteen assets no page uses any more (mostly old GIFs, 22 MB) are removed. |
||
|
|
cb7014b378 |
feat(costs): table view, and the wiki and help center for the new trip page
Costs gets a second view next to the ledger: the expenses as a table grouped by category, with name, date, total, persons and days editable in place and the per person and per day figures worked out beside them. A total with a payer or in a foreign currency stays locked and opens the expense instead, so balances and frozen rates are never changed behind the dialog. In the table view the side column shows a summary by category, day, payer and status. The switch sits in the bar and the choice is remembered; the ledger stays the default. On the Plan tab a place's reservation shows as one Reservation pill whose colour carries the status and which opens the booking, and the drop line of a drag that lost its source row now clears on the next mouse move. The booking detail formats its times in the user's clock, and a hotel without a check-out runs to the end of the last day in the timeline. The wiki pages of the trip are rewritten for the new layout, with new screenshots from the screenshot run (new Plan tab, bookings, transports, dialogs, costs table). The help center guides of the Plan tab are rewritten and translated into every locale, and every new string is translated. |
||
|
|
237f42a493 |
fix(llm-parse): harden photo reading and finish the receipt scan
A photo is now decoded only when its header says it is within 40 megapixels, and bytes TREK cannot shrink, such as a WebP, are sent only up to 3.5 MB. A scanned PDF page is drawn by its long edge, and pdf.js skips an embedded image above the same pixel cap instead of decoding it. The receipt reading is held to bounds in the shared contract, keeps the minus sign of a discount and leaves such lines out so the Ticket split can save, and its prompt states the JSON shape, so a server without json_schema still answers something the parser reads. The model lookup sends the configured key, the import error names the photo formats, the scanned photo is attached only for someone who may upload files, and the phone settlement reloads after a receipt is saved from the trip sheets. Test ids that clashed after the rebase are renumbered, and the wiki describes the image setting, photo import and receipt scan. |
||
|
|
b1c05bca83 |
feat(llm-parse): read a photo, as a booking or as a receipt in Costs
Whether the AI Parsing model reads images is an admin setting now, and it lands with the two features that send a photo, so it has something to control. The setting is `vision: 'auto' | 'on' | 'off'` on the addon config. On Automatic a local Ollama is asked what the model can do (`/api/show`), and a cloud provider means no; Yes and No are applied as set. The old `multimodal` flag is not read: the admin screens wrote it false on every save, so a stored false meant nothing. The per-user toggle keeps its on/off meaning and is read by the extraction now. The lookup is server-side only: capped read, validated body, never throws, cached per server and model. GET /api/llm/capabilities answers it for the signed-in caller, and both photo pickers below ask it. The booking import takes JPG, PNG and WEBP when the model reads images. The photo goes as an image to every provider (image_url, an Anthropic image block, Ollama's native chat with `images`), capped at 1600px, and skips KDE Itinerary. A scanned PDF, which used to reach the model as empty text, has its first two pages drawn by pdf-parse and sent the same way. The rest is the import as it was: background job, review, the file attached to the booking, the linked cost. On a real qwen3.5:4b a train ticket came back as bare clock times until the router gave the model every type's field meanings, and its price as an object inside a string, which read as 4900; both are handled. Costs gets Scan receipt: a dialog laid out like the booking import's takes one photo, read on the import job queue, then reviewed in the existing expense editor pre-filled with merchant, total, currency, day and lines, the photo waiting in Receipts & Invoices to be uploaded on save. No table, no migration, no category guess, no document-type detection. |
||
|
|
d1dc8542ac |
fix(costs): leave out an expense no rate is known for instead of counting it 1:1
A foreign expense without a frozen rate is converted with today's rate. When the server could not fetch rates itself, it counted the amount 1:1 in the trip currency, although the browser had the rates all along. A VND bill on an AUD trip was settled as millions of AUD. Such a row is now left out of the balances, flows, final budgets, trip totals and per-person summaries. The settlement reports it under `unconverted` and says which currency its figures are in; the trip summary lists the ids and the budget prompt says how many are missing. The browser lends the rates it already holds (`fallback_fx`) when it saves an expense or a settle-up transfer, and to heal existing rows through the new POST .../budget/freeze-rates route, which an editor's Costs view calls for the rows the settlement left out. The server still prefers its own rate, uses a lent one only while it is quoted against the trip currency, and never replaces a frozen or explicit rate. The settlement also takes the browser's display rate (`base_rate`) in place of a display quote the server cannot fetch; the browser leaves out a figure the query would refuse. The browser's own fetch gives up after 10 s. Changing a row's currency while no rate is available no longer keeps the old currency's rate; the row goes back to unfrozen. MCP gets the same through the new freeze_budget_rates tool, which only ever pins the server's own rate. |
||
|
|
a5745fd856 |
fix(ssrf): connect past a link-local DNS record instead of refusing the host (#2506)
Since 4.3.0 the guard resolves a name to every address it has, and a single blocked address refused the whole name. LAN DNS servers commonly publish a host's fe80:: address next to its IPv4, so a self-hosted IdP on such a name failed every login with "Requests to link-local / cloud-metadata addresses are not allowed". Immich, Synology and the other strict-guard integrations on the same kind of name failed the same way. Both guards now leave a blocked address out of the list the socket is pinned to and connect over the rest, and a name that resolves to nothing else is still refused. A private address among the remaining ones still needs ALLOW_INTERNAL_NETWORK under the strict guard, and ALLOW_LINK_LOCAL_IPS works as before. Leaving addresses out is only safe if the blocked set is complete, so the strict guard now blocks everything the admin lane does. The AWS IMDSv6 (fd00:ec2::/32) and Alibaba (100.100.100.200, 100.100.100.100) metadata addresses sit in ULA and CGNAT space, which ALLOW_INTERNAL_NETWORK opened to any URL a user can type. Both guards also read an IPv4-mapped or IPv4-compatible address as the IPv4 it carries: Node prints an AAAA record of ::a9fe:a9fe as ::169.254.169.254, which passed both guards, and ::ffff:100.100.100.200 passed the admin lane. Neither guard connects to a link-local or metadata address now, and the strict guard never connects to loopback either. |
||
|
|
8864edda18 |
feat(budget): link existing expenses to bookings and places, several per booking
Discussion #2084: a cost bought later (the seat upgrade for a flight) could not be tied to the booking it belongs to, and a booking held one expense. - The Costs block of the booking, transport and place forms lists every linked expense with edit, unlink (the expense stays in Costs) and delete, and offers the expenses that belong nowhere yet under Link existing expense. The phone sheets get the same block in their own design, and their Files row lists the attached files and links an existing one. - PUT /budget/:id, update_budget_item (MCP) and costs.update (plugin RPC) take reservation_id / place_id; create and update refuse an id from another trip on all three paths. - The booking's mirrored price is the sum of its expenses when they share one currency (none counts as the trip's), else the first one, with that currency, and follows every change to a linked expense. The booking form's price field leaves a booking with several expenses alone. - Deleting a booking deletes all of its linked expenses and broadcasts each one, also from the MCP delete tools. - Cards show the price with its currency. - Booking and transport dialogs: Booking Type and Travelers as dropdowns at the top, the URL beside Files (transports gain the URL field). The row is one shared component; linking and unlinking files go through the store. Help center pictures and texts for the booking, transport and expense guides are redone in all 27 languages, five Costs labels that were still English in 16 languages are translated, and the wiki follows. |
||
|
|
1f79776070 |
feat(todo): To-Do in the look of the packing list, detail pane always in reach
Rows get the packing list's filled checkbox, priority, due date and list as quiet badges, and the assignee as an avatar with the name as its tooltip. The tasks sit in a card whose head names the filter and holds the sort, Priority or Due date, which moved out of the sidebar; a second click on the active one goes back to the manual order. The progress card matches the packing list's. The detail pane is a card with a box in its head that ticks the task off, and Delete and Save changes stay in sight: the To-Do view now fills the space under the Lists bar instead of a fixed height that pushed the pane's foot off screen. The detail pane and the new-task form share one set of fields, and a new list is named in a dialog, from the sidebar and from the form; the dialog moved to shared/NameDialog for both lists. The To-Do help pictures are re-rendered, the texts that described the old sidebar and form follow in all 27 languages, and the wiki page is updated. The phone screens are unchanged. |
||
|
|
dc60f39554 |
feat(ui): Costs bar like the other tabs, desktop packing list in the phone's look
The Costs tab opens with the same bar as Transports, Bookings, Lists and Files: the tab's name, the trip's dates and days in one pill, the travelers, and Settle up and Add expense on the right. The desktop packing list takes the phone's look without losing anything it shows. Category cards get a tinted head band, rows get hairlines and filled checkboxes, owner, quantity and weight read as small badges, and the bin and a menu with Move to List, Sharing, Rename and Delete replace the row of icons. What a row does not use stays dimmed until hover or focus, so nothing leaves a gap. A click on the name renames, ticked items too. Add item opens a composer shaped like the next row, Add list moved into the Lists bar, Add list and Save as template ask for the name in a dialog, and Remove checked sits beside the progress bar. The Bags panel becomes a card, and the item menu, the bag picker, the list menu and the bag member picker share one popover look and close on a click beside them. Help center pictures for Costs and Lists are re-rendered, the Lists guide texts follow the new controls in all 27 languages, and the wiki pages are updated. The phone screens are unchanged. |
||
|
|
5c312a8497 |
feat(packing): print, PDF and Markdown/CSV export of packing lists
The packing list bar gets an export menu next to the import button, both now icon only. The menu prints the open view as a TREK styled page (trip, dates, packed count, one card per category, bags), which the browser can also save as a PDF, and downloads it as a Markdown checklist or as a CSV in the exact format the import reads, bags included, so a list works as a personal template without admin rights. The import now also reads Markdown checklists: headings become categories, "- [x]" marks an item packed, a "3x" prefix sets the quantity and a "(200 g)" suffix the weight. The phone action sheet offers the same exports through the shared usePackingExport hook. The help center gets an export guide with new pictures, the import guide and the wiki page describe the Markdown format, in all 27 languages. Addresses discussions #1420 and #875. |
||
|
|
2a8071f642 |
feat(plugins): search plugins can answer the place search as you type
A searchProvider plugin can now implement an optional suggest() beside search(). Its places then show up in the place search's typed-ahead list, after the core suggestions and marked with the plugin's name, on desktop and phone. An index that cannot take a request per keystroke leaves it out and keeps answering the full search only. The child reports which functions each hook object carries at load, so GET /api/plugin-search/suggest only asks the plugins that implement suggest: from the second character, three rows at most across all of them, 800 ms to answer. The feed marks plugins holding hook:search-provider and the client only asks the route while one is installed. A plugin row carries its whole place, so picking it skips the details lookup, which knows no plugin id. The phone's search results now name their source as the desktop ones do, and the desktop dropdown scrolls instead of running past the dialog. Addresses discussion #2221. |
||
|
|
1727732841 |
feat(plugins): plugins can add POI categories to the map and answer them
A new manifest capability poiCategories (up to 4, icon from an allow-list, strict #rrggbb color) and the hook poiCategoryProvider.getPois behind the hook:poi-category-provider permission. GET /api/plugin-pois in the plugin contributions module answers them in the /api/maps/pois shape, with search_plugin_pois as the MCP twin. plugin: ids no longer count as Google place ids. SDK 1.8.0 carries the types, validation and docs. On the map, one hook merges the built-in categories with the ones active plugins declare, and the pill shows the plugin chips after a divider on desktop and phone, scrolling sideways instead of shrinking a chip below its touch target. A plugin chip searches through a repo that refuses offline and parses the answer strictly. Its pins, on Leaflet and GL, use the category's own icon and color, both checked again before they reach any markup, and the hover card lists the plugin's details as escaped text. A plugin POI can be added as a place like any other. The admin panels on desktop and phone show a chip for the new permission and the categories a plugin would add, from one shared module. Addresses discussion #1781. |
||
|
|
c7c9ba24a3 |
fix(journey): select all takes every photo in the range and imports keep their order
The photo picker's Select all took only the pages already loaded, 50 at a time, and replaced whatever had been picked on another tab. It now loads the rest of a day, trip or date-range search before selecting (cancelled by closing the picker or switching tab), adds to earlier picks, and Deselect all only clears what is in view. Add stays disabled until that loading is done. The whole-library tab keeps selecting what is loaded and shows the count as "N+". A date-filtered Immich search stopped at 1,000 photos without a word, because its page budget was absolute; it is now relative to the page asked for, with an absolute ceiling of 270 raw pages sized so the picker's deepest page still gets a full answer. Adds go out in batches of 500 ids, under the JSON body limit, and each batch is sent oldest first, so photos land in an entry in the order they were taken. When a later batch fails, the photos already saved are counted and the journey reloads while the error still shows, through one hook for desktop and phone. After a provider add, the capture-time lookup runs at most four at a time per user, shared by all of that user's batches. When it fills a row, a journey:photos:updated event reloads open journeys once per batch, from the REST routes and the MCP add tool alike. Device uploads, which arrive one file per request, get the same lookup without the event. Addresses discussion #1587. |
||
|
|
650451bdf7 |
feat(notifications): Web Push delivers notifications to every device, even with TREK closed
A fourth built-in channel next to email, webhook and ntfy. Browsers register per device, the server encrypts per RFC 8291 and signs per RFC 8292 with node:crypto and sends only to known push services through the SSRF guard. The event matrix decides per category what arrives by push; admins switch the channel on under Notifications. VAPID keys come from the environment or are generated once, only while neither stored row exists, and kept encrypted. A pair that does not decrypt or match is never overwritten and never swapped for another one: push stays off with an error in the log and every device stays registered until the key is fixed. The subject is VAPID_SUBJECT, then an https APP_URL, then the project URL. Only chat messages collapse into one notification per trip, and a replacement still alerts. A 403 removes a device only after three failures in a row; 404 and 410 remove it at once. Password change, reset and an admin setting a password drop the user's devices with the other credentials. The request log redacts subscriptions and endpoints, boot errors print *** for every secret variable, and logout forgets the device with a plain fetch so an expired session cannot redirect midway. Addresses discussion #894. |
||
|
|
7ce31756a3 |
feat(settings): every date picker starts its week on the day you pick
Settings > General > Language & region gets "Week starts on" (Monday, Sunday or Saturday), and Admin > User Defaults a default for everyone who has not picked one. Monday stays the built-in value, so nothing changes for anyone who leaves it alone. Both calendar implementations follow it: CustomDatePicker, used by trip dates, bookings, costs, to-dos and the admin school holidays on desktop and phone, and the journey date picker, whose weekday headers were hardcoded English and now speak the app language. The grid math and the header row live in one helper (utils/calendarWeek.ts). The day names on the options come from Intl, so only the label and a hint needed translating. Vacay keeps its own per-plan week start. The value is validated like the other regional settings: VALID_VALUES for admin defaults, the display-settings MCP tool, and the managed key list. Addresses discussion #2029. |
||
|
|
bb5c5dcfc1 |
docs: TREK ships 27 languages now
Azerbaijani, Estonian, Slovak and Thai joined; the README and the wiki still counted 23. |
||
|
|
b7f777024a |
feat(i18n): bring the Thai translation up to date with dev
Translates the keys dev gained since the branch was cut, the help center above all, and drops the six keys dev no longer has. Registers th everywhere a language is listed: notification emails, the rate-limit message, the placeholder check and the wiki. The two tests that counted the languages by hand now derive the count from SUPPORTED_LANGUAGES. |
||
|
|
3c036ea25d |
feat(i18n): bring the Slovak translation up to date with dev
Translates the keys dev gained since the branch was cut, the help center above all, and drops the six keys dev no longer has. Registers sk everywhere a language is listed: notification emails, the rate-limit message, the placeholder check and the wiki. The two tests that counted the languages by hand now derive the count from SUPPORTED_LANGUAGES. |
||
|
|
ffa085aef7 |
feat(i18n): bring the Estonian translation up to date with dev
Translates the keys dev gained since the branch was cut, the help center above all, and drops the six keys dev no longer has. Registers et everywhere a language is listed: notification emails, the rate-limit message, the placeholder check and the wiki. The two tests that counted the languages by hand now derive the count from SUPPORTED_LANGUAGES. |
||
|
|
9263421c5e | feat(i18n): add Estonian (et) translation |