ad893eb1cc
52 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ad893eb1cc
|
Release 3.1.0 (#1185)
* Phase 0 — NestJS + Zod foundation harness (F1–F8) (#1050) Co-hosted NestJS app behind the existing Express server via a strangler-fig dispatcher, sharing the same better-sqlite3 connection and JWT httpOnly cookie. Additive and dormant: default routing stays on Express, Nest only serves its own /api/_nest diagnostics until a module opts in. F1 @trek/shared Zod contract package; F2 Nest bootstrap co-hosted (fall-through, single Dockerfile/port); F3 shared better-sqlite3 provider; F4 JWT cookie auth guard (+ @CurrentUser, admin guard); F5 Zod validation pipe + error-envelope parity; F6 Nest test + coverage gates; F7 per-prefix strangler toggle (env, default Express); F8 CI build/typecheck/test/coverage. Remaining F4/F6/F8 checklist items (trip-access + permission levels + MFA policy, e2e harness/seed + 80% gate, Nest↔Express parity test, Playwright PR-comment workflow) are tracked on the first consuming module cards (L1/A1/C1). * feat(weather): migrate /api/weather to the NestJS pilot module (L1) (#1053) First strangler migration (L1): /api/weather is served by a NestJS module. - @trek/shared/weather Zod contract; Nest controller byte-identical to the legacy Express route (paths, query params, status codes, { error } bodies, lang default, ApiError/500 passthrough). Service reuses getWeather/getDetailedWeather (+ shared cache; MCP tools unchanged). - Strangler routes /api/weather to Nest by default; the legacy Express route + its migration-time parity test were decommissioned in this PR. - Frontend (FE2): weatherApi typed against the @trek/shared WeatherResult contract. - Harness: reusable Nest-vs-Express parity harness, e2e harness (temp SQLite + seed/cookie helpers, real JwtAuthGuard), src/nest coverage gate raised to >=80%, src/nest test guide. - Verified end-to-end on a prod mirror (dev1): 401/400/200 via Nest with real Open-Meteo data, Express route gone. * fix(packing): multiply item weight by quantity in bag/total weight calcs (#898) Quantity now counts toward bag and total weights. Generalised to an itemWeight() helper used by every weight sum (bag totals + max, unassigned, grand total; sidebar + bag modal) with unit tests. * feat(i18n): add Korean (ko) translation (#977) Korean translation by @ppuassi, topped up to full en.ts key parity. Language registration follows separately. * feat(i18n): add Japanese (ja) translation (#829) Japanese translation by @soma3978, at full en.ts key parity, registered in supportedLanguages + TranslationContext. * Add Turkish (tr) translation + language registry (#1029) Turkish translation by @SkyLostTR, at full en.ts key parity, registered in supportedLanguages + TranslationContext. * i18n: register Korean + add Ukrainian translation (#1055) Korean translation by @ppuassi (#977) — now registered. Ukrainian by @JeffyOLOLO (#902) — lifted onto a clean branch. Both at full en.ts key parity (2258 keys). * chore: fix monorepo build pipeline and migrate shared to built package (#1056) * chore: fix monorepo build pipeline and migrate shared to built package - Root package.json: add workspace scripts (dev, build, test, test:cov, test:e2e) that delegate to actual scripts in shared/server/client workspaces - shared: add tsup build step (CJS + ESM dual output, .d.ts); consumers now import from the built dist instead of raw TS source via path aliases - server: replace tsc-alias with tsconfig-paths (tsc-alias mangled node_modules paths); fix MCP SDK path aliases to point to root node_modules (../node_modules) - server/scripts/dev.mjs: delay node --watch until tsc -w signals first-pass done, eliminating the spurious restart on every dev startup - client/vite.config.js + vitest.config.ts: remove @trek/shared path alias (no longer needed now that shared is a proper package) - Consolidate package-lock.json at the workspace root; drop per-workspace lock files * chore: fix test script to reflect root package.json * chore: add missing lint and prettier script in root package.json * fix(ci): build shared before tests; fix vitest MCP SDK alias paths vitest.config.ts aliases pointed at ./node_modules/ (server-local) but packages are hoisted to the root node_modules/ in the npm workspace — changed to ../node_modules/. CI jobs now install and build shared before running server/client tests so that @trek/shared's dist/ exists when vitest resolves the package. * fix(docker): update Dockerfile and CI for monorepo workspace structure Dockerfile: - Add shared-builder stage that produces @trek/shared dist before client and server stages need it - Each build stage carries root package.json + package-lock.json so npm can resolve @trek/shared as a workspace dependency - Production stage installs via workspace context (npm ci --workspace=server --omit=dev) so node_modules/@trek/shared symlinks to shared/dist correctly - Copy server/tsconfig.json into the image so tsconfig-paths/register can find the MCP SDK path aliases at runtime - CMD cds into /app/server before starting node so tsconfig-paths baseUrl resolves and ../node_modules points to /app/node_modules - Remove mkdir for /app/server (now a real dir); keep symlinks for uploads/data docker.yml version-bump: - Replace manual per-workspace cd+npm-version calls with single: npm version --workspaces --include-workspace-root --no-git-tag-version (mirrors the version:* scripts in root package.json) - git add now references root package-lock.json; adds shared/package.json .dockerignore: add shared/dist package.json: fix version:prerelease preid (alpha → pre) * fix(tests): use in-memory SQLite per worker in test mode vitest pool:forks spawns parallel worker processes that all called initDb() on the same data/travel.db, causing SQLite "database is locked" and "duplicate column name" races. When NODE_ENV=test each fork now gets an isolated :memory: DB so migrations run independently with no file contention. * chore(ci): add ACT guards to skip DockerHub steps in local act runs act sets ACT=true automatically. Guards added: - docker login: if: ${{ !env.ACT }} - build outputs: type=docker (local load) when ACT, push-by-digest when CI - digest export/upload: if: ${{ !env.ACT }} - merge job: if: ${{ !env.ACT }} - release-helm job (docker.yml): if: ${{ !env.ACT }} - version-bump git push (docker.yml): wrapped in [ -z "$ACT" ] shell guard Run locally with: ./bin/act -j build -W .github/workflows/docker.yml \ -P ubuntu-latest=catthehacker/ubuntu:act-latest * fix(ci): move ACT guards to step level; add guards to security.yml env context is invalid in job-level if conditions — moved all ACT guards down to individual steps. Also guards docker login + scout in security.yml so act can run the build-only part of that workflow. * fix(ci): skip git fetch and tag logic in act (no remote access in local containers) * Revert "fix(ci): skip git fetch and tag logic in act (no remote access in local containers)" This reverts commit 67cf290cda34705c0b68a9c763403418558c88f1. * Revert "fix(ci): move ACT guards to step level; add guards to security.yml" This reverts commit f92b95e0546599f936b88dea5cd8fa1164eaa6cb. * Revert "chore(ci): add ACT guards to skip DockerHub steps in local act runs" This reverts commit 797183de0844ebc2a2537aa326ffd160fc71de3a. * fix(docker): add musl optional deps so alpine builds find native rollup/sharp binaries npm prunes libc-constrained optional deps to the host libc (glibc) when generating the lockfile, leaving no musl entry for Alpine containers. Declaring the x64/arm64 musl variants as explicit root optionalDependencies forces them into the lockfile so npm ci on Alpine can install them. Covers shared-builder (tsup/rollup) and client-builder (vite/rollup + sharp icon generation) for both linux/amd64 and linux/arm64 CI targets. * fix(docker): copy client dist into server/public so the server resolves static files correctly The server runs from /app/server and serves static files relative to that directory, so the client build output must land at /app/server/public, not /app/public. * feat(planner): real road routes (OSRM) with travel-time connectors (#1060) * feat(planner): real road routes (OSRM) with travel-time connectors Replace the straight-line "as the crow flies" route with real OSRM road geometry (FOSSGIS routed-car/-foot) and an Apple-Maps style render (blue casing under a lighter core) on both the Leaflet and Mapbox GL maps. Routes are off by default and toggled per session, with a driving/walking mode switch in the day footer. Each day shows per-segment travel time/distance connectors between places, computed from the OSRM legs and split at transport bookings. Also redesigns the day header for visual consistency: vertical number+weather capsule, name with a divider before the date, subtle hotel/rental pills that stay on one line, and a hover-revealed 2x2 action square (edit / add transport / add note / collapse). Drops the Google Maps button. * test(planner): update route hook tests for calculateRouteWithLegs * remove route_calculation setting, always use OSRM routing (#1064) The per-user route_calculation toggle was a second, hidden on/off layer on top of the day footer's show-route button, and made it easy to end up with straight-line routes for no obvious reason. Drop the setting entirely: routing is always on, the footer toggle stays the single switch. Old stored values are simply ignored (settings are key-value, no migration needed). * chore: move i18n to shared package (#1066) * chore: move i18n to shared package * chore: move server translations to shared package and apply linter and prettier on entire shared package * feat(dashboard): upcoming reservations endpoint + travel-stats country/distance Adds GET /api/reservations/upcoming for the dashboard widget, switches travel-stats to the same country source as Atlas (manual + place-derived, ISO codes), and a distance service for flown km. * i18n(dashboard): dashboard keys across locales * feat(dashboard): boarding-pass hero, atlas row, live widgets + modal portal fix Reworked dashboard layout: boarding-pass hero with hover + days-left countdown, atlas stats row with real flags, searchable currency widget, editable timezone widget, new-trip FAB. Modals now portal to document.body to avoid inheriting dashboard-scoped button/font styles. * i18n(dashboard): sync all locales to one key set + German copy-dialog strings Brings every locale's dashboard namespace to the same 149-key set (missing keys backfilled from English) and translates the previously English-only copy-trip dialog into German. * refactor(dashboard): replace hardcoded strings with i18n keys Hero, atlas row, trip cards, filters, currency and timezone widgets now resolve all visible copy through t() instead of hardcoded English/German. * feat(i18n): add Greek translation (#1061) * i18n: complete Turkish (tr) translation (#1075) Fill in the remaining ~2100 UI strings in shared/src/i18n/tr so Turkish matches the English catalog. Brand names, URLs, and technical placeholders are left untranslated by design. * chore: prettier + lint * chore: enforce prettier & lint on shared package * feat: Updated border of map markers to reflect category color. (#1062) * feat(dashboard): mobile layout, glass UI, context bottom nav + OIDC PKCE (#1079) * feat(dashboard): mobile layout, glass tiles, plain-text countdown, place photos - Rework the mobile dashboard: cover hero, separate boarding-pass card, trimmed atlas (trips + days only), stacked widgets - New floating bottom tab bar with a centred context-aware + button (new trip / place / journey / entry depending on the page) - Move profile + notifications into a small top strip on the dashboard - Desktop: glassmorphic tiles (light + dark), neutral dark palette, plain-text countdown module, real place photos in the boarding pass * i18n(dashboard): translate new dashboard keys across all locales Fill the dashboard-rework keys (hero, atlas, fx, tz, upcoming, copy dialog, aria labels, countdown) that were left as English placeholders, plus the new startsIn/aria keys, for all 19 languages. * feat(oidc): send PKCE (S256) in the OIDC login flow The OIDC client now generates a code_verifier per login, sends the S256 code_challenge on the authorize request and the code_verifier on the token exchange. Works whether the provider has PKCE optional or required (fixes login against providers that require PKCE, e.g. Pocket ID). * Migrate TREK 3 to NestJS + React 19 (shared Zod contracts) (#1087) * Migrate TREK 3 to NestJS + React 19 with a shared Zod contract layer Brownfield strangler migration of the backend onto NestJS modules (auth, trips, days, places, assignments, packing, todo, budget, reservations, collab, files, photos, journey, share, settings, backup, oidc, oauth, admin, atlas, vacay, weather, airports, maps, categories, tags, notifications, system-notices) served through a per-prefix dispatcher, keeping the existing SQLite/better-sqlite3 DB and JWT httpOnly cookie auth, with behavioural parity for every route. Client: React 19 upgrade, "page = wiring container + data hook" pattern across all pages, per-domain Zustand stores bound to @trek/shared contracts, and decomposition of the large components (DayPlanSidebar, PackingListPanel, CollabNotes, FileManager, MemoriesPanel, PlacesSidebar, CollabChat, SystemNoticeModal, BudgetPanel, PlaceFormModal, ...) into focused render units backed by in-file hooks. Apply the shared global request pipeline (helmet/CSP, CORS, HSTS, forced HTTPS, the global MFA policy and request logging) to the NestJS instance as well, so a migrated route is protected identically to the legacy fallback rather than bypassing it. * Finish the NestJS migration — drop the legacy Express app NestJS now serves the whole surface: every /api domain plus the platform routes (uploads, /mcp, the OAuth/MCP SDK + /.well-known metadata and the production SPA fallback). Removed server/src/app.ts, all of server/src/routes/* and the strangler dispatcher; index.ts and the integration suite share a single buildApp() bootstrap so prod and tests can't drift. - Platform/transport routes extracted to nest/platform/platform.routes.ts and mounted before app.init() — Nest's router answers an unmatched request with a 404, so a route registered after init is never reached. The SPA fallback is a NotFoundException filter and the catch-all uses a RegExp (Express 5's path-to-regexp rejects a bare '*'). - New modules: memories (/api/integrations/memories — the Journey gallery's Immich/Synology proxy), addons (GET /api/addons) and the cross-trip GET /api/reservations/upcoming. - TrekExceptionFilter reproduces the old multer / err.statusCode handling so upload rejections keep their 400/413 { error } body and non-ASCII filenames survive (defParamCharset). - addTripToJourney and the MCP get_journey_share_link tool gained the trip-access check they were missing. - Re-pointed the 34 integration tests + the websocket test onto the Nest app; removed the now-meaningless Express-vs-Nest parity tests and a few orphaned client components. * Restore the reset-password rate limit and fix copyTrip reservation links Two correctness/security gaps the NestJS migration introduced: - POST /api/auth/reset-password lost its per-IP rate limiter. Restore it (5 attempts / 15 min on a dedicated bucket, same as the old resetLimiter) so reset tokens can't be brute-forced unthrottled. Covered by AUTH-019. - copyTripById did not copy reservations.end_day_id (a day reference — now remapped through dayMap like day_id) or needs_review, so a duplicated trip lost multi-day transport end-day links and reset the review flag. * Clean up dead code, dedupe helpers, fix the reset-password contract - Remove server exports orphaned by the Express removal: the immich album-link helpers, seven route-only service exports, getFileByIdFull; de-export internal-only helpers (utcSuffix). - De-duplicate verifyTripAccess (9 identical copies -> services/tripAccess.ts) and avatarUrl (3 -> services/avatarUrl.ts); name the bcrypt cost (BCRYPT_COST) and the email regex (EMAIL_REGEX). Public API unchanged. - resetPasswordRequestSchema declared `password`, but the client sends and the service reads `new_password` — rename it so the contract matches and the client types resolve. - Make ATLAS-013 deterministic: stub the admin-1 GeoJSON download instead of fetching ~4600 features from GitHub during the test (it hung the suite). * Make the client typecheck runnable (vitest/vite ambient types) The client had no `typecheck` script and tsc couldn't even start (the baseUrl deprecation errored out, same as server/shared already silence). Add `ignoreDeprecations: "6.0"` to match the other workspaces, a `typecheck` npm script, and a src/vite-env.d.ts referencing vite/client + vitest/globals so tsc knows the test globals (describe/it/expect/vi). This turns ~3600 phantom "Cannot find name" errors into a real, measurable count (~590 actual type errors remain, to be worked down). Type-only; no runtime change. * Derive client domain types from the shared schema contracts Add entity/response Zod schemas to @trek/shared (place, trip, assignment, day, budget, packing, reservation), each matched against the producing server service, and re-export them from client types.ts instead of the hand-written duplicates that had drifted (name/title, amount/total_price, owner_id/user_id, cover_url/cover_image, ...). Updates the call sites and test fixtures the corrected types surfaced; type-only, no runtime behaviour change. * chore(db): log swallowed errors in addon-disable migration + guard against destructive migrations The migration that disables the legacy "memories" addon swallowed any error in an empty catch, as did ~30 other catch blocks in the migration runner (column adds, the journey rebuild, index probes). Replace each silent catch with the existing console.warn('[migrations] ...') log so failures are visible. Control flow is unchanged: every step stays non-fatal, nothing new is thrown. Add a static guardrail test that scans the migration source and fails when a new destructive statement (DROP TABLE / DROP COLUMN / TRUNCATE / DELETE FROM / ALTER ... DROP) appears outside a reviewed allowlist, and when an empty/silent catch block is reintroduced. The existing destructive statements are all legitimate table rebuilds or bounded cleanups and are recorded in the allowlist with a reason. * Re-check SSRF on every redirect hop when resolving short links Replace the one-shot checkSsrf + fetch(redirect:'follow') in the maps and place short-link resolvers with safeFetchFollow, which follows redirects manually and re-runs checkSsrf against the DNS-pinned IP of each hop (max 5). A redirect to an internal/loopback address is now blocked even when the initial URL is public, while legitimate cross-host redirects (goo.gl -> maps.google.com) still resolve. * Reject WebSocket tokens minted before a password change Stamp the user's password_version onto the ephemeral ws token and verify it on connect, closing the socket (4001) when it no longer matches, so a token issued before a password reset can't be replayed. Tokens minted without a version are treated as version 0, matching the JWT pv-claim semantics. * fix(i18n): guard locale key parity and finish the OAuth consent page strings Every non-en locale now exposes the exact same flat key set as en. Keys that had drifted out of sync are backfilled with the English source value (tagged en-fallback) so t() resolves a real string instead of relying on the silent runtime fallback; no existing translation was touched and no key was removed. Add a parity test that imports each aggregated locale bundle and asserts its key set matches en, with a diagnostic listing of any missing/extra keys. This complements the file-level check in shared/scripts by guarding the merged export the app actually serves. Finish internationalising OAuthAuthorizePage: the ~15 remaining hardcoded English chrome strings now go through oauth.authorize.* keys (English source in en, en-fallback placeholders elsewhere). Markup and behaviour are unchanged. * Add semantic theme color tokens to Tailwind Map the CSS theme variables from src/index.css (:root light / .dark dark) to named Tailwind utilities — bg-surface, text-content, border-edge, bg-accent and their variants. This gives components a Tailwind-native target for the theme colors so we can replace inline `style={{ ... 'var(--...)' }}` with utility classes without changing the rendered values. * Surface silent store failures to the user and validate API responses in dev Reservation toggle, todo/packing toggle and budget reorder were swallowing API errors after rolling back, so the user saw the change silently snap back with no explanation. Route those failures through the existing toast channel (new store/notify.ts bridges to window.__addToast, the same channel SystemNoticeBanner uses); the reservation toggle re-throws so ReservationsPanel's own translated toast finally fires. Also wire the existing parseInDev/checkInDev response validation into the maps and notification-test endpoints to catch contract drift in dev. * Migrate static theme inline styles to Tailwind utilities and extract page sub-components Replace the static, color-only inline `style={{ ... 'var(--bg-primary)' ... }}` props with the new semantic Tailwind utilities (bg-surface, text-content, border-edge, ...) wherever the result is byte-identical; dynamic/conditional theme styles and hardcoded status colors are left inline. Extract the Atlas country-search autocomplete, the Admin update banner, and two Journey dialogs into their own presentational components to shrink the oversized page files, keeping behaviour and markup identical. * Remove the unrouted photos page and its dead photo components PhotosPage was never wired into the router and its usePhotos hook read a tripStore photos slice that was never implemented; the Photos gallery, lightbox and upload components were only reachable through it. Per-trip photos now live in the Journey gallery (Immich/Synology). Removed the dead page, hook and components — the live Journey PhotoLightbox is a separate component and stays. * Resolve the remaining client type errors and the trip.title navbar bug Drive the client typecheck to zero without any/ts-ignore: convert the tripId route param to a number once at the page boundary so it matches the numeric props and store actions it feeds, fix trip.name -> trip.title (the wire field is title, so the old read rendered blank in the files/offline views), and tighten the scattered handler-arity, DOM-cast and untyped-payload sites. No runtime behaviour change. * Convert the remaining dynamic and hardcoded inline styles to Tailwind utilities Second styling pass over the components and pages: move conditional theme colors into className ternaries (bg-accent / bg-surface-hover etc.), turn reused CSSProperties constants into className constants, and express static hardcoded hex/rgba colors as Tailwind arbitrary values so the exact rendered colour is preserved. Truly dynamic styling (computed geometry, gradients, multi-part shadows, data-driven colours, the undefined --sidebar/--nav layout vars) stays inline as it cannot be expressed as a static class. Updated three component tests that asserted the old inline active-state styles to assert the equivalent utility class instead. Verified: client typecheck 0, full client suite green, and a live light/dark render check in the dev server confirms the semantic theme tokens resolve correctly (the earlier 'transparent popups' were a stale dev server that pre-dated the tailwind.config token addition, not a code issue). * Add eslint flat-config for client and server and gate typecheck, lint and pages in CI client and server had lint scripts but no eslint config (only shared was linted in CI). Add flat configs mirroring shared's stack (js + typescript-eslint recommended + eslint-config-prettier) plus the client's react-hooks/react-refresh plugins. Pre-existing patterns in this never-linted code (explicit any, require() in the CommonJS server, empty catches, exhaustive-deps) are set to 'warn' rather than 'error' so the gate passes at 0 errors without a repo-wide reformat — these can be ratcheted to errors over time. Wire blocking typecheck + lint + lint:pages steps into the client and server CI jobs (now that both typechecks are clean) and promote the server typecheck from informational to blocking. * Decompose the remaining God Components into hooks, helpers and sub-components FE6: split the oversized page and panel components into thin layout shells plus colocated use<Component> hooks, .constants.ts, .helpers.ts (with tests) and presentational sub-components, following the established 'logic in a hook, render in slices' pattern. Behaviour, markup, classes and effect order are unchanged. Largest reductions: PackingListPanel 1598->42, FileManager 1055->36, AdminPage 1525->167, BudgetPanel 1266->146, JourneyDetailPage 2822->547, PlacesSidebar 945->66, CollabChat 861->106, CollabNotes 1417->532. DayPlanSidebar's drag-and-drop render body was left intact (ref-identity sensitive) and only its toolbar/modals/constants were extracted. * Fix duplicate React keys in the file-assign place list When a place is assigned to the same day more than once it appeared twice in a day's list, so the place-button key={p.id} collided and React warned about duplicate keys. Key by place id + render index so siblings stay unique. Pre-existing in the old FileManager; behaviour unchanged. * Format the shared package and drop an unused import to satisfy the lint gate The i18n and schema changes added code that wasn't prettier-formatted, and place.schema.ts imported categorySchema without using it. Run prettier over shared and remove the import so 'npm run lint' + 'format:check' pass. * Install all workspaces in the server CI job so SWC's native binary is present The server vitest config transforms via unplugin-swc, which needs @swc/core's platform-specific native binary. A workspace-scoped 'npm ci --workspace server' skips that optional dependency, so vitest failed to load the config on the Linux runner. Use a full 'npm ci'. * Re-resolve dependencies with npm install in the server CI job for SWC Full 'npm ci' still skipped @swc/core's Linux native binary because the committed lockfile was generated on Windows and lacks the Linux optional-dep install metadata. 'npm install' re-resolves and fetches the platform-matching binary, which the server's unplugin-swc transform needs to load vitest.config.ts. * Install @swc/core's Linux binary explicitly in the server CI job Neither npm ci nor npm install fetched @swc/core-linux-x64-gnu on the Linux runner because the lockfile was generated on Windows and lacks the Linux optional-dep metadata. Add a step that installs the matching @swc/core-linux-x64-gnu version (no-save, no-lockfile) so unplugin-swc can load the server's vitest config. * Use legacy-peer-deps when installing the SWC Linux binary in CI The explicit @swc/core-linux-x64-gnu install re-resolved the tree and hit the pre-existing lucide-react/react-19 peer conflict that the lockfile was generated around. Add --legacy-peer-deps so the step matches the project's resolution and installs the binary. * Keep the lockfile when installing the SWC binary so other deps stay pinned Dropping --no-package-lock made npm re-resolve the whole tree and upgrade eslint, whose newer recommended config flagged no-useless-assignment as an error in the server lint step. Keep the lockfile so only @swc/core-linux-x64-gnu is added and every other dependency (incl. eslint) stays at its locked version. * Fix a batch of reported bugs: Atlas regions, planner overlays, imports, Safari modals (#1094) * Start the Journey date picker week on Monday (#1078) The Journey entry date picker started the week on Sunday (firstDow = getDay(), headers Su-first) while every other picker (CustomDateTimePicker, VacayCalendar) starts on Monday. Align it: Monday-first leading offset ((getDay()+6)%7) and Mo-first weekday headers. * Fix Taiwan resolving to CN-TW in the Atlas country search (#1049) natural-earth gives Taiwan ISO_A2='CN-TW' (a subdivision-style value) with ADM0_A3='TWN'. The dynamic A2_TO_A3 augmentation added 'CN-TW'->'TWN', which then overwrote the legitimate TWN->TW entry in the reverse map, so Taiwan's country option resolved to 'CN-TW' — unresolvable by Intl.DisplayNames (no name, broken flag, not searchable). Only augment A2_TO_A3 with real 2-letter codes. * Drop empty leftover dateless days when a trip gets a shorter dated range (#1083) generateDays kept all unused dateless placeholder days after switching to an explicit (shorter) date range, so day_count (COUNT(*) FROM days) stayed inflated. Delete the empty leftovers (no assignments/notes/accommodations) like the dateless path already does, while preserving any that still hold content. Adds TRIP-SVC-017. * Render GPX and route overlays once the Mapbox style has loaded (#1036) The GPX and route geojson effects ran before the map 'load' event had attached their sources, so on the first paint they hit the early return and never re-ran. Add mapReady to their dependencies so they fire again the moment the sources exist. * Convert HEIC trip and journey covers to JPEG before upload (#1085) HEIC/HEIF covers coming straight off an iPhone could not be rendered in the preview or stored as a usable image. Route both cover pickers through normalizeImageFile, the same conversion the journal entry editor already uses, so the file becomes a JPEG before it leaves the browser. * Name GPX routes and tracks after their source file so multiple imports stick (#1054) Unnamed routes and tracks all fell back to the same generic 'GPX Route' / 'GPX Track' label, so the name-based import dedup dropped every one after the first - importing several files (or one file with several tracks) only kept a single place. Derive the default name from the source filename with an index suffix when a file holds more than one geometry, thread the filename down through the controller, and let the import modal take more than one file at a time. Adds PLACE-SVC-037/038. * Namespace the modal backdrop class so content blockers stop hiding it (#1027) Generic class names like .modal-backdrop sit on the cosmetic filter lists that content blockers (1Blocker, EasyList Annoyances) ship, and get hidden with display:none. The shared Modal - used by New Trip and Add Place - carried that class, so Safari users running such a blocker saw the modal silently fail to open with no error and no network request. Rename it to .trek-modal-backdrop. * Highlight GB regions by resolving England/Scotland/Wales/NI to finer admin-1 codes (#1067) A zoom-8 reverse geocode of a UK place only resolves to the constituent country (GB-ENG/SCT/WLS/NIR), but Natural Earth's admin-1 polygons for GB are counties and boroughs (GB-LND, GB-MAN, GB-CON, ...). Those four codes match no polygon, so places in England never highlighted in the Atlas while CH/IT/NL/etc. worked. When a GB lookup lands on a constituent country, re-resolve it at a finer zoom where Nominatim exposes the county/borough code the polygons actually carry. Other countries keep the exact zoom-8 behaviour. Adds ATLAS-UNIT-021. * Surface the real place-search error instead of a generic toast (#1092) When a place search or detail lookup fails, the backend already forwards the upstream reason - including descriptive Google Places API messages such as 'Places API (New) has not been used in project ... or it is disabled'. The planner discarded it and always showed 'Place search failed', so a key that is mis-enabled, unbilled, or pointed at the legacy API instead of Places API (New) looked like an unexplained silent failure. Show the server-provided message when present, and stop the Atlas bucket-list search from swallowing its error without a trace. * Await the async cover normalization in the TripFormModal paste test (#1085) handleCoverSelect now normalizes the pasted file before previewing it, so URL.createObjectURL is called a microtask later. The assertion moves into waitFor; a non-HEIC file still passes through unchanged. * fix(pwa): removed orientation from the manifest (#1058) * fix(journey): raise PhotoLightbox z-index above MobileEntryView (#1101) * feat(transport): add bus, taxi, bicycle, ferry and other transport types (#1105) Closes #718. Adds five new transport reservation types alongside the existing flight/train/car/cruise: bus, taxi, bicycle, ferry and a generic 'transport_other' catch-all. The new types are treated as first-class transports everywhere — the transport modal, day plan, route calculation, map overlays, file grouping and the PDF export — and are translated across all 20 locales. A dedicated 'transport_other' value is used for the catch-all so existing 'other' bookings are not reclassified as transport. * feat(reservations): native booking-confirmation import via KDE KItinerary (#1102) * feat(reservations): native booking-confirmation import via KDE KItinerary Adds a two-step preview → confirm flow for importing booking emails, PDFs, PKPass and HTML confirmations. The server invokes the KDE kitinerary-extractor binary, maps JSON-LD schema.org output to TREK reservation shapes, and persists via the existing createReservation pipeline (accommodations, budget, places, WebSocket broadcasts). - NestJS BookingImportModule: preview + confirm endpoints under /api/trips/:tripId/reservations/import/booking{,/confirm} - KitineraryExtractorService: spawns the binary, filters stderr noise, handles QDateTime (@value) timezone-aware datetimes - kitinerary-mapper: FlightReservation, TrainReservation, BusReservation, BoatReservation, LodgingReservation, FoodEstablishmentReservation, RentalCarReservation, EventReservation → typed preview items - BookingImportService: auto-creates place rows; geocodes venues without coordinates via Nominatim (name+address → address → name fallback); resolves day IDs for accommodation linking - BookingImportModal: drag-and-drop multi-file upload, preview cards with type icons, per-item exclude toggle, confirm step - Shared Zod contracts: BookingImportPreviewItem, PreviewResponse, ConfirmRequest, ConfirmResponse — consumed by controller, service, API client and modal - Dockerfile: node:24-trixie-slim runtime; amd64 downloads KDE static binary + locales; arm64 installs libkitinerary-bin + symlinks to fixed path; ENV KITINERARY_EXTRACTOR_PATH set for both arches - /api/health/features exposes { bookingImport: boolean } so the UI hides the Import button when the binary is absent - i18n keys (English), wiki docs, API.md, README one-liner * i18n: add booking import translations for all 19 non-English locales Adds 17 reservations.import.* keys and undo.importBooking to ar, br, cs, de, es, fr, gr, hu, id, it, ja, ko, nl, pl, ru, tr, uk, zh, zh-TW. * chore: enforce i18n parity * docs(wiki): add KItinerary local setup instructions to dev environment guide * feat(costs): rework Budget into Costs — Splitwise-style, multi-currency, mobile (#1106) * fix(journey): authorize reads of the journey share link GET /api/journeys/:id/share-link now requires journey access (canAccessJourney), matching the create/delete share-link routes and the get_journey_share_link MCP tool. Returns no link when the caller lacks access to the journey. * feat(costs): rework Budget into Costs — Splitwise-style, multi-currency, mobile Renames the Budget addon to "Costs" (UI only) and reworks it into a Tricount/ Splitwise-style cost tracker: multiple payers per expense, equal split across chosen members, settle-up with persisted history + undo, 12 fixed categories, per-expense currency with live FX conversion to a user-set display currency (Settings -> Display), and locale-correct money formatting. Adds a desktop and a dedicated mobile layout. A migration backfills existing budget items (single payer, split members, currency). Closes #551 (per-expense currency). Also switches the app font to self-hosted Poppins (Geist for secondary subtext), replacing the Google Fonts CDN dependency. * fix(costs): neutral dashboard dark palette + liquid glass, full page width, entry-count badge - Dark mode used a warm oklch palette that read brownish; switch to the neutral zinc tokens used by the dashboard (#121215 bg, #f4f4f5 ink) and add a subtle backdrop-blur glass on cards. - Costs now uses the full available page width on desktop instead of a 1280px cap. - Render the expense count next to the Expenses title as a badge. - Adapt budget/journey unit tests to the new payer-based settlement model and the Costs rename (category default 'other', Costs tab/CostsPanel). * fix(costs): drop the entry-count badge, always show row edit/delete actions Removes the count badge next to the Expenses title and makes the per-row edit/delete actions permanently visible (no longer hover-only) on desktop too. * feat(costs): currency-native money formatting, custom select/date, rename addon to Costs - Format every amount in its own currency convention (symbol position, grouping and decimal separators) regardless of app language, via a currency->locale map (EUR -> '12,00 €', USD -> '$12.00', JPY -> '¥12', ...). Previously Intl used the app locale, so EUR showed the symbol in front under an English UI. - Use TREK's CustomSelect (searchable, with symbols) and CustomDatePicker in the add/edit expense modal instead of the native <select>/<input type=date>. - Rename the 'Budget Planner' add-on to 'Costs' in the admin list (display only; id/tables/permissions/MCP stay 'budget') via seed + a migration for existing DBs. * feat(auth): configurable session duration via SESSION_DURATION Adds a SESSION_DURATION env var (ms-style strings: 1h, 7d, 30d, ...) controlling how long a session stays valid before re-login. It drives both the trek_session JWT exp claim and the cookie maxAge from one source, so they never drift. Invalid values warn at startup and fall back to the default (24h — unchanged). The MFA challenge token and MCP OAuth tokens keep their own TTL. Implements the request from discussion #946. Documented in the env-var wiki page, .env.example and docker-compose.yml. * feat: Passkey (WebAuthn) login (#1111) * feat(auth): passkey (WebAuthn) login — server endpoints, schema + admin toggle Add @simplewebauthn/server registration and primary (discoverable) login ceremonies under /api/auth/passkey, a webauthn_credentials + single-use webauthn_challenges schema (migration), the instance-wide passkey_login toggle (default off) enforced before auth by a guard, and require_mfa satisfaction via a verified passkey. RP ID/origin come only from server config (webauthn_rp_id/origins -> APP_URL), never request headers. * feat(auth): passkey enrolment, login button + admin settings UI PasskeysSection in account settings (add/rename/remove with a current-password step-up), a 'Sign in with a passkey' button on the login page, the admin enable + RP-ID/origins controls, and a per-user admin reset action. * i18n(auth): passkey strings across all locales Add login/settings/admin passkey keys to en and all 19 translated locales. * chore: update kitinerary version * Backend/frontend hardening & consistency cleanups (#1113) * refactor(auth): session token validation and password-change consistency * refactor(journey): entry field allow-list and public share-link consistency * refactor(mcp): align tool authorization with the REST permission checks * chore: input validation and sanitisation touch-ups (uploads, pdf, maps, backup, csp) * feat: optimize routes around accommodation, confirm note deletions (#1123) Optimize day routes around the accommodation When a day has an accommodation set, the route optimizer now treats it as the day's home base: it optimizes a loop that leaves the hotel and returns to it, so the stop nearest the hotel comes first. On a transfer day - checking out of one hotel and into another - the route runs from the first hotel to the second instead. The optimizer also gained a 2-opt pass on top of the nearest-neighbor ordering, which removes the crossings the greedy pass used to leave behind. A new display setting ("optimize route from accommodation", on by default) lets you turn the anchoring off. Confirm before deleting notes Deleting a plan note or a collab note now asks for confirmation first. On phones and tablets the edit and delete icons sit close together and were easy to mis-tap, which deleted notes with no way back. * fix: miscellaneous bug fixes (#1139) * fix(share): serve place thumbnails in shared trip links (#1100) Google-sourced place photos are stored as image_url pointing at the JWT-guarded /api/maps/place-photo/:placeId/bytes endpoint, so they 401 for an unauthenticated shared-trip viewer and render as broken images. Rewrite place image_url values in the shared payload to a public, token-scoped proxy (/api/shared/:token/place-photo/:placeId/bytes) and add an unguarded SharedController route that validates the token and that the place belongs to its trip before streaming the cached bytes. Mirrors the existing JourneyPublicController precedent. No client changes needed. * fix(atlas): replace Natural Earth with geoBoundaries for up-to-date regions (#1119) Atlas sourced country and sub-national boundaries from Natural Earth's GitHub `master` at runtime. That data is stale (e.g. it still shows Norway's pre-2020 counties such as Oppland/Hordaland) and depicts some contested territory in unwanted ways (nvkelso/natural-earth-vector#391), so Natural Earth is dropped entirely. - Country borders (admin0) now come from the geoBoundaries CGAZ composite; sub-national regions (admin1) from per-country gbOpen, which carries ISO 3166-2 codes. A new script (server/scripts/build-atlas-geo.mjs) normalizes and quantizes them into committed gzipped bundles under server/assets/atlas, read server-side at runtime (no network at boot, no GitHub CSP allowlist entry). - New GET /addons/atlas/countries/geo serves the country layer; the client fetches it from the API instead of GitHub. - A migration reconciles manually-marked visited_regions against the new bundle (valid code -> keep; region name still matches -> re-code; curated merge crosswalk for renamed reforms; else leave intact), with UNIQUE-safe dedup. bucket_list and visited_countries hold only invariant alpha-2 country codes, so they are untouched. - Attribution added (NOTICE.md + README) per geoBoundaries CC BY 4.0. Closes #1119 * fix(packing): make templates admin-only to create, usable by members Creating a packing-list template was gated only by trip access, so any trip member could create one from the Lists feature, while applying a template silently failed for non-admins because the apply dropdown was populated from the AdminGuard-protected /api/admin/packing-templates endpoint. - save-as-template now returns 403 for non-admins; the Save-as-Template button is hidden unless the user is an admin (both the TripPlanner toolbar and the inline packing header). - add member-accessible GET /api/trips/:tripId/packing/templates so the apply dropdown lists templates for any trip member; client fetches from it instead of the admin endpoint. Closes #1120 Closes #1121 * fix(packing): show bag tracking to non-admin members The global Bag Tracking toggle was only readable via the admin-gated GET /api/admin/bag-tracking, so non-admin trip members got 403 and the weight fields, bag circles, and BAGS sidebar never rendered (#1124). Surface the flag through the already-authenticated GET /api/addons (loaded into the client addon store on app start for every user); the packing hook reads it from the store instead of the admin endpoint. The admin write path stays admin-gated and unchanged. * Fix a batch of reported bugs (#1145) * fix(maps): fall back to OSM/Wikipedia for place photos and normalize non-standard language codes (#1137) * fix(auth): refuse password reset for OIDC/SSO-linked accounts (#1129) * fix(docker): ship server/assets (airports + atlas geo) in the runtime image (#1133, #1119) * fix(unraid): point the template at a PNG icon Unraid can render (#1073) * fix(offline): serve cached file blobs when offline or on network failure (#1046, #1069) * fix(map): centre the selected pin in the visible map area above the bottom panel (#1125) * fix(pdf): render persisted place-photo proxy URLs as images (#1130) * fix(planner): show the selected place category in the edit form (#1134) * fix(dashboard): collapse list-view trip cards to a compact row on mobile (#1132) * Support multi-leg (layover) flights (#1146) * feat(transport): support multi-leg (layover) flights in the booking form A flight booking can now hold an ordered chain of airports (e.g. FRA -> BER -> HND) instead of a single departure/arrival pair. The route is entered as a list of waypoints with a '+ add stop' button; each stop carries its own arrival and departure time plus the airline/flight number of the segment leaving it, while the whole booking keeps one price. Stored without a schema change: the existing reservation_endpoints rows carry the ordered waypoints (from/stop/to by sequence) and a metadata.legs array holds the per-leg detail. Top-level metadata (departure_airport/arrival_airport/airline/ flight_number) mirrors the first and last leg, so a single-leg flight persists exactly as before and legacy readers keep working. * feat(planner): show each flight leg as its own day-plan entry, ordered by time A multi-leg flight now expands into one entry per leg (BER -> FRA, then FRA -> HND), each on its own day with its own times, instead of a single span. Each leg is an addressable slot (reservation id + leg index) so places and notes can be dropped into the layover gap between legs; the per-leg position is persisted in metadata.legs[i].day_positions and survives a reload. Day-plan items are now ordered chronologically: anything with a time (a place's time, a flight leg, a timed note) sorts by that time, and untimed items inherit the time of the item before them so they stay where they were placed. * feat(planner): show the full multi-stop route in the bookings panel The route row now lists every waypoint (FRA -> BER -> HND) by sequence instead of just the first and last airport. * feat(map): draw multi-leg flights as connected legs with a marker per airport Both the Leaflet and Mapbox overlays now render a flight over all its waypoints: one great-circle arc per leg and a marker at every airport, with the label showing the full route and the summed distance. A single-leg flight is unchanged. Also drops the floating stats badge that was drawn on transport arcs. * fix(map): centre a clicked place above the bottom inspector panel Selecting a place panned/flew it to the dead centre of the screen, where it sat behind the detail card. Both overlays now bias the target into the visible area above the bottom panel (Leaflet offsets the pan by the inspector inset; Mapbox passes the padding to flyTo). * feat: show the full multi-stop flight route in PDF and calendar export The PDF day list and the ICS export now render the whole route (FRA → BER → HND) for a multi-leg flight instead of just the first and last airport, falling back to the flat metadata for single-leg flights. The ICS keeps a single event per booking. * feat(import): group connecting flight legs into one multi-leg booking When a booking confirmation contains several flight legs sharing a PNR that connect at the same airport with a short layover (under 24h), they are now imported as a single multi-leg booking (from/stop/to endpoints + metadata.legs) instead of one booking per leg. A round trip (same PNR, multi-day gap) stays two separate bookings, and a single flight is unchanged. * i18n: translate the new flight-route strings into all languages * i18n: translate the Costs page into every language The Budget → Costs rework left the new costs.* strings untranslated in every non-English locale (they fell back to English). Translate them across all supported languages. * Revert "fix(map): centre a clicked place above the bottom inspector panel" This reverts commit 0936103f0438f49f628c0d116ad33312554bad80. * Explore places on the map, planner route fixes, and instance-wide Mapbox (#1147) * feat(maps): add an OSM POI search endpoint (category within a viewport) New /api/maps/pois queries OpenStreetMap via Overpass for places of a category (restaurants, cafes, hotels, sights, …) inside a bounding box. OSM-only by design — it never calls Google, even when a Google key is configured. * feat(map): explore nearby places on the trip map (OSM category pill) A floating, icon-only pill over the planner map lets you toggle a POI category and see those OpenStreetMap places in the current view; clicking a marker opens the add-place form pre-filled (name, address, website, phone). Single-select with a 'search this area' action after the map moves. Renders on both the Leaflet and Mapbox maps, and can be turned off in settings (discussion #841). * fix(planner): anchor timed places when optimising and route transports by location - The day optimiser no longer reshuffles places that have a set time — they stay anchored to their time, like locked places. - The route now uses a transport's departure/arrival location as a waypoint when it has one (e.g. a flight's airport), instead of breaking the route at every booking; transports without a location are ignored for routing but still show their leg's distance/duration under the booking. * feat(admin): instance-wide Mapbox defaults in default user settings Admins can set a shared Mapbox token (plus style, 3D and quality) as instance defaults, so the whole instance can use Mapbox without each user pasting their own key. Users without their own value inherit it via the existing admin-defaults merge; the shared token is stored encrypted (discussion #920). * Reorder whole days and insert a day (#589) (#1148) * feat(days): reorder whole days and insert a day at a position Adds reorderDays + insertDay to the day service and a PUT /days/reorder route (plus an optional position on create). Day rows stay stable so a day's assignments, notes, bookings and accommodations ride along by id; on a dated trip the calendar dates stay pinned to their slots while the content moves across them, and each booking's date is re-stamped onto its day's new date (time-of-day preserved) so day_id stays consistent. Renumbering uses the two-phase write to avoid the UNIQUE(trip_id, day_number) collision, and a move that would invert an accommodation's check-in/out span is rejected. * feat(planner): reorder days from a toolbar popup, and add days A new toolbar button opens a popup listing the days; drag a row by its grip or use the up/down arrows to reorder, and add a day from there. Reorders apply optimistically with rollback and sync over WebSocket; the day headers are left untouched, so the existing place drop-targets are unaffected. * i18n: add day-reorder strings across all languages * Map/planner/dashboard polish and small community features (#1155) * feat(planner): reorder days in a modal instead of a dropdown The day-reorder control opened a small anchored dropdown; move it into the shared Modal (portal, dimmed backdrop, Esc/backdrop close) so it matches the Add activity dialog. Drag handles, up/down arrows and the day badges are unchanged. * feat(map): explore reliability, Mapbox popups + compass, region-biased search POI explore: clamp oversized viewports, query the Overpass mirrors in parallel (first valid response wins) with a per-request timeout and a short-lived cache, and surface a retry when every mirror fails - so it returns results at any zoom instead of timing out. Mapbox renderer: add the place/POI hover popups (name, category, address, photo) the Leaflet map already had, plus a compass pill next to the explore pill that resets the view to north. /api/maps/search: accept an optional locationBias to fix foreign-region bias and expose Google's place types in the result. * feat(dashboard): list-view and mobile polish Use the Archived status label for the filter and show Open dates for trips without dates; drop the unused settings button next to the view toggle. Desktop list view renders the date as a stat-style block separated from the counts. Mobile list rows are stacked (slim cover banner + centred date), trip actions stay visible (touch has no hover), and the hero card's hover lift is disabled on touch; small spacing fix under the sidebar. * feat: small community-requested options Raise the plan-note subtitle limit to 250 characters and add more note icons. Expose is_archived and cover_image on the update_trip MCP tool. Add place coordinates to the PDF export. Allow creating a category from an existing to-do, and add a show/hide toggle on the admin password fields. * test(shared): bump day-note subtitle limit assertion to 250 * test: align specs with the new search param order and archive label Keep lang as the 3rd positional arg of the maps search controller so the existing unit test stays valid, and forward locationBias as the 4th. Add the now-used Popup to the MapViewGL mapbox mock, switch the dashboard archive-filter query to the Archived label, and expect the 4-arg search call. * fix(packing): add more bag colors so sub-bags stop repeating (#1156) The auto-assigned bag palette only had 8 colors, so the 9th bag reused the first one. Double it to 16 (keeping the existing 8 and their order) and keep the server and client lists in sync - both cycle BAG_COLORS[count % length]. * fix(packing): respect per-item quantity in bulk import (#1157) * AirTrail integration: import flights & two-way sync (#214) (#1158) * feat(admin): register AirTrail as an integration addon Off by default; toggle lives in Admin -> Addons with a Plane icon. The per-user connection (URL + API key) follows in integration settings. * feat(integrations): add per-user AirTrail connection Settings -> Integrations gains an AirTrail section: instance URL + Bearer API key (encrypted at rest via apiKeyCrypto), a self-signed-TLS opt-in and a test-connection check. Served by a small Nest controller under /api/integrations/airtrail, gated on the airtrail addon and SSRF-guarded. The key is per-user, so it only ever returns that user's own flights. * feat(transport): import flights from AirTrail Adds an AirTrail Import button next to Manual Transport that lists the user's AirTrail flights and highlights the ones inside the trip dates. Selected flights become reservations linked to their AirTrail origin (external_* columns), deduped against flights already in the trip, then broadcast to every member. The mapping resolves airports, airport-local times and flight metadata; the linkage is what the two-way sync rides on. * feat(transport): badge AirTrail-linked flights as synced Linked reservations show an 'AirTrail synced' badge, or 'no longer synced' once the flight is gone from AirTrail. * feat(transport): keep TREK and AirTrail flights in sync both ways A scheduled poll reconciles each connected owner's flights: field edits (detected by snapshot hash, since AirTrail has no updated_at) flow into the linked reservation and broadcast live; a flight deleted in AirTrail keeps the TREK row but stops syncing. Editing a linked flight in TREK pushes back to AirTrail under the importer's credentials, preserving the existing seat manifest; if the owner disconnected the link detaches so the poll can't revert the local edit. Deleting in TREK never touches AirTrail. * i18n(airtrail): add AirTrail strings across all locales * test(airtrail): cover flight mapping, timezones and snapshot hashing * fix(airtrail): reduce airline/aircraft objects to codes The flight list/get response returns airline and aircraft as joined objects ({icao, iata, name, ...}), not bare codes. Mapping them straight through produced '[object Object]' titles and stored objects in metadata, which crashed reservation rendering. Extract the ICAO/IATA code instead, and title flights by their flight number. * fix(airtrail): clear error on non-JSON responses, tolerate /api in URL A misconfigured instance URL made AirTrail serve its SPA/login HTML, and the raw JSON.parse failure surfaced as 'Unexpected token <'. Surface an actionable message instead, and strip a pasted trailing /api so the base URL still resolves. * feat(transport): sync AirTrail edits on trip open, not just on the poll Add a per-user on-demand sync (POST /integrations/airtrail/sync) triggered when a connected user opens a trip, so AirTrail-side edits appear right away instead of waiting up to a full poll cycle. Lower the background poll from 15 to 5 minutes as a safety net. * fix(transport): refresh imported AirTrail flights without a reload loadTrip doesn't fetch reservations, so a freshly imported flight only appeared after a full page reload — use loadReservations instead. Also show flight dates in the user's locale format (e.g. 13.06.2026) rather than the raw ISO string. * style(settings): align AirTrail connection with the photo-provider layout Match the Immich section: stacked URL/key fields, a ToggleSwitch for self-signed TLS, and a Save / Test-connection row with a status badge. * feat(transport): add a seat field when editing flights The transport editor only offered a seat field for trains; flights had none even though imports store metadata.seat. Show and persist a seat for flights too. * style(transport): match the AirTrail button height to Manual Transport * feat(transport): put the flight seat next to flight number and sync it to AirTrail Move the seat from a standalone row to the per-leg flight details (beside the flight number), stored per leg in metadata.legs[].seat with the first leg mirrored to metadata.seat. On push, set the seat number on the user's own AirTrail seat (the one with a userId), leaving co-passengers untouched; import/poll read that same seat back. * refactor(planner): move the AirTrail trip-open sync into useTripPlanner Page containers must not own state/effects (lint:pages). Same logic, relocated from the page into its data hook. * test(db): pin the region-reconciliation test to its schema version The test re-ran 'the last migration' assuming the reconciliation is last; it no longer is once later migrations are appended. Pin to version 135 and re-run from there (the appended migrations are idempotent). * Various fixes: 2FA autofocus, viewer-timezone times, duplicate place guard (#1159) * fix(auth): autofocus the 2FA code input when the MFA step appears (#767) * fix(notifications): show notification and admin times in the viewer timezone (#1149) SQLite CURRENT_TIMESTAMP is UTC but the string has no Z, so the client parsed it as local time. Normalize in-app notification created_at to ISO-UTC, and stop forcing the admin user table to render in the server timezone. * fix(places): warn before adding a duplicate place (#1152) Manually adding a place did not check the existing pool, so the same POI could land in Unplanned twice. Flag a likely duplicate by Google Place ID, name or near-identical coordinates and require a confirming second click to add anyway. * fix(planner): make route tools reachable in mobile day plan sheet (#1142) * wiki: update dev env * wiki: small precision in dev env * fix(planner): make route tools reachable in mobile day plan sheet On mobile, selecting a day closes the plan sheet immediately, so the route tools footer (Route toggle / Optimize / routing profile) - gated on the selected day - was never reachable. Desktop was unaffected. - Add showRouteToolsWhenExpanded prop to DayPlanSidebar: when set, route tools render on any expanded day with 2+ assigned places - Make handleOptimize accept an explicit dayId (defaulting to selectedDayId, preserving desktop behavior) - Keep the distance/duration pill gated on the selected day, since routeInfo belongs to the selected day's calculated route - Enable the prop on the mobile plan sheet in TripPlannerPage * fix(planner): correct route-tools prop doc and dev-environment wiki - Reword the showRouteToolsWhenExpanded JSDoc to list the controls the footer actually renders (Route toggle / Optimize / travel profile); there is no "Open in Google Maps" action in that block. - Wiki: drop the non-existent server test:parity script, document the real shared i18n:parity checks, and fix the i18n note (the translation layer already lives in @trek/shared, it is not "upcoming"). --------- Co-authored-by: jubnl <jgunther021@gmail.com> Co-authored-by: Maurice <mauriceboe@icloud.com> * feat(places): enrich list-imported places via the Places API (#886) (#1161) * feat(places): enrich list-imported places via the Places API (#886) Google/Naver list imports only carry a name and coordinates, so the places open as bare pins — the Maps tab jumps to coordinates, with no photo, address or open/closed. Add an opt-in "Enrich places via Google" toggle to the list-import dialog, shown only when a Google Maps key is configured. When enabled, after the (fast, unchanged) import the server runs a background pass that re-resolves each place by name — biased to and validated against the imported coordinates so a common-name search cannot overwrite the wrong place — and fills the empty address/website/phone/photo columns plus the resolved google_place_id, pushing each row over the live sync. Opening hours and the proper Maps link then work on demand from the stored id. Enrichment only fills empty fields, runs detached so a long list never blocks the import, and no-ops when no key is configured. * fix(places): use the ToggleSwitch component for the enrich toggle Match the rest of the app — the import-enrichment opt-in used a raw checkbox; swap it for the shared ToggleSwitch (text left, switch right) like the settings toggles. * fix(maps): bound place-photo cache growth (Wikimedia + Google) (#1174) The place-photo cache (uploads/photos/google) grew unbounded: a Wikimedia geosearch path cached full-res originals despite requesting a 400px thumb, the writer applied no size guard, nothing reclaimed orphaned files, and backups archived the whole re-derivable cache verbatim. - Prefer the scaled `thumburl` over the full-res `info.url` in the Commons geosearch fallback. - Downscale any cached image to <=800px JPEG via the existing jimp dep, with a safe fallback to the original bytes on decode failure. - Add sweepOrphans() (orphaned meta rows + stray files) wired into the scheduler (startup + nightly), and removeIfUnreferenced() called on place delete for prompt reclamation. - Exclude the re-derivable photo/trek caches from backups; restores self-heal as the cache dirs are recreated at startup. * fix(sync): remap temp ids, prevent id collisions, surface failed mutations (#1175) Closes three offline BLOCKERs from the PWA audit: - B1: offline edits/deletes of an offline-created entity were lost. The negative temp id was baked into the PUT/DELETE url and never rewritten after the CREATE returned a real id, so dependents 404'd and were dropped. Dependents now carry a {id} placeholder + tempEntityId; flush builds a tempId->realId map and durably rewrites still-queued dependents on CREATE success (survives flush boundaries / reloads). - B2: tempId = -(Date.now()) collided within a millisecond, overwriting an optimistic row. Replaced with a monotonic nextTempId() minter. - B3: any 4xx marked the mutation failed with no rollback and no signal, and the badge ignored failed rows. Terminal failures now roll back the phantom optimistic CREATE; 401/408/425/429 are treated as retryable; failedCount() is surfaced in OfflineBanner (red pill) and OfflineTab. * fix(maps): make offline tiles cover real trips (cap coherence + zoom-clamp) (#1177) Closes BLOCKER B5 — the offline map was blank for most real trips: - The Workbox 'map-tiles' cache held only 1000 entries while the prefetcher budgeted ~3413, so prefetched tiles were evicted on arrival. Both caps are now a coherent 12288 (~180 MB), kept in sync with cross-referencing comments. - prefetchTilesForTrip skipped a trip entirely when its all-zooms estimate exceeded the cap, so region/road-trip bboxes got no tiles. Removed the all-or-nothing guard; prefetchTiles already fills zooms low→high and stops at the budget, so large trips now cache the zooms that fit instead of nothing. * fix(security): stop cross-user offline data leak on shared devices (#1176) Closes BLOCKER B4 — three reinforcing paths could serve one account's cached data to the next user on a shared device: - The Workbox 'api-data' cache keyed trip/user-scoped GETs by URL only (cookie-blind). Changed to NetworkOnly; offline reads come from the per-user IndexedDB cache via the repo layer instead. - IndexedDB had no per-user scoping. The Dexie connection is now scoped per user (trek-offline-u<id>) behind a Proxy so the ~19 importers keep a stable binding; login opens the user DB, logout deletes it and returns to the anonymous DB. - logout() was fire-and-forget and racy: background flush/syncAll could re-seed the DB after the wipe. It is now async and ordered — close an auth gate, unregister sync triggers, disconnect, clear caches, delete the user DB — and flush()/syncAll() bail when the gate is closed. * fix(db): scope, evict, and cap the offline blob cache (H3) (#1178) Blob cache previously leaked forever: clearTripData omitted it, entries had no trip discriminator, and there was no size/count bound, so file blobs survived trip eviction and could starve the map-tile cache for quota. - BlobCacheEntry gains tripId + bytes; Dexie v3 adds a tripId index with a backfill upgrade (legacy rows -> tripId -1, bytes from blob.size) - clearTripData purges the trip's blobs in-transaction - enforceBlobBudget() evicts oldest-by-cachedAt past 200 entries / 100 MB - tripSyncManager threads tripId/bytes into puts and enforces the budget * fix(repo): fall back to Dexie when a network read fails (H2) (#1179) Repos gated reads on raw navigator.onLine and the online branch had no try/catch, so a captive portal or connected-but-no-internet (navigator.onLine lying "true") threw a network error instead of serving the good cached copy — blanking the trip even though Dexie held it. - new onlineThenCache(onlineFn, cacheFn) helper: reads the cache when offline, and on a network-level failure (Axios error with no HTTP response). A genuine HTTP error (4xx/5xx — the server responded) is rethrown so callers still set error state / navigate, not masked by a stale cache. - gates only on navigator.onLine, NOT the connectivity probe: the probe is a coarse global flag and one failed health check would otherwise divert every read to the (possibly empty) cache even when the request would succeed. - every repo list/get read path routed through it (reads only — writes still go through the mutation queue so failures surface) - tests: captive-portal fallback, HTTP-error rethrow, non-Axios rethrow * fix(store): reset and uniformly hydrate trip-scoped slices in loadTrip (H4, H5) (#1180) loadTrip only replaced the first slice group, so budget/reservations/files from a previous trip stayed visible after switching trips (data exposure on a shared screen). Those three also loaded via separate tab-gated effects, so they never hydrated offline for an unopened tab. - resetTrip() clears every trip-scoped slice (keeps global tags/categories) and runs at the top of loadTrip, so a switch can't leak the prior trip's data - loadTrip now hydrates budget/reservations/files through their repos alongside the rest (non-fatal catches), making offline hydration uniform - useTripPlanner drops the redundant loadFiles + reservations/budget effects; tab-gated lazy reloads stay as on-demand refresh - tests: cross-trip no-leak, uniform hydration, resetTrip * fix(sync): re-hydrate active trip store on reconnect/online (H1) (#1181) setRefetchCallback was dead code, so on reconnect the queue flushed and Dexie re-seeded but the open trip's Zustand store was never refreshed — a collaborator's edits made while we were offline didn't appear until navigating away and back. - new tripStore.hydrateActiveTrip(): silent refresh of the active trip's collaborative state (days/places/packing/todo/budget/reservations/files), no resetTrip and no isLoading toggle so there's no splash on reconnect - syncTriggers wires setRefetchCallback to it (WS layer awaits the flush hook first) and re-hydrates open trips after the online-event syncAll; cleared on unregister - websocket exposes getActiveTrips() for the online-event path - tests: refetch wiring + ordering, silent hydrate without reset/splash * fix(server): lengthen idempotency key TTL to survive multi-day offline (H6) (#1182) The nightly cleanup deleted idempotency keys older than 24h. The TREK client replays queued mutations with their X-Idempotency-Key on reconnect, so a device offline longer than a day had its keys GC'd before it returned — the replayed POST was then treated as new and created a duplicate. - raise the TTL to 30 days (DEFAULT_IDEMPOTENCY_TTL_SECONDS), overridable via IDEMPOTENCY_TTL_SECONDS - extract purgeExpiredIdempotencyKeys(now, ttl, db) (mirrors cleanupOldBackups) with an injectable db, and have the cron job call it - tests: 30-day default eviction, 25-day key retained (was dropped at 24h), env override H7 (exactly-once across the lost-response window) is deferred: a correct fix must store the response in the same DB transaction as the entity write. Doing it in the generic interceptor (reserve-before-handler) cannot store the real response body for the crash case, which would break the client's temp->real id remapping on replay (mutationQueue.flush relies on the entity in the body). It needs a per-service change and is tracked separately. * fix(realtime): correct assignment:created echo dedup (H11) (#1183) When X-Idempotency/X-Socket-Id let an own-echo through, the assignment:created dedup had two bugs: it keyed on place id, so (1) a legitimate second assignment of a place already on the day was silently dropped, and (2) the temp-version reconciliation matched place?.id === placeId, letting undefined === undefined collapse place-less rows onto each other. - dedup now keys on assignment id (exact-id duplicate -> no-op) - temp (negative-id) optimistic rows are reconciled only when a real placeId matches, replacing just that row; a sibling temp of another place is untouched - everything else appends, including a genuine 2nd assignment of the same place - tests: 2nd-of-same-place kept, correct temp picked among siblings, place-less rows don't collapse Note: the broader own-echo suppression relies on X-Socket-Id being sent; this fixes the client-side fallback when an echo slips through. * fix(pwa): persist offline storage + Mapbox offline policy (H8, H9) (#1184) H8: prefetched tiles and file blobs could be evicted under storage pressure (worsened by opaque tile responses inflating the quota ~7MB each), blanking the offline map right when a traveler needs it. Request persistent storage at app init so the browser exempts our caches from eviction. We deliberately keep tile requests no-cors (a cors switch would break self-hosted/custom tile providers without CORS headers), so persistence is the safe mitigation rather than de-opaquing responses. H9: Mapbox GL users had no offline map at all — no runtimeCaching matched the Mapbox hosts. Add a StaleWhileRevalidate rule for api.mapbox.com / *.tiles.mapbox.com so visited areas are available offline (best-effort; full pre-download still requires the Leaflet renderer, now documented). - new sync/persistentStorage.ts requestPersistentStorage(), called from main.tsx - vite.config: mapbox-tiles SW cache rule - MapViewAuto / tilePrefetcher comments document the offline-maps policy - tests for the persist helper (granted / already-persisted / absent / rejects) * ci(security): only fail Docker Scout on fixable CVEs Add only-fixed so the scan no longer fails on vulnerabilities with no upstream fix available (e.g. base-image OS packages), and only flags actionable, fixable findings. * build(docker): rebuild gosu with a current Go toolchain Debian's apt gosu ships an old Go stdlib that the image CVE scan flags (1 critical + several high, all in golang/stdlib). Build gosu from source with a current Go toolchain and copy the static binary in instead; the runtime behaviour is unchanged — gosu still drops root to node at startup. * build(deps): bump tsx's esbuild to 0.28.1 (GHSA-gv7w-rqvm-qjhr) The production image's last image-scan finding was esbuild 0.28.0, pulled in transitively by tsx. Pin tsx's esbuild to 0.28.1 (within tsx's ~0.28.0 range) to clear GHSA-gv7w-rqvm-qjhr. Lockfile-only; no runtime change. * feat(auth): add "Remember me" checkbox to extend session lifetime (#1189) Adds a "Remember me" checkbox to the login form (single responsive page, covers mobile + desktop). Unchecked (default) issues the existing SESSION_DURATION JWT with a browser-session cookie (no maxAge); checked issues a longer-lived JWT plus a persistent cookie sized by the new SESSION_DURATION_REMEMBER env var (default 30d). The choice is threaded through the MFA verify leg so it survives the step-up. Register/demo logins keep their current persistent behaviour. * chore(ssrf): include lookup error code in error message * fix(backup): restore from Docker, fail-fast on shadowed /app, bundle encryption key (#1193) (#1197) * fix(backup): restore uploads through symlinked dir and bundle encryption key (#1193) Restoring a backup inside Docker threw ERR_FS_CP_DIR_TO_NON_DIR because /app/server/uploads is a symlink to the mounted /app/uploads volume and cpSync (dereference:false) refuses to overwrite the symlink node with a directory. The DB was swapped before this failing copy, so users saw restored data but missing upload files (trip covers). Resolve the symlink with realpathSync before copying so the merge targets the real directory; no-op on a plain dir, so non-Docker behavior is unchanged. Also bundle the at-rest encryption key (data/.encryption_key) into the backup so a restore onto a different install can decrypt stored secrets (API keys, MFA, SMTP/OIDC). Skipped when ENCRYPTION_KEY is provided via env (the file is not the source of truth then). On restore the key is swapped back if the archive carries one; a restart is required for the in-memory key to take effect. * fix(docker): fail fast when a volume shadows /app (#1193) Mounting an old volume at /app hides the image's node_modules and dist, so startup crashed with a cryptic "Cannot find module 'tsconfig-paths/register'". Add a CMD preflight that detects the missing app files and exits with actionable guidance. Document in the README that only /app/data and /app/uploads should be mounted, never /app. * fix: ssrf test * fix(places): fall back to search when autocomplete details lookup fails (#1192) (#1198) Clicking an auto-suggest dropdown item did a second /maps/details lookup that could fail (details kill-switch off, an overloaded OSM Overpass mirror behind a proxy, or any upstream error), dead-ending on "Place search failed" while the search button stayed reliable. handleSelectSuggestion now treats a missing or coordinate-less details result (or a thrown error) as a miss and falls back to the text-search path the search button uses, applying the first result. The error toast only fires if the fallback also returns nothing. Adds tests for the previously untested suggestion-click path. * fix(planner): scroll long place description/notes on mobile (#1195) (#1199) The place details card (PlaceInspector) clipped long description/notes with no way to scroll. The content area is a flex column whose children (description/notes) had the default flex-shrink: 1, so once the card hit its maxHeight cap they compressed to fit and their overflow:hidden clipped the text instead of overflowing into a scroll region. - Make the content area a bounded scroll region (flex: 1 1 auto, minHeight: 0, overflowY: auto, momentum + overscroll containment). - Pin description/notes with flexShrink: 0 so they keep natural height and the card overflows into the scroll instead of clipping. - Pin header/footer with flexShrink: 0 so they stay fixed while scrolling. - Add wordBreak/overflowWrap to the description div to fix horizontal clip. * Day plan: hotel travel times at start/end + login toggle polish (#1206) * fix(login): use the shared toggle for the stay-signed-in option * feat(planner): show hotel travel times at the start and end of a day * fix(login): give the stay-signed-in toggle an accessible name and fix its test * fix(trips): keep the day-count field empty when cleared and validate it (#1204) (#1207) * docs(readme): refresh dashboard, costs and trip screenshots (#1208) * docs(readme): refresh dashboard, costs and trip screenshots * docs(readme): correct outdated info (React 19, NestJS, 20 languages, Costs rename, passkeys, AirTrail, notifications) * chore: update all dependencies (#1209) * chore: update all dependencies * chore: remove lint errors * fix(client): restore typecheck after dependency bump vitest 4 types vi.fn() as Mock<Procedure | Constructable>, which no longer assigns to the strictly-typed onUpdate prop; type the mock explicitly. TS6 + the new transitive @types/node 25 stopped auto- including node builtin module types, so import('node:buffer') failed; add @types/node as a direct client devDependency and a scoped node type reference in the one test that needs it. * test: fix constructor mocks for vitest 4 Reflect.construct semantics vitest 4 resolves new-invoked mocks via Reflect.construct, which rejects arrow-function implementations (including mockReturnValue sugar) as non-constructable. Convert mapbox-gl and better-sqlite3 mocks that the code instantiates with new to regular function implementations. * fix(planner): only route to multi-day transport endpoints on their pickup/drop-off days (#1210) (#1212) * chore: move to Frankfurter API for exchange rate (#1214) * Restore nest coverage to >=80% after the #1209 dep bump (istanbul provider + branch tests) (#1213) * fix(server): set oxc:false in vitest so the SWC transform survives the Vite 8 bump * fix(server): switch coverage to the istanbul provider (v8 under-reports branches on Vite 8 + Vitest 4) * test(nest): cover controller/service branches to clear the 80% coverage gate * fix(planner): correct transfer-day hotel legs and connect them to transports (#1215) When you change hotels on a day, the morning bookend leg showed the hotel you check into instead of the one you slept in whenever the morning stay didn't end exactly on that day — both bookends collapsed onto the arriving hotel. The morning hotel is now picked by "checked in earlier and still in range" rather than "checks out today", which also fixes the route optimizer's start anchor for the same case. The bookend legs now connect to the first/last located waypoint of the day — a place or a transport endpoint (a car return, a taxi or train arrival) — so the hotel-to-transport drives are included too. * feat(transports): add kitinerary import-from-file button to Transports tab * docs(config): document SESSION_DURATION_REMEMBER across deployment artifacts Add SESSION_DURATION_REMEMBER to docker-compose, .env.example, README env table, Helm chart (values + configmap passthrough), the Unraid template, and the Unraid install guide. Where the base SESSION_DURATION was also absent (README, charts, Unraid) add the pair so the Remember-me variable has context. --------- Co-authored-by: gzor <risenbrowser@web.de> Co-authored-by: ppuassi <34529179+ppuassi@users.noreply.github.com> Co-authored-by: sss3978 <106522699+soma3978@users.noreply.github.com> Co-authored-by: SkyLostTR <onurluerin@gmail.com> Co-authored-by: Julien G. <66769052+jubnl@users.noreply.github.com> Co-authored-by: Dimitris Kafetzis <39215021+Dkafetzis@users.noreply.github.com> Co-authored-by: Ahmet Yılmaz <70577707+sharkpaw@users.noreply.github.com> Co-authored-by: jubnl <jgunther021@gmail.com> Co-authored-by: jufy111 <40817638+jufy111@users.noreply.github.com> Co-authored-by: Larinel <bodink7@gmail.com> Co-authored-by: rossanorbr <48014819+rossanorbr@users.noreply.github.com> |
||
|
|
25f326a659
|
v3.0.16 — bug fixes (#964)
* fix(mcp): MCP RFC compliant for more strict clients * fix(mcp): serve flat /.well-known/oauth-protected-resource for ChatGPT reconnect Clients such as ChatGPT probe the flat well-known URL on every fresh discovery cycle (i.e. after a full disconnect/reconnect where cached OAuth state is cleared). The SDK's mcpAuthMetadataRouter only serves the path-based form /.well-known/oauth-protected-resource/mcp, so the flat probe returned 404. Without the resource metadata, ChatGPT fell back to the issuer URL as the resource parameter (https://…/ instead of https://…/mcp). The authorize handler then rejected it with invalid_target and redirected back to ChatGPT's callback with an error — showing the user the TREK home page instead of the consent form. Add an explicit GET handler for the flat URL that returns the same protected resource metadata, so the resource URI is discovered correctly on the first probe. * fix(mcp): fix OAuth popup blank page — SW denylist and COOP header Service worker was intercepting /oauth/authorize navigate requests (not in denylist), serving index.html, and React Router's catch-all redirected to / instead of the SDK authorize handler. Helmet's default COOP: same-origin isolated the /oauth/consent popup from its cross-origin opener, making window.opener null and breaking the popup-based OAuth completion signal for ChatGPT and similar clients. * fix(ntfy): encode non-Latin-1 header values with RFC 2047 to prevent ByteString crash Todo/trip names containing chars like → or € (and non-Latin-1 locale templates for Czech, Chinese, Russian, etc.) caused the Fetch API to throw when setting the ntfy Title header. Apply RFC 2047 base64 encoded-word encoding for any header value containing chars above U+00FF; ntfy decodes this automatically. * docs(mcp): document Cloudflare bot detection blocking ChatGPT MCP requests Add Cloudflare WAF note to MCP-Setup and a full troubleshooting entry covering root cause (IP reputation + UA heuristics), free-plan limitation (disable Bot Fight Mode entirely, with explicit warning), and paid-plan WAF skip rule with the full expression syntax and path table for all MCP/OAuth/.well-known routes. * fix(pwa): detect upstream proxy auth challenges and recover gracefully Behind Cloudflare Zero Trust or Pangolin, cross-origin auth redirects on /api/* calls surface as CORS errors (error.response === undefined) that the existing 401 interceptor never catches, leaving the PWA stuck with network-error toasts instead of re-authenticating. New connectivity module probes /api/health every 30s using fetch with cache:no-store and inspects Content-Type to reliably detect whether the server is reachable vs intercepted by an upstream proxy. axios interceptor changes: - On !error.response + navigator.onLine: run probeNow(); if the health probe also fails (proxy is intercepting all requests), trigger a guarded window.location.reload() so the edge proxy can intercept the top-level navigation and run its auth flow (covers CF Access and Pangolin 302 mode) - On error.response status 401 with text/html body: same reload path, covering Pangolin header-auth extended compatibility mode which returns 401+HTML instead of a 302 redirect. TREK own 401s are always JSON so there is no collision with the existing AUTH_REQUIRED branch. - sessionStorage flag prevents reload loops; cleared on any successful response so the guard resets after re-auth. /api/health excluded from SW NetworkFirst cache (vite.config.js regex) and Cache-Control: no-store added server-side so probes always hit the network and cannot be served stale from the 24h api-data cache. LoginPage caches last-known appConfig in localStorage so the SSO button renders in OIDC+UN/PW dual mode even when the config fetch is intercepted by the proxy. Auto-redirect to IdP skipped when config comes from cache to avoid redirect loops while the proxy is challenging. Fixes discussion #836. * fix(files): add bottom-nav padding to files tab wrapper on mobile * fix(budget): expose toolbar on mobile so users can add budget categories * fix(pwa): unregister SW before proxy-reauth reload so Pangolin can challenge WorkBox's NavigationRoute served the cached SPA shell on window.location.reload(), meaning Pangolin/CF Access never saw the navigation and the app was left stuck showing stale offline data. Unregistering the SW first lets the navigation reach the network so the upstream proxy can run its auth flow. Also rebuilds server/public with corrected sw.js (health excluded from NetworkFirst, /oauth/ and /.well-known/ added to NavigationRoute denylist). * chore: remove committed build artifacts from server/public Dockerfile and Proxmox community script both rebuild client/dist and copy it into server/public at build time — committed artifacts were never used. Replace with .gitkeep and add server/public/* to .gitignore. * chore: add build-from-sources script |
||
|
|
6072b969d6
|
Bug fixes - May 2nd 2026 (#941)
* fix: collab chat input hidden by mobile bottom nav bar Closes #939 * chore: prepare database for nest + typeorm * fix(ssrf): relax internal network resolution (#947) * docs(ssrf): update Internal-Network-Access wiki to reflect relaxed guard Loopback, link-local, and .local/.internal hostnames are now all overridable with ALLOW_INTERNAL_NETWORK=true (commit 9a08368). Merge the two-tier "always blocked / conditionally blocked" structure into a single table, add a warning about cloud metadata exposure. * fix(ssrf): let .local/.internal hostnames pass to IP-level checks The pre-DNS hostname block was redundant: any .local/.internal host that resolves to a private IP is already gated by isPrivateNetwork + ALLOW_INTERNAL_NETWORK, and any that resolves to loopback/link-local is caught by isAlwaysBlocked unconditionally. Dropping the hostname pre-check means Docker/LAN deployments can reach services on .local hostnames (e.g. immich.local) with ALLOW_INTERNAL_NETWORK=true, while loopback and link-local IPs (including 169.254.169.254) remain hard-blocked with no override. Reverts the isAlwaysBlocked guard loosening from 9a08368. * fix(auth): trim username and email on all write paths Self-registration stored values verbatim, so trailing whitespace could produce rows that lookup code (which trims input) silently misses. Trim username and email before validation and INSERT in registerUser, adminService.updateUser, and oidcService.findOrCreateUser. updateSettings and adminService.createUser already trimmed correctly. Adds a one-shot backfill migration (trimUserWhitespace) that trims existing dirty rows; collisions are resolved by appending __migrated_<id> to the value with a loud console.warn so operators can review affected accounts. 18 new tests covering registration trim, duplicate detection, admin update trim, trip-member lookup regression, and all migration branches. * feat(notices): add v3014-whitespace-collision admin notice Adds a dismissible banner for admins on v3.0.14+ that fires only when the whitespace-trimming migration detected a username/email collision (stored in app_settings as whitespace_migration_collision=true). Notice conditions: existingUserBeforeVersion(3.0.14) + role=admin + custom predicate reading the app_settings flag. Predicate registered in registry.ts; migration step writes the flag when hadCollision=true. All 15 translation files updated with title/body keys. 7 integration tests added (SN-COLLISION-1 through -7) covering all condition branches: shown when all conditions met, hidden when flag absent/false, hidden for non-admin, hidden for new user, hidden below min app version, hidden after dismissal. |
||
|
|
1f5deeba6c
|
Bug fixes - April 27th 2026 (#907)
* fix: clean up dangling FK references before deleting a user Resolves FOREIGN KEY constraint failed (500) on DELETE /api/admin/users/:id and DELETE /api/auth/me when the target user had rows in trip_members.invited_by, share_tokens.created_by, budget_items.paid_by_user_id, journeys.user_id, journey_entries.author_id, journey_contributors.user_id, or journey_share_tokens.created_by — none of which had ON DELETE clauses. Introduces deleteUserCompletely() in userCleanupService.ts which wraps all cleanup and the final DELETE FROM users in a single transaction. Both adminService.deleteUser and authService.deleteAccount now call it instead of the bare DELETE. Tests ADMIN-005b and AUTH-040 cover all reference types including notification sender/recipient and notice dismissals. * test: extend FK deletion tests to cover journeys, files, and photos ADMIN-005b and AUTH-040 now also seed and assert: - owned journey with entries (cascade-deleted via journeys.user_id cleanup) - trip_files.uploaded_by (SET NULL — file survives, attribution cleared) - trek_photos.owner_id (SET NULL — photo record survives, owner cleared) - trip_photos.user_id (CASCADE — photo association removed) * test: extend user deletion tests to cover all FK relationships ADMIN-005b and AUTH-040 now seed and assert every user FK relationship: CASCADE (row deleted): trips, trip_members, tags, mcp_tokens, oauth_tokens, oauth_consents, vacay_plans, vacay_plan_members, bucket_list, visited_countries, visited_regions, packing_templates, invite_tokens, collab_notes, settings, password_reset_tokens, notification_channel_preferences SET NULL (row survives, column nulled): categories, todo_items.assigned_user_id, packing_bags, audit_log Caught and fixed: notification_preferences was dropped in migration 72; correct table is notification_channel_preferences. * fix: preserve URL hash and OIDC redirect target through login flow - Include location.hash in redirect param at all three producer sites (ProtectedRoute, axios 401 interceptor, OAuthAuthorizePage) so hash fragments survive the login bounce - Stash redirectTarget in sessionStorage before any OIDC provider redirect and restore it after the code exchange, since the IdP strips the original ?redirect= param during the roundtrip - Clear sessionStorage on OIDC error to avoid stale state - Add tests covering sessionStorage stash on mount, navigate to saved redirect after OIDC exchange, fallback to /dashboard, and cleanup on error * fix: use day position instead of ID for accommodation date range clamping Math.min/Math.max over raw day IDs breaks the start/end picker when a trip's day IDs are non-monotonic relative to day_number (normal after repeated generateDays extend/shrink cycles). Replaced with findIndex lookups so clamping is always based on positional order. Closes #889 * fix: normalize env var comparisons to be case-insensitive All NODE_ENV, DEMO_MODE, OIDC_ONLY, FORCE_HTTPS, COOKIE_SECURE, and ALLOW_INTERNAL_NETWORK checks now use .toLowerCase() so values like 'Production' or 'True' behave identically to their lowercase forms. Also adds APP_VERSION to the startup banner. * fix: delete surplus days when shortening a trip When shrinking a trip's date range, surplus days are now deleted along with their assignments, notes, and accommodations (cascade). Places remain in the trip pool; reservations keep their day reference nulled by the existing ON DELETE SET NULL constraint (issue #909). Updates TRIP-SVC-011 to reflect the new behaviour; adds TRIP-SVC-016 as a regression test for the empty-day case. * fix: auto-backup retention deletes itself and manual backups on Docker Two bugs in cleanupOldBackups: 1. Filter was .endsWith('.zip') — swept manual backup-*.zip files too. Now restricted to auto-backup-* prefix. 2. Age was derived from stat.birthtimeMs, which is 0 on overlayfs (Docker default), making every backup appear epoch-old and get deleted immediately. Age is now parsed from the filename timestamp and falls back to mtimeMs (reliable on overlayfs). Also converts inline require('./services/auditLog') calls to a static import throughout scheduler.ts, and adds 8 unit tests covering the fixed retention logic including the overlayfs regression case. * test: update TRIP-024 to match delete behavior on trip shrink * feat: add bypass-branch-check label to skip branch enforcement |
||
|
|
4436b6f673
|
fix(journey,pdf): journey reorder sort_order + PDF multi-day transport (#848)
* fix(journey): make sort_order authoritative for within-day entry ordering Reorder buttons appeared broken because the server ORDER BY put entry_time before sort_order, so entries synced from trip places with differing times would always sort by time regardless of sort_order writes. The client store mirrored the same comparator, making even the optimistic update invisible. - Change ORDER BY to (entry_date, sort_order, id) in getJourneyFull and listEntries - Fix syncTripPlaces and onPlaceCreated to assign MAX+1 sort_order per day instead of day_number/0 - Update client store comparator to match - Add DB migration to backfill sort_order using old effective key (entry_time, id) so existing journeys retain their visual order - Add tests: JOURNEY-SVC-089–093, FE-STORE-JOURNEY-018–019 Closes #846 * fix(pdf): include multi-day transport return/arrival in PDF itinerary (#847) Reservations were matched to days by pickup date only, so the end-day card (e.g. car Return, flight Arrival) was silently dropped from the PDF. Add span-aware helpers mirroring DayPlanSidebar logic: match by day_id/end_day_id span, show reservation_end_time on end days, prefix title with phase label (Return/Arrival/etc.), and use per-day position for sort order. * test(pdf): add missing day_id to transport reservation fixture |
||
|
|
58218ff5f6
|
fix(oidc,ui): restore Authentik login and fix mobile delete dialog (#845)
OIDC: when OIDC_DISCOVERY_URL is explicitly set, trust the discovery doc's issuer for id_token comparison instead of rejecting a path mismatch as an error. Authentik (and similar realm-path providers) return a canonical issuer like /application/o/<slug>/ that differs from the operator's base OIDC_ISSUER. Strict equality blocked login in 3.x despite working in v2. Default discovery (no custom URL) keeps the strict check. Adds OIDC-SVC-037/038/039. UI: ConfirmDialog and CopyTripDialog lacked the --bottom-nav-h paddingBottom offset that other overlays already use. On mobile portrait the action buttons were hidden behind the sticky bottom nav bar. Closes #843 Closes #844 |
||
|
|
7798d2a3fd
|
fix(oidc): normalize id_token iss claim before issuer comparison (#837)
jwt.verify does an exact string match on the issuer. Providers like Authentik include a trailing slash in the id_token iss claim while the configured issuer is already normalized (no trailing slash), causing every login attempt to fail with jwt issuer invalid. Move the issuer check out of jwt.verify options and apply the same trailing-slash normalization used in the discovery doc validation. Also adds OIDC-SVC-033–036 unit tests covering exact match, trailing slash, wrong issuer, and wrong audience cases. Closes #834 |
||
|
|
ba7b99fb7d
|
fix: update backend tests and service bugs for gallery 1-to-N schema
updatePhoto: write sort_order to journey_entry_photos (junction) not journey_photos, since JP_SELECT reads jep.sort_order — updating the gallery row had no visible effect. deletePhoto: include id in return value so callers that check deleted.id still work. Tests updated for new schema: - journeyShareService: insertJourneyPhoto helper now inserts into journey_photos (keyed by journey_id) + journey_entry_photos junction instead of the old entry_id-keyed table - SVC-081: deleteEntry cascades junction rows (journey_entry_photos), not gallery rows (journey_photos); assert junction is gone, gallery is preserved - SVC-086: syncTripPhotos now populates the gallery directly — no [Trip Photos] wrapper entry; assert journey_photos gallery row instead - INT-028: error message updated to 'journey_photo_id required' |
||
|
|
5eaf7492dc
|
fix(backups,files): auto-backups rejected by validator; trip file download broken after cookie migration
Fixes #773: isValidBackupFilename regex anchored to ^backup- rejected all auto-backup-* filenames, causing 400 on download/restore/delete. Broadened to ^(?:auto-)?backup-. Fixes #774: three regressions in the trip Files tab — - openFile import shadowed by a local function of the same name inside FileManager; PDF preview modal was calling the local with a URL string, corrupting state and crashing on the second click (mime_type read on undefined). Fixed by aliasing the import as openFileUrl. - GET /:id/download used a bespoke authenticateDownload that checked only Bearer header and ?token= query param, ignoring the trek_session cookie. After the JWT-to-cookie migration the client sends cookies only, so every download silently 401-ed. Extended authenticateDownload to accept req and check cookie → Bearer → query token in priority order. - files.download and files.openError translation keys were missing from all 15 locale files; t() was returning the raw key as a truthy string, defeating the || 'Download' fallback. |
||
|
|
edf14e2ebc
|
test(maps): update getPlacePhoto stubs to use text() instead of json()
mapsService now reads the details response body via .text() before parsing, so test stubs need text() rather than json(). |
||
|
|
c857d38bcd | test(notifications): bump event_types count to 9 after adding todo_due | ||
|
|
dd90c6d424
|
fix(mcp): add RFC 9728 PRM, RFC 8707 audience binding, and collab sub-feature gating
Root cause: claude.ai's MCP connector (spec 2025-06-18) requires the resource server to publish Protected Resource Metadata and return WWW-Authenticate on 401s to bind the /mcp endpoint to its AS. Without these, it silently shows no tools after OAuth. - Add /.well-known/oauth-protected-resource (RFC 9728) with addon gating - Emit WWW-Authenticate: Bearer resource_metadata=... on 401/auth-failure 403s - Open CORS (origin: *) on both .well-known/* endpoints per RFC 8414/9728 - Accept resource parameter at authorize + token endpoints (RFC 8707) - Store audience on oauth_tokens; validate on every MCP request - Refresh tokens inherit audience; add resource_parameter_supported to AS metadata - DB migration: ADD COLUMN audience TEXT to oauth_tokens - Gate collab MCP tools/resources by chat/notes/polls sub-features individually - Invalidate MCP sessions when collab sub-features are toggled in admin - Update test mocks and MCP.md |
||
|
|
4974013995 |
fix journey bugs reported by roel-de-vries (#722-#736)
Mobile UI: - #722 timeline carousel no longer cut off by BottomNav (uses --bottom-nav-h var) - #723 scroll-snap-type relaxed to proximity so small swipes no longer skip entries - #724 defensive padding-bottom fix in JourneySettingsDialog for iOS PWA - #725 add back/settings buttons + journey title subtitle to mobile activity view - #726 active entry re-centers after scroll settle; tap inactive card activates it (does not jump straight into editor) Entry editor flow: - #727 photo uploads queue locally until Save for existing entries too (previously fired upload immediately; Cancel silently kept the new photo) - #728 Cancel/Close with unsaved changes now requires confirm (window.confirm) - #729 linking a Gallery photo into an entry now copies the row (old MOVE behavior meant Remove-from-Entry also nuked the Gallery original) - #731 addPhoto / addProviderPhoto / linkPhotoToEntry promote skeleton entries to concrete 'entry' type when content is added Permissions: - #732 updateJourney switched from canEdit to isOwner — editors can still edit entries and photos, just not the journey shell (title, cover, status) - #733 Contributors list gains a per-row remove (X) control with confirm - #734 my_role is computed server-side and returned with the journey; UI gates Settings/Add/Edit/Delete controls based on role - #736 createOrUpdateJourneyShareLink + deleteJourneyShareLink now require isOwner (previously NO permission check at all — anyone authenticated could publish or unpublish a journey) Immich upload (#730): - migration 111: add users.immich_auto_upload (default 0) - migration 112: seed provider_field for the toggle (idempotent, FK-safe) - journey photo upload only mirrors to Immich when the user has opted in - Settings UI gets a "Mirror journey photos to Immich on upload" checkbox Test updates: - JOURNEY-SVC-019 inverted to assert editor cannot update journey settings - JOURNEY-SHARE-007 now passes userId (owner) to deleteJourneyShareLink - FE-PAGE-JOURNEYDETAIL-148 inverted to assert photos stay pending until Save - client/tests still green (2676/2676) Also fixed en route: gallery entry title is now the literal 'Gallery' on the wire (used to send the translated label, which broke server-side title === 'Gallery' checks in non-English locales); confirm interpolation uses {username} single braces matching the existing i18n runtime; Settings footer uses icon-only delete/archive buttons on mobile so the row doesn't wrap. |
||
|
|
777b68f87b |
fix tests for sidebar/settings refactor + weather archive fallback
- DayPlanSidebar: add aria-label to undo button, replace title with aria-label
so tests can still locate buttons by accessible name after tooltip refactor
- tests: switch getByTitle("Add Note") to getByLabelText
- tests: find undo button via aria-label (new expand/collapse button also uses
width:30, breaking the old style-based lookup)
- PlacesSidebar tests: loosen "All" button regex to account for count badge
- DisplaySettingsTab tests: use getByRole for Auto button (two "Auto" spans
coexist for mobile/desktop); handle multiple English matches in lang test
- weatherService tests: past-date case now expects an archive fetch instead
of an immediate no_forecast error
|
||
|
|
6a718fccea
|
feat(import): selective GPX/KML element import and performance improvements
Add type-selector UI in the file import modal letting users choose which GPX elements (waypoints, routes, tracks) or KML/KMZ elements (points, paths) to import. KML LineString placemarks are now imported as path places with route_geometry. Performance improvements: - Extract MemoPlaceRow with React.memo and contentVisibility:auto to cut unnecessary re-renders in PlacesSidebar - Add weatherQueue to cap concurrent weather fetches at 3 - Replace sequential per-place deletes with a single bulkDelete API call (new DELETE /places/bulk endpoint + deletePlacesMany service) - Memoize atlas/photo/weather service calls to avoid redundant requests - Add multi-select mode to PlacesSidebar for bulk operations Add large GPX/KML/KMZ fixtures for integration/perf testing and two profiler analysis scripts under scripts/. |
||
|
|
9c2decb095
|
fix(maps): reduce Google Places API quota usage with persistent caching
P0 — stop the bleeding:
- Honor place.image_url in MapView and TripPlannerPage to skip redundant fetchPhoto calls
- Trim Place Details field mask (drop reviews/editorialSummary from default; new getPlaceDetailsExpanded for inspector)
- Admin toggle places_photos_enabled (default ON) to kill Google photo fetches under quota pressure; Wikimedia unaffected
- Return { photoUrl: null } instead of 204 so client handles disabled state cleanly
P1 — structural fix:
- New placePhotoCache service: persistent disk cache at uploads/photos/google/<sha1>.jpg, atomic writes, stampede dedup via in-flight Map
- Migrations 105-107: google_place_photo_meta table, place_details_cache table, backfill signed Google URLs to stable proxy URLs
- getPlacePhoto rewrites to fetch image bytes directly, store on disk, return /api/maps/place-photo/:id/bytes proxy URL
- Stable proxy URLs written to places.image_url — survive container restarts, no expiry
- New GET /api/maps/place-photo/:placeId/bytes route serving cached files with long-lived Cache-Control
- Place Details DB row cache with 7-day TTL; ?refresh=1 escape hatch
- photoService fast-path: proxy URLs bypass the mapsApi round-trip and go straight to urlToBase64
Bug fixes:
- MapView now requests base64 thumbs for places with proxy image_url (markers were showing color fallback)
- createPlaceIcon accepts /api/maps/place-photo/ URLs as interim fallback while thumb generates
- setSelectedAssignmentId ReferenceError in mobile day-detail handler (use selectAssignment)
- Remove redundant decodeURIComponent on already-decoded Express route param
- Use SHA1 hash for disk filenames to prevent coords:lat:lng pseudo-ID collisions
- Add checkSsrf guard to Wikimedia byte fetch
- Tighten migration 107 LIKE filter to avoid rewriting manually-pasted Google image URLs
- Validate enabled is boolean on PUT /admin/places-photos
- Drop aggressive iconCache.clear() on every thumb arrival
Observability:
- googleFetch() wrapper counts and debug-logs every outbound Google API call with running total
|
||
|
|
3b94727c07
|
fix(journey): fix issue #704 — active logic, archive, places rename, search, trip reminders
- Derive journey lifecycle from linked trip dates (live/upcoming/completed/draft) instead of relying solely on status field; status=archived always wins - Add Archive/Restore Journey action in journey settings dialog - Rename cities → places end-to-end (SQL alias, TS types, stats field, all locales) - Wire up search icon: toggles inline input, filters by title+subtitle client-side - Fix channelConfigured check: trip reminders enabled by default since inapp is always available; remove channel check, controlled solely by admin setting - Expose notify_trip_reminder toggle in Admin → Settings → Notifications - Add trip_date_min/trip_date_max to listJourneys SQL for client-side lifecycle - Add archived status to Journey type (server + client) - Update all 15 locale files with new keys (search, archive, places, trip reminders) |
||
|
|
5046e1a2e0
|
fix(synology): wire shared-album passphrase through journey-entry add flow
Thread selectedAlbumPassphrase from ProviderPicker through onAdd → journeyApi.addProviderPhotos → POST /entries/:entryId/provider-photos → addProviderPhoto service → getOrCreateTrekPhoto so shared-album photos have their passphrase encrypted and persisted on trek_photos at add-time, enabling streamPhoto to forward it to Synology correctly (#689). |
||
|
|
da70388f4b
|
fix(journey): resolve Immich photos on public share by matching trek_photos.id
validateShareTokenForPhoto was querying journey_photos by jp.id but the public page sends p.photo_id (trek_photos.id) in the URL. In a fresh database the IDs coincidentally match, masking the bug. In production instances with many Immich-synced photos the trek_photos autoincrement is far ahead of journey_photos, causing a 404 for every Immich photo on the public share page. Fix: change the lookup to jp.photo_id = ? so validation is keyed on trek_photos.id, which is what the client sends and what streamPhoto needs. Updated the test helper to return trekId and added a regression test that pre-populates trek_photos to produce diverging IDs. Closes #675. |
||
|
|
bfe84b3016
|
feat(notifications): add ntfy as a first-class notification channel
Adds ntfy.sh (and self-hosted instances) as a new push notification channel with full parity to the existing webhook channel. - Backend: NtfyConfig type, getUserNtfyConfig, getAdminNtfyConfig, resolveNtfyUrl, sendNtfy (header-based API with Title/Priority/Tags/ Click headers), testNtfy, NTFY_EVENT_META (priority + emoji tags per event), SSRF guard via existing checkSsrf + createPinnedDispatcher - notificationPreferencesService: ntfy added to NotifChannel union, IMPLEMENTED_COMBOS, getActiveChannels parser, getAvailableChannels, ADMIN_GLOBAL_CHANNELS, and AvailableChannels interface - notificationService: per-user ntfy dispatch after webhook block; admin-scoped ntfy via getAdminGlobalPref for version_available events - Routes: POST /api/notifications/test-ntfy with saved-token fallback - authService: admin_ntfy_server/topic/token in ADMIN_SETTINGS_KEYS, masked + encrypted on read/write - settingsService: ntfy_token added to ENCRYPTED_SETTING_KEYS - Frontend: ntfy topic/server/token inputs + Save/Test/Clear buttons in NotificationsTab; admin Ntfy panel in AdminPage; testNtfy API method - i18n: full English strings; English placeholders in 14 other locales - Tests: resolveNtfyUrl, sendNtfy, dispatch integration, UI tests, MSW handler for test-ntfy endpoint |
||
|
|
875c91e5ff
|
feat(places): unified file import modal with drag-and-drop and deduplication
- Replace separate GPX and KML/KMZ import buttons with a single "Import file" modal accepting all three formats, with a drag-and-drop drop zone - Support dragging files directly onto the Places sidebar panel; overlay appears on hover and pre-loads the file into the modal on drop - Fix [object Object] description bug in KML imports caused by fast-xml-parser returning mixed-content nodes as objects; add stopNodes config and object guard in asTrimmedString - Fix CDATA sections leaking into descriptions (e.g. "text.]]>") by unwrapping CDATA markers before tag stripping - Add import deduplication across all import paths (GPX, KML/KMZ, Google list, Naver list): reimporting skips places already in the trip by name (case-insensitive) or by coordinates (within ~11 m tolerance), with intra-batch dedup so duplicate placemarks within the same file are also collapsed - Fix KML route returning 400 "No valid Placemarks found" when all placemarks were valid but deduplicated; 400 now only fires when the file contains zero placemarks - Show a warning toast "All places were already in the trip" instead of a misleading success toast when a reimport produces zero new places (GPX, KML/KMZ, Google list, Naver list) - Add 8 new i18n keys across all 14 locales; remove 11 keys made unused by the modal consolidation |
||
|
|
801ffbfb7b
|
fix(kml-import): address PR #488 review issues
- Strip BOM (U+FEFF) from 14 translation files injected by editor - Guard KMZ unpack against zip-bomb: check entry.uncompressedSize against 50 MB cap (KMZ_DECOMPRESSED_SIZE_LIMIT) before calling .buffer(); limit is an exported constant so tests can override it - Fix non-BMP HTML entity decoding: replace String.fromCharCode with String.fromCodePoint + 0x10FFFF bounds check so emoji like 😀 round-trip correctly - Switch KML namespace stripping from regex to fast-xml-parser's removeNSPrefix option; XMLValidator accepts namespaced XML natively, making the pre-strip step unnecessary - Remove dead skippedCount overwrite after transaction; per-loop increment already tracks it alongside per-item error messages - Type multer req.file as Express.Multer.File on both /import/gpx and /import/map routes instead of (req as any).file - Add unit tests: emoji entity decoding (decimal + hex), KMZ zip-bomb rejection, KMZ-with-no-KML rejection |
||
|
|
a1a7795945
|
Merge PR #488: KMZ/KML place import
Resolves conflicts with Naver list import (PR #662) — kept both unified list-import dialog and new KMZ/KML dialog. Dropped duplicate react-dom import and unused CustomSelect import from PlacesSidebar. |
||
|
|
607498cabe
|
fix(search-autocomplete): address PR #542 review issues
- Fix race condition: AbortController cancels in-flight autocomplete requests on each keystroke; stale responses no longer overwrite fresh ones - Remove acTrigger state hack; onFocus calls fetchSuggestions directly - Cap autocomplete input at 200 chars server-side (400 on violation) - Filter Nominatim suggestions with empty osm_id segments - Revert getPlaceDetails OSM branch from unconditional parallel fetch to conditional serial: Nominatim called only when Overpass lacks coords/address - Wire places.loadingDetails i18n key to Loader2 spinner via aria-label/role - Add tests: MAPS-017, MAPS-040c, MAPS-093, FE-MAPS-004 |
||
|
|
35321076cf
|
Merge branch 'review/pr-542' into feat/search-autocomplete | ||
|
|
a438652a50
|
fix(trips): preserve day content when trip date range changes
Rewrites generateDays to remap days positionally by day_number instead of matching by date identity. Previously any date range shift with no overlap would cascade-delete all day_assignments, day_notes, and day_accommodations. New behaviour: - Shift/partial overlap: existing days remapped to new dates in order - Shrink: overflow days become dateless (date=NULL) instead of deleted, preserving all child data for manual reassignment - Grow: existing days kept, new empty days appended - Clear dates: all days nullified, content intact Also fixes a UNIQUE(trip_id, day_number) collision that would occur when spare dateless days remained after growing into a partially-dateless trip (maxAssigned base was wrong). Closes #646 |
||
|
|
b3571f391a |
Fix skeleton entry deletion and add hide suggestions toggle (#619)
- Revert filled skeleton entries back to skeleton on delete instead of permanently removing them - Add per-user hide_skeletons preference on journey_contributors (migration 99) - Add PATCH /journeys/:id/preferences endpoint for toggling skeleton visibility - Add Eye/EyeOff toggle button with custom tooltip in journey detail header - Filter skeleton entries from timeline when hidden - Add i18n keys for all 14 languages |
||
|
|
6a23118342
|
fix(notifications): fix SMTP error surfacing, webhook button label, backup timestamp
- testSmtp now surfaces real nodemailer error instead of generic 'SMTP not configured' on send failure - admin webhook test button uses correct i18n key (was showing 'Test-E-Mail senden' in all languages) - backup created_at uses stat.mtime instead of unreliable stat.birthtime on Linux |
||
|
|
714e2ad703
|
fix(tests): update test helpers and assertions for migration-98 photo schema
trek_photos is now the central registry; trip_photos and journey_photos reference it via photo_id FK. Updated all affected test helpers and direct-SQL assertions to join trek_photos instead of querying stale columns (asset_id, provider, owner_id) on the leaf tables. Also fix ATLAS-UNIT-019: getVisitedRegions now fires background geocoding and returns immediately, so the test must call it twice — once to trigger the fill, once after advancing fake timers to read cached results. |
||
|
|
479ab49d67
|
Merge branch 'dev' into search-auto-complete | ||
|
|
7fca16d866
|
Switch location bias from a point to a bounding box for improved autocomplete accuracy and validation. | ||
|
|
e629548a42
|
fix(tests): align tests to actual working code | ||
|
|
ad27c5f6be
|
fix: restore broken tests after prerelease workflow refactor
- Export __clearVersionCacheForTests() from adminService; call in
versionNotification beforeEach to reset module-scoped cache between
tests (VNOTIF-002..006 failed because VNOTIF-001 cached
update_available:false, short-circuiting all subsequent test fetches)
- Seed appVersion:'2.9.10' in Navbar test authStore; appVersion moved
from local useEffect state to authStore in last commit so the test
render no longer fetches it independently (FE-COMP-NAVBAR-016)
- Add data-testid="weekend-days" to VacaySettings weekend-days
container; use within() in tests to scope button count to that
section, fixing false positives from the week-start buttons which
share the same inline styles (FE-COMP-VACAYSETTINGS-003/004)
- Pass isPrerelease={true} in GitHubPanel FE-ADMIN-GH-007; component
filters out prerelease releases when isPrerelease=false so the badge
was never rendered (pre-existing, unrelated to last commit)
|
||
|
|
de157cb87b |
test: comprehensive Journey test suite — 89.5% new code coverage
Server (172 tests): - journeyService unit tests (87 tests): CRUD, access control, sync, photos, contributors - journeyShareService unit tests (20 tests): share links, token validation, public access - journey integration tests (45 tests): all API routes, auth, permissions, edge cases - Test helpers: journey factories, RESET_TABLES updated Client (340+ tests): - journeyStore tests (15 tests): all store actions and state management - JourneyPage tests (20 tests): frontpage, create flow, suggestions, navigation - JourneyDetailPage tests (94 tests): all sub-components, entry editor, settings, share links, contributors, gallery, map, trip linking - JourneyPublicPage tests (18 tests): public view, tabs, restricted access - JourneyBookPDF tests (6 tests): PDF generation - BottomNav tests (9 tests): profile sheet, navigation - PhotoLightbox tests (8 tests): keyboard nav, counter - JourneyMap tests (12 tests): markers, polylines, zoom - Component tests: moodConfig, stripMarkdown, MarkdownToolbar, JournalBody, MobileTopHeader - DashboardPage tests (32 tests): spotlight card, quick actions, widget settings SonarQube: exclude unused MemoriesPanel from coverage (dead code, moved to Journey) |
||
|
|
47d9cce936
|
fix(tests): update tests for granular auth toggles
- Add new fields to AppConfig type and buildAppConfig factory - Update FE-PAGE-ADMIN-018: heading changed to "Authentication Methods" - Update FE-PAGE-ADMIN-053: oidc_only toggle removed from OIDC panel - Update FE-PAGE-LOGIN-007/017: mocks now include password_login/oidc_login - Update ADMIN-SVC-049: updateOidcSettings no longer writes oidc_only |
||
|
|
bfd2553d1e
|
feat(auth): split OIDC_ONLY into granular auth toggles
Replaces the coarse oidc_only + allow_registration settings with four independent toggles: password_login, password_registration, oidc_login, oidc_registration. Each can be enabled/disabled individually in Admin > Settings without affecting the others. - Add resolveAuthToggles() in authService.ts as the central resolver; falls back to legacy oidc_only/allow_registration keys when new keys are absent (backward compat) - OIDC_ONLY env var still works and overrides DB toggles for password_*, with a visual lock in the admin UI when active - Server enforces lockout prevention: cannot disable all login methods - oidc_login gate added to OIDC /login and /callback routes - Remove oidc_only toggle from OIDC settings panel; replaced by the granular toggles in the Settings tab - Add 6 new resolveAuthToggles() unit tests; fix AUTH-DB-033 error message assertion - Update OIDC_ONLY descriptions in README, docker-compose, Helm values, Unraid template, and .env.example to clarify override semantics Closes #492 |
||
|
|
eff3fcfe10
|
test: update expected event_types count after adding synology_session_cleared | ||
|
|
7a22d742ab
|
test: add comprehensive coverage for OAuth scopes, MCP, and core services
Adds new and expanded test suites across client and server to cover the OAuth 2.1 scope system, MCP session manager, collab service, unified memories helpers, OIDC service, budget slice, and OAuth authorize page. Also extends SonarQube coverage exclusions to include bootstrapping files (migrations, scheduler, main.tsx, types.ts) that are not meaningfully testable. |
||
|
|
7c0a0d5f39
|
security(oauth): harden OAuth 2.1/MCP implementation (Critical + High + Medium findings)
Address 14 security findings from internal review of the OAuth 2.1 + MCP layer: Critical: - C1: Scope-gate all MCP resources (trips, budget, packing, collab, atlas, vacay, etc.) - C2: Wire token/session revocation into active MCP session lifecycle per (user, client_id) - C3: Refresh-token replay detection via parent_token_id chain + cascade revoke on replay High: - H1: Validate PKCE code_challenge (43-char base64url) and code_verifier (43–128 chars) format - H2: Rate-limit /oauth/token (30/min), /authorize/validate (30/min), /oauth/revoke (10/min) - H3: Strip client metadata from unauthenticated /authorize/validate responses (oracle prevention) - H4: Constant-time secret comparison via crypto.timingSafeEqual (prevents timing attacks) - H5: Collapse all invalid_grant cases to a single generic message; log specifics server-side Medium: - M1: Set Cache-Control: no-store + Pragma: no-cache on token endpoint responses - M2: Return 404 (not 200/403) on discovery + revoke endpoints when MCP addon is disabled - M4: Audit-log all OAuth lifecycle events (create, consent, issue, refresh, revoke, replay) - M5: Union consent scopes on re-authorization instead of replacing existing grants - M7: Require httpOnly cookie auth (not Bearer JWT) on all state-mutating OAuth endpoints - M8: Strict Bearer scheme check in MCP token verification Refactoring: - Extract MCP session management (sessions Map, revokeUserSessions, revokeUserSessionsForClient) into mcp/sessionManager.ts to break the circular dependency between oauthService and mcp/index - Extract verifyJwtAndLoadUser helper in auth middleware, shared by authenticate and new requireCookieAuth middleware Tests: - Fix all existing integration tests broken by the security hardening (OAUTH-019 to OAUTH-032) - Add 13 new integration tests covering M1, M2, H1, H3, H5, M5, M7, C3 - Add 14 new unit tests covering C2, C3, H1, H3, M5 behaviors in oauthService |
||
|
|
583ac6d4d9
|
Add tests for mapsApi.autocomplete and autocompletePlaces service interactions | ||
|
|
f2908fdd65
|
test(mcp): add tests for OAuth 2.1, addon gating, and budget reorder
Covers OAuth integration flow, scope enforcement, addon-gated tool access, oauthService unit tests, and budget reorder/permission/reservation-sync scenarios. |
||
|
|
0e3e6df1f0 |
Merge remote-tracking branch 'origin/dev' into feat/places-kmz-kml-import
# Conflicts: # server/tests/integration/places.test.ts |
||
|
|
8c8bd5bc37 | refactor(server): consolidate KML import utilities | ||
|
|
781861f799
|
test: relax ReDoS timing thresholds for CI compatibility
MAPS-024 and MAPS-026 were asserting < 100ms on adversarial regex input, which passed locally but flaked on CI runners (~150-170ms). These are not cases of catastrophic backtracking — true ReDoS would take seconds, not ~150ms. Raise the threshold to 500ms to remain meaningful while being reliable across environments. |
||
|
|
b4922322ae
|
test: expand test suite to 87.3% backend coverage
Add new integration test files covering previously untested routes: - categories.test.ts — GET /api/categories - oidc.test.ts — full OIDC login flow (callback, state, errors) - settings.test.ts — GET/PUT /api/settings, bulk save - tags.test.ts — CRUD for trip tags - todo.test.ts — todo items CRUD and reorder Add new unit test files covering service-layer logic: - adminService.test.ts — user/invite management, packing templates, OIDC settings - atlasService.test.ts — atlas search and place enrichment - authServiceDb.test.ts — DB-backed auth helpers (login, register, MFA) - backupService.test.ts — export/import/restore logic - categoryService.test.ts — category CRUD - dayService.test.ts — day management and accommodation helpers - mapsService.test.ts — route/directions helpers - oidcService.test.ts — OIDC state, auth code, role resolution, user upsert - packingService.test.ts — packing item/bag/template operations - placeService.test.ts — place CRUD and tag attachment - settingsService.test.ts — settings get/set/bulk - tagService.test.ts — tag CRUD - todoService.test.ts — todo CRUD and reorder - tripService.test.ts — trip CRUD, member management, archiving - vacayService.test.ts — vacay integration helpers - tripAccess.test.ts (middleware) — requireTripAccess middleware Expand existing integration and unit test files with additional cases across admin, atlas, auth, backup, collab, days, files, maps, memories (Immich/Synology), notifications, places, reservations, share, vacay, weather, auth middleware, ephemeral tokens, notification preferences, permissions, SSRF guard, and WebSocket connection tests. Update test helpers (factories.ts, test-db.ts) with new factory functions and seed data required by the expanded suite. Fix minor issues in server/src/routes/reservations.ts and server/src/services/atlasService.ts surfaced by new test coverage. Update sonar-project.properties to reflect new coverage thresholds. |
||
|
|
5bcadb3cc6
|
test: apply suite review improvements (01–11)
- Fix SEC-005: rewrite path traversal test to upload a real file, inject traversal filename into DB, and assert the download does not succeed - Fix SEC-007: rename misleading test description to reflect it tests rejection of an invalid token, not acceptance of a valid one - Delete health.test.ts: all 3 tests were exact duplicates of auth.test.ts and misc.test.ts - Remove duplicate describe blocks from misc.test.ts: Categories endpoint (duplicate of categories.test.ts) and App config (duplicate of auth.test.ts) - Remove TRIP-016 from trips.test.ts: weaker duplicate of TRIP-007 (no body assertion) - Remove API Keys describe block from profile.test.ts: canonical copy lives in security.test.ts where it belongs - Remove avatarUrl describe block from budgetService.test.ts: identical tests already exist in authService.test.ts; drop now-unused import - Add DB verification to ASSIGN-007 and PACK-006 reorder tests: query day_assignments / packing_items after PUT reorder to confirm order changed - Strengthen BUDGET-007/008/009: add member/payer setup and assert concrete values (total_paid, per-user balance, flow direction and amount) - Remove 6 pointless Map-semantics tests from inAppNotificationActions.test.ts; keep only the two built-in registration checks - Remove 5 passthrough tests from queryHelpers.test.ts; keep the 4 tests that cover actual flat-to-nested transformation logic |
||
|
|
5271463064 | feat(server): add KML and KMZ place import pipeline | ||
|
|
5cc81ae4b0
|
refactor(server): replace node-fetch with native fetch + undici, fix photo integrations
Replace node-fetch v2 with Node 22's built-in fetch API across the entire server.
Add undici as an explicit dependency to provide the dispatcher API needed for
DNS pinning (SSRF rebinding prevention) in ssrfGuard.ts. All seven service files
that used a plain `import fetch from 'node-fetch'` are updated to use the global.
The ssrfGuard safeFetch/createPinnedAgent is rewritten as createPinnedDispatcher
using an undici Agent, with correct handling of the `all: true` lookup callback
required by Node 18+. The collabService dynamic require() and notifications agent
option are updated to use the dispatcher pattern. Test mocks are migrated from
vi.mock('node-fetch') to vi.stubGlobal('fetch'), and streaming test fixtures are
updated to use Web ReadableStream instead of Node Readable.
Fix several bugs in the Synology and Immich photo integrations:
- pipeAsset: guard against setting headers after stream has already started
- _getSynologySession: clear stale SID and re-login when decrypt_api_key returns null
instead of propagating success(null) downstream
- _requestSynologyApi: return retrySession error (not stale session) on retry failure;
also retry on error codes 106 (timeout) and 107 (duplicate login), not only 119
- searchSynologyPhotos: fix incorrect total field type (Synology list_item returns no
total); hasMore correctly uses allItems.length === limit
- _splitPackedSynologyId: validate cache_key format before use; callers return 400
- getImmichCredentials / _getSynologyCredentials: treat null from decrypt_api_key as
a missing-credentials condition rather than casting null to string
- Synology size param: enforce allowlist ['sm', 'm', 'xl'] per API documentation
|
||
|
|
7b37d337c1
|
fix(security): address notification system security audit findings
- SSRF: guard sendWebhook() with checkSsrf() + createPinnedAgent() to block
requests to loopback, link-local, private network, and cloud metadata endpoints
- XSS: escape subject, body, and ctaHref in buildEmailHtml() via escapeHtml()
to prevent HTML injection through user-controlled params (actor, preview, etc.)
- Encrypt webhook URLs at rest: apply maybe_encrypt_api_key on save
(settingsService for user URLs, authService for admin URL) and decrypt_api_key
on read in getUserWebhookUrl() / getAdminWebhookUrl()
- Log failed channel dispatches: inspect Promise.allSettled() results and log
rejections via logError instead of silently dropping them
- Log admin webhook failures: replace fire-and-forget .catch(() => {}) with
.catch(err => logError(...)) and await the call
- Migration 69: guard against missing notification_preferences table on fresh installs
- Migration 70: drop the now-unused notification_preferences table
- Refactor: extract applyUserChannelPrefs() helper to deduplicate
setPreferences / setAdminPreferences logic
- Tests: add SEC-016 (XSS, 5 cases) and SEC-017 (SSRF, 6 cases) test suites;
mock ssrfGuard in notificationService tests
|
||
|
|
fc29c5f7d0
|
feat(notifications): add unified multi-channel notification system
Introduces a fully featured notification system with three delivery channels (in-app, email, webhook), normalized per-user/per-event/ per-channel preferences, admin-scoped notifications, scheduled trip reminders and version update alerts. - New notificationService.send() as the single orchestration entry point - In-app notifications with simple/boolean/navigate types and WebSocket push - Per-user preference matrix with normalized notification_channel_preferences table - Admin notification preferences stored globally in app_settings - Migration 69 normalizes legacy notification_preferences table - Scheduler hooks for daily trip reminders and version checks - DevNotificationsPanel for testing in dev mode - All new tests passing, covering dispatch, preferences, migration, boolean responses, resilience, and full API integration (NSVC, NPREF, INOTIF, MIGR, VNOTIF, NROUTE series) - Previous tests passing |