mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 21:31:46 +00:00
264cf7d3843efe704177f4bf021d0bff0d4ff9e9
629 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
264cf7d384 |
fix(vacay): keep the mode toolbar above the mobile bottom nav
The floating Vacation/Company toolbar was pinned at bottom-3 with z-30, so on mobile it landed in the same band as the fixed bottom nav (z-60) and got hidden behind it - and could scroll out of reach entirely. Pin it above the nav with the shared --bottom-nav-h variable (0px on desktop, so nothing changes there) and reserve matching space below the calendar grid so it never gets swallowed. |
||
|
|
2d79254c33 | feat(pdf): add legs to pdf export | ||
|
|
e6fcbc7789 |
fix(shared-view): render each leg of multi-leg flights correctly
The read-only shared view showed the overall trip start/end airports and the first leg's flight number on every leg of a multi-leg flight. The Day Plan already expands legs (each carries __leg), but the renderer ignored it and read flat top-level metadata; the Bookings tab had the same bug. - Day Plan: use __leg for per-leg airline/flight number/route, plus dep-arr time - Bookings tab: list each leg via getFlightLegs() - unique React keys for multi-leg rows Closes #1219 |
||
|
|
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 |
||
|
|
86ee8044da |
v3.0.22 Bug Fixes & Improvements (#1041)
Bundles the v3.0.22 bug fixes and improvements. See the release notes for the full list. |
||
|
|
117942f45e |
v3.0.21 Bug Fixes (#998)
* fix(journey): remove photo upload count limit and surface upload errors (#997) Removes the arbitrary 10-file cap on journey entry photo uploads and 20-file cap on gallery uploads. MulterErrors now return proper 4xx responses instead of 500, and the client surfaces the server error message via toast rather than silently trapping the user in the post editor overlay. * fix(planner): remove correct assignment when place assigned to same day multiple times When a place was assigned to the same day more than once, the "Remove from day" button in PlaceInspector always deleted the first assignment (Array.find on place.id) instead of the currently selected one. Now prefers selectedAssignmentId when available. Fixes #1005 * fix(map): enable 3D terrain for Mapbox outdoors style in trip planner wantsTerrain() only matched satellite styles, so the outdoors-v12 style was flat in the planner despite showing correct 3D terrain in the settings preview. Added outdoors-v12 to the allowlist; marker drift is already handled by syncMarkerAltitudes(). Fixes #1002 * fix(maps): send Referer header on Google API calls when APP_URL is set Supports HTTP referrer restrictions on GCP API keys. Documents the restriction types and photo troubleshooting steps in the wiki. |
||
|
|
da3cba2de3 |
v3.0.19 Bug Fixes (#992)
* fix(mcp): replace relative oauth constent redirect by absolute redirect derived from APP_URL (#987) * feat(journey): convert HEIC/HEIF uploads to JPEG for cross-platform compatibility HEIC is an Apple-only format not recognised as an image by many browsers and platforms. heic-to (lazy-loaded) now converts HEIC/HEIF files to JPEG before upload in both the gallery and entry editor photo pickers. Embedded metadata (EXIF, GPS) may be lost during conversion — documented in the Journey Journal wiki page. * fix(journey): skip heic-to import for non-HEIC files to avoid test env failures * fix(notifications): prevent double-escaping HTML in password reset emails buildPasswordResetHtml passed a pre-built HTML block to buildEmailHtml, which then escaped it again — rendering raw tags as plain text in the email. |
||
|
|
e7b419d397 |
security: login timing enumeration fix + dep CVE patches (v3.0.18) (#984)
* fix(security): equalise login response timing to prevent user enumeration (CWE-208)
Always run bcrypt.compareSync regardless of whether the email exists, using a
module-scope DUMMY_PASSWORD_HASH for unknown/OIDC-only accounts. Also wraps the
login handler in a 350ms minimum-latency pad (matching /forgot-password) as
defence-in-depth against CPU jitter and future code-path drift.
Fixes: CWE-203, CWE-208 — Observable Timing Discrepancy (CVSS 5.3 Medium)
* chore(deps): patch hono/picomatch/ip-address/brace-expansion CVEs, bump to node:24-alpine
Extends server/package.json overrides to pin hono >=4.12.16, picomatch >=4.0.4,
brace-expansion >=2.0.3, ip-address >=10.1.1. Adds matching overrides to client/.
Lockfiles regenerated to resolve: hono 4.12.18, ip-address 10.2.0, picomatch 4.0.4.
Also bumps base image node:22-alpine -> node:24-alpine (reduces base image CVEs)
and adds .github/workflows/security.yml to gate PRs on critical/high CVEs via
Docker Scout.
Addresses: CVE-2026-44456, CVE-2026-44455 (hono), CVE-2026-42338 (ip-address),
CVE-2026-33671, CVE-2026-33672 (picomatch), CVE-2026-33750 (brace-expansion)
* chore: update emails in security.md
* ci(security): use docker/login-action for Scout auth instead of env vars
* chore: regenerate lock files
* chore: correct secret names
* chore: pr perms write
* fix(docker): remove package-lock.json from production image after npm ci
Docker Scout reads package-lock.json as an SBOM source and reports all
lockfile entries including devDependencies (e.g. picomatch via vitest/vite)
even when they are not physically installed. The lockfile has no runtime
purpose after npm ci completes, so delete it to ensure Scout only reports
packages actually present in node_modules.
* fix(docker): remove npm CLI from production image to eliminate bundled CVEs
picomatch@4.0.3, brace-expansion@5.0.4, and ip-address@10.1.0 were all
coming from /usr/local/lib/node_modules/npm — npm's own bundled packages
shipped with node:24-alpine. The production container only needs the node
binary to run the server; npm is unused at runtime.
Removing npm + npx after npm ci drops the package count from 500 to 365
and eliminates all npm-ecosystem CVEs (0H 0M remaining from npm packages).
Only busybox CVE-2025-60876 remains, which has no fix in Alpine 3.23.
* fix(deps): remove client overrides and brace-expansion server override; audit fix
brace-expansion ^2.0.3 in the client forced all installations to v2, breaking
minimatch in CI (test:coverage path via @vitest/coverage-v8 -> test-exclude)
which expects the named-export API of brace-expansion v5. The CVE it targeted
(>=4.0.0,<5.0.5) was only in npm's own bundled packages, already eliminated
by removing npm from the Docker image.
Also removes picomatch and ip-address client overrides for the same reason:
all three CVEs sourced from /usr/local/lib/node_modules/npm/, not app deps.
Drops brace-expansion from server overrides (server uses v2.1.0, outside the
affected range >=4.0.0).
* fix(#981): align public share itinerary order with daily planner (#985)
The public share page rendered daily items in a different order than the
authenticated planner because it used a simplified, divergent merge
algorithm. Five specific bugs:
1. shareService never loaded reservation_day_positions, so per-day
transport positions were lost on the share page (fell back to
day_plan_position ?? 999, pushing transports to the bottom).
2. Multi-day transports (overnight trains/flights) only appeared on their
start day due to date-string filtering instead of day_id span logic.
3. Assignment-linked transports appeared twice (once as place, once as
transport card) because the assignment_id exclusion was missing.
4. Time-based transport insertion was absent; missing positions used 999
instead of a computed fractional position from the place timeline.
5. created_at tiebreaker was missing for assignments and notes with equal
order_index/sort_order, making order non-deterministic on the share page.
Fix: extract the authoritative merge logic (parseTimeToMinutes,
getSpanPhase, getDisplayTimeForDay, getTransportForDay, getMergedItems)
from DayPlanSidebar into client/src/utils/dayMerge.ts and use it in both
the planner and SharedTripPage. Enrich the shareService payload with
day_positions from reservation_day_positions and add created_at tiebreakers
to the assignment and day_notes ORDER BY clauses.
* fix(#983): shift owner vacay entries when update_trip moves trip window
updateTrip() now calls shiftOwnerEntriesForTripWindow() which looks up
the owner's own vacay plan (not the active plan) and shifts all entries
in the old date window by the same offset as the trip start date.
|
||
|
|
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 |
||
|
|
51ab30f436 |
Bug fixes - April 30th 2026 (#936)
* fix: hotel day-range clamping in ReservationModal + stale assignment_id on accommodation clear (issues #929, #934)
* ReservationModal hotel start/end pickers now use findIndex-based
positional clamping instead of raw ID arithmetic, matching the fix
applied to DayDetailPanel in
|
||
|
|
78d6f2ba77 |
Bug fixes - April 28th 2026 (#915)
* fix: replace raw day-ID range checks with position-based helper (issue #889 follow-up)
Commit
|
||
|
|
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 |
||
|
|
35ed712d46 |
Fix demo banner overlapping bottom tab bar on mobile
The demo welcome modal extended below the mobile bottom tab bar, hiding the dismiss button so visitors couldn't close it. - Use dvh so mobile URL bar is accounted for correctly - Reserve ~80px of bottom padding for the tab bar - Make the footer sticky so the dismiss button stays visible while scrolling through the modal content - Bump z-index to ensure the overlay sits above the tab bar |
||
|
|
2a37eeccb3 |
fix: hot fixes 23-04-2026 (#856)
* fix(packing): resolve avatar URL path in bag and category assignees (#854) packingService was returning raw avatar filenames from the DB instead of the full /uploads/avatars/<filename> path, causing broken profile images for users with uploaded avatars. * fix(budget): use Map.get() to fix category rename no-op (#855) * fix(security): relax Referrer-Policy and document HSTS_INCLUDE_SUBDOMAINS (#862) (#863) - Change Helmet default from no-referrer to strict-origin-when-cross-origin so browsers send the origin on cross-origin requests, allowing Google Maps API key restrictions by HTTP referrer to work correctly - Document HSTS_INCLUDE_SUBDOMAINS in all deployment artifacts: .env.example, docker-compose.yml, README.md, unraid-template.xml, charts/values.yaml, charts/configmap.yaml, wiki/Environment-Variables.md * fix(planner): prefetch budget items on trip page mount (#864) Loads budgetItems alongside reservations when TripPlannerPage mounts so the Budget category dropdown in ReservationModal and TransportModal shows pre-existing categories on first open, regardless of whether the Budget tab has been visited. Closes #861 * fix(reservations): prevent Invalid Date when end time is set without end date (#866) When reservation_end_time held a bare time string ("HH:MM"), fmtDate() produced Invalid Date on the reservation card. - Modal: when end date is blank but end time is filled, construct a same-day ISO datetime using the start date (prevents time-only strings from ever being persisted) - Panel: derive endDatePart via regex so date-only end values ("YYYY-MM-DD") still show the multi-day range, while bare time strings are skipped and handled correctly by the existing time column logic Closes #860 * fix(planner): format reservation end time instead of rendering raw ISO string (#867) Closes #859 * fix(planner): wire Route toggle into mobile day sidebar (#850) (#868) The per-booking Route icon was missing on mobile because the mobile DayPlanSidebar invocation in TripPlannerPage didn't pass visibleConnectionIds or onToggleConnection. Mobile PWA users couldn't activate reservation map overlays without forcing desktop mode. Also corrects the Map-Features wiki: fixes the setting name ("Booking route labels" not "Show connection labels"), documents the route_calculation requirement for travel-time pills, and explains that overlays are off by default and must be toggled per reservation. |
||
|
|
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 |
||
|
|
28dbd86d03 |
fix(files): open attachments only in new tab (#840)
window.open with noreferrer returns null, which triggered the popup-blocked download fallback in addition to the new-tab open. Use a target=_blank anchor click instead. |
||
|
|
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 |
||
|
|
afce302b59 | fix: restore price and budget category fields in TransportModal | ||
|
|
ccea7f7a65 |
fix: restore map share toggle and fix public journey horizontal scroll
Re-adds the share_map permission toggle to the journey share settings UI so owners can control whether the map is visible on the public share page. Fixes horizontal scrollbar on the public journey page caused by decorative hero circles with negative offsets overflowing the viewport. |
||
|
|
45a5b4e588 |
fix: remove obsolete map share toggle and make public desktop entries openable
Map permission is always enabled on new links (share always includes map). Removed the toggle from the share settings UI since the map is now always part of the combined timeline+map view with no standalone value in toggling it. Desktop entry cards on the public share page now open MobileEntryView on click, matching the mobile behaviour added in #826. |
||
|
|
71aa8f8051 |
feat: journey gallery 1-to-N model with M:N entry-photo junction table
Replaces the old model where journey_photos was keyed per-entry with a per-journey gallery table (one row per unique photo per journey) and a new junction table journey_entry_photos that links gallery photos to entries. Key changes: - Migration 121: renames old journey_photos to journey_photos_old, creates the new gallery table + junction table, backfills both from existing data, drops the backup, removes synthetic 'Gallery' / '[Trip Photos]' wrapper entries - journeyService: rewrites photo helpers (JP_SELECT/JOIN now joins via journey_entry_photos → journey_photos → trek_photos); adds uploadGalleryPhotos, addProviderPhotoToGallery, unlinkPhotoFromEntry, deleteGalleryPhoto; simplifies deletePhoto and linkPhotoToEntry against the new schema; syncTripPhotos inserts directly into the gallery instead of a wrapper entry - journeyShareService: updates public photo and asset validation queries to join through the gallery table instead of entry_id; getPublicJourney now returns a dedicated gallery array alongside per-entry photos - journey routes: adds gallery upload, provider-photo, and delete endpoints (POST/DELETE /:id/gallery/*); adds unlink-from-entry route (DELETE /entries/:entryId/photos/:journeyPhotoId); updates link-photo to accept journey_photo_id with a backwards-compat photo_id alias - types: adds GalleryPhoto interface - client api: adds uploadGalleryPhotos, addProviderPhotosToGallery, unlinkPhoto, deleteGalleryPhoto; updates linkPhoto param name to journeyPhotoId - journeyStore: adds GalleryPhoto type, gallery field on JourneyDetail, uploadGalleryPhotos / unlinkPhoto / deleteGalleryPhoto store actions - JourneyDetailPage + tests: updated to work with the new gallery model |
||
|
|
f6b3931bc4 |
fix: mobile public share — remove map tab (#828), cap timeline width (#827), wire entry click (#826)
- #828: exclude 'map' from availableViews on mobile; MobileMapTimeline already shows combined map+timeline so the standalone map tab is redundant - #827: cap timeline feed column at xl:max-w-[50%] on ≥1280px viewports so the map aside is not dwarfed on wide monitors; applies to both desktop two-column layouts (JourneyPublicPage) - #826: wire MobileMapTimeline onEntryClick to setViewingEntry; render MobileEntryView with readOnly + public photo URL builder so photos load via the share token endpoint; add publicPhotoUrl prop to MobileEntryView so photo URLs are routable for both authenticated and public-share contexts |
||
|
|
3f489880da |
fix(journey): dedupe gallery photos and fix Immich picker button visibility on mobile (#802 #819)
Fix #802: ProviderPicker modal now uses dvh-based max-height, items-end on mobile (bottom-sheet), flex-shrink-0 on all fixed sections, min-h-0 on the scrollable grid, and env(safe-area-inset-bottom) padding so the Add button is always reachable above the iOS home indicator. Fix #819: Gallery view now deduplicates photos by photo_id (underlying trek_photos.id) so a photo linked from Gallery into an activity no longer appears twice. Gallery delete cascades to all copies. EntryEditor From Gallery grid and photo count also deduplicated. Server photo_count uses COUNT(DISTINCT photo_id). Preserves #729 guarantee (removing from an activity does not delete the Gallery copy). |
||
|
|
a8c27f9d4a |
test: update tests to match translated share link button and desktop two-column map layout
- 'Remove share link' → 'Delete link' (now uses share.deleteLink i18n key) - FE-PAGE-PUBLICJOURNEY-009/012: map tab no longer exists in desktop two-column layout; map is always rendered in the sidebar — tests updated to verify the journey-map testid is present without requiring a tab click |
||
|
|
288d33ba42 |
fix(journey/mobile): eliminate carousel scroll stutter on mobile
- Defer activeIndex updates until scrolling settles (150ms debounce) instead of updating every RAF — mid-swipe card resize (240→320px) caused layout reflow on every frame, which is the main stutter source - Switch scrollSnapType from 'proximity' to 'mandatory' for reliable browser-native snapping without needing a JS re-center pass - Remove scroll-smooth CSS class (conflicts with mandatory snap) - Remove the post-settle scrollIntoView call (mandatory snap handles it) - Drop the now-unused activeIndexRef Closes #818 |
||
|
|
e7fb78dc1e | fix(journey/settings): translate 'Remove share link' button using share.deleteLink key | ||
|
|
4d3bf390a5 |
feat(journey/settings): warn on unsaved changes before closing modal
- Track dirty state (title/subtitle changed from original) - Intercept X button, backdrop click, and Cancel with handleClose - Show ConfirmDialog when dirty; proceed with onClose only on confirm - Add common.discardChanges and common.discard keys to all 15 locales |
||
|
|
001b2365a1 |
fix(journey): correct map marker color offset and scroll-sync for unlocated entries
- sidebarMapItems now derives dayIdx from all timeline dates (not just located-entry dates), so markers stay color-aligned with day headers even when some days have no location - scroll-sync no longer calls highlightMarker for unlocated entries, preventing the map from clearing or misfiring when the scroll winner has no corresponding marker - same dayIdx fix applied to JourneyPublicPage desktop two-column view |
||
|
|
7d5dadc441 | feat(journey/public): match desktop timeline view to in-app experience | ||
|
|
c912ad4b01 | fix(journey): expand DAY_COLORS to 30 unique colors to cover a full month | ||
|
|
bd6cd55a13 | fix(journey): resolve issues #789-801 — mobile layout, day colors, location formatting, date picker, public share UX | ||
|
|
757764d046 |
hotfix: offline banner as bottom pill instead of full-width top bar
The top bar still blocked the trip planner's top nav on mobile even after #808's padding trick — nav layouts that position their own sticky headers were ignoring the --offline-banner-h offset, and the bar looked alarming for what is usually a 2s blip. Redesign as a small floating pill anchored bottom-center, hovering above the mobile bottom nav (calc(var(--bottom-nav-h) + 16px)). No layout shift anywhere, nothing ever covers the nav, and the pill looks like a passing status chip rather than an error banner. Reverts the body padding-top / navbar top offset introduced in #808 since they're no longer needed with the pill positioning. |
||
|
|
70ba24bfe1 |
fix(test): cancel Navbar theme-transition timer on unmount
The dark-mode toggle kicked off a 360ms setTimeout that removed a CSS class via 'document.documentElement'. In vitest the document was torn down before the timer fired, triggering an unhandled ReferenceError that flipped the whole run to a non-zero exit even though every test passed. Track the handle in a ref and clearTimeout on unmount (and before scheduling a new one). |
||
|
|
32f431e879 |
fix: translate months in journey timeline (#815)
formatDate() in both JourneyDetailPage and JourneyPublicPage passed undefined/'en' as the locale to toLocaleDateString, so weekday/month names always followed the browser's language instead of the app's selected UI language. Thread the selected locale through from useTranslation() in both pages. Public view still falls back to 'en' when no settings locale is available (shared links can be opened by anyone). |
||
|
|
906d8821a4 |
fix: offline banner no longer covers the top of the app (#813)
OfflineBanner was fixed at top:0 but the rest of the page had no idea it was visible, so on mobile (and the desktop nav on wider screens) the banner sat on top of the header content. When the banner is visible it now sets --offline-banner-h on <html>; body reserves that space via padding-top, and the desktop fixed Navbar shifts its top by the same amount. When back online the var is removed and everything snaps back. |
||
|
|
82b16a4bf5 |
fix(i18n): use 'polls' consistently in Dutch trip collab (#814)
Mixed 'peilingen' (titles/tabs) with 'poll/polls' (everywhere else). Normalised to 'polls' per reporter's preference — more common in modern Dutch usage anyway. |
||
|
|
069269e69c |
fix: integrations settings squish on mobile (#812) + polish
PhotoProvidersSection: - Replace raw <input type=checkbox> with TREK's ToggleSwitch so the 'spiegeln zu Immich'-style options match the rest of the app. - Wrap action row in flex-wrap so the connected/disconnected badge drops to its own line on mobile instead of clipping. - Add a short 'Test' translation (memories.testShort) shown on mobile in place of 'Test connection' — 14 languages kept in sync. ToggleSwitch: - Explicit type='button' (never a form submitter), minWidth + flex- shrink:0 so the toggle doesn't get squished next to long labels, padding:0 so no inherited UA margin warps the inner circle. MapSettingsTab: - 'Mapbox' instead of 'Mapbox GL' on narrow screens — the provider card is too cramped on mobile for the full name. - Drop the 'Experimental' badge on mobile entirely; it overlapped the title at that width. Still shown on >=sm. DisplaySettingsTab: - Time format buttons show just '24h' / '12h' on mobile; the '(14:30)' / '(2:30 PM)' hint stays on >=sm. Test updated to match the role query since the label is now split across nodes. |
||
|
|
534149ba22 |
fix(test): query form by tag since Save button is now in Modal footer
After moving Save/Cancel into the Modal's sticky footer prop, the
button no longer lives inside the <form> element, so walking up via
closest('form') returns null. Query the form directly via
document.querySelector('form') — same semantics, just doesn't assume
the button is a descendant of the form.
|
||
|
|
2dd6e04b44 |
fix: treat new-category placeholder name '...' as a UI placeholder (#811)
When a user adds a new packing category, the first item is seeded with name '...' because the server rejects empty names. That string was rendered as a real value in the input, forcing users to delete the dots before typing. Now we detect the sentinel, show it as a faint placeholder in the display span, and start the edit input empty (with '...' as the HTML placeholder). |
||
|
|
0e3d9f6ddc |
fix: reservation card header overlap on mobile (#810)
Status and category chips collided with the reservation title on narrow viewports because the header was a single-line flex with inline chips of natural width. flexWrap on the outer row plus the inner chip group lets the title+actions drop to a second row when content overflows, so the chips and the title never overlap. |
||
|
|
3b7442c2d5 |
fix: bottom-nav related mobile cutoffs (#805, #806, #807)
TransportModal + ReservationModal: move Save/Cancel into the Modal's footer slot so they stay visible on long forms (same fix as PlaceFormModal in this PR). DayDetailPanel: the floating day info panel was anchored at a fixed bottom: 96px which didn't account for safe-area-inset-bottom, causing it to overlap the bottom nav on devices with a home indicator. Use calc(var(--bottom-nav-h) + 20px) so it always floats above the tab bar with a safe gap. |
||
|
|
9e5100c71c |
fix: keep modal save button visible on mobile (#803, #804)
Two fixes in Modal.tsx: - Replace 100vh with 100dvh so iOS Safari PWA respects the actual visible viewport. Explicitly subtract --bottom-nav-h on mobile so the modal never extends behind the tab bar. - overflow-hidden on the container so the footer's bottom corners inherit rounded-2xl. - flex-shrink-0 on header and footer + min-h-0 on the body so the body shrinks and scrolls while the footer stays put. One fix in PlaceFormModal.tsx: - Save/cancel were rendered inside the scrollable body. Moved them into the Modal's footer slot. |
||
|
|
09431f725c |
feat(dashboard): add pre-copy confirmation modal showing what will and won't be copied
Introduces CopyTripDialog — a two-section modal that appears before the copy action and lists what is carried over (days, places, budget items, packing lists, TODOs, notes) and what is intentionally skipped (collaborators, collab data, files, share tokens). Addresses the UX gap raised in #786. |
||
|
|
9012bffabc |
fix(atlas): constrain bucket list width to prevent panel overflow
With 30+ bucket list entries the panel expanded to near-full viewport width, elongating the Stats tab, hiding overflow entries, and covering the Leaflet zoom controls. Measure the stats content width via ResizeObserver and use it as maxWidth on the horizontal bucket row so scroll activates exactly when entries exceed the stats panel width. Also fixes the ResizeObserver test mock to use a class (matching the IntersectionObserver pattern) so the instance methods are accessible. Closes #787 |
||
|
|
24a85b0f91 |
fix(reservations): clear location when accommodation place is removed
When hotel_place_id is cleared in the modal, also clear the location field that was auto-filled from the place. Location is hidden for hotel type so users had no way to remove the stale address after unlinking. |
||
|
|
a81fe3da0a |
fix(reservations): clear editingReservation after successful save
When a reservation was saved, only setShowReservationModal(false) was called. The modal's useEffect watches [reservation, isOpen, ...], so flipping isOpen to false re-ran the effect with the stale editingReservation (old assignment_id), resetting the form to the pre-edit state during the closing animation. Users perceived this as the value reverting after save. Calling setEditingReservation(null) immediately after the close mirrors the existing onClose handler and prevents the stale-prop form reset. |
||
|
|
70ba4d5435 |
fix(reservations): show day date range on accommodation cards
Hotel reservations store their date range in day_accommodations rather than on reservation_time, so the card date block never rendered. Pull accommodation_start_day_id / accommodation_end_day_id from the SQL join and surface them on the card. Also apply Maurice's badge-pill pattern (day name + localized date pill) to the day-range display, consistent with the modal day selectors. |
||
|
|
9f70b56a3a |
fix: pass lat/lng/name to placePhoto in PDF thumbnail fetch
Without these args, the Wikimedia fallback (used when no Google Maps key is configured) silently skips the fetch because lat/lng are NaN. The plan view (PlaceAvatar/photoService) already passes all three; this aligns the PDF path with the same behaviour. |
||
|
|
2f9d7adf4a |
fix: PDF thumbnails missing for MCP-added places (osm_id)
fetchPlacePhotos only checked google_place_id, skipping places that only have osm_id (e.g. those added via MCP). Mirror PlaceAvatar logic by falling back to osm_id in both the filter and the photo fetch call. |