* 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.
* 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.
* fix: replace raw day-ID range checks with position-based helper (issue #889 follow-up)
Commit 8e05ba7 fixed the accommodation date-range pickers, but the
post-save state filters in DayDetailPanel and several other consumers
still compared `day.id >= start_day_id && day.id <= end_day_id`. With
non-monotonic ID layouts (day_number 1-9 → IDs 17-25, day_number 10-16
→ IDs 1-7) this made the just-saved accommodation immediately invisible
— matching the regression reported in the last comment of #889.
Introduces `isDayInAccommodationRange` in `client/src/utils/dayOrder.ts`
which compares positional order (`day_number` with `indexOf` fallback)
rather than raw IDs. Falls back to the old numeric comparison when
endpoint days are absent from the loaded array (sparse test data or
partial loads) so existing tests are unaffected.
Fixed call sites:
- DayDetailPanel.tsx (initial load, post-create, post-delete, post-edit-save)
- DayPlanSidebar.tsx (daily badge renderer)
- SharedTripPage.tsx (public share view)
- TripPDF.tsx (PDF export filter + sort)
Also declares `day_number?: number` on the client `Day` type (already
returned by the server but previously untyped).
Adds regression tests FE-PLANNER-DAYDETAIL-060/061/062 covering the
edit-save, create-save, and initial-load paths with the reporter's exact
non-monotonic ID layout.
* fix: non-transport reservations no longer appear as transports in day planner (issue #914)
getTransportForDay now uses TRANSPORT_TYPES allowlist instead of only excluding hotels,
and the click handler dispatches to onEditReservation for non-transport types instead of
always opening TransportModal, preventing silent type coercion to 'flight'.
* feat: add file attachment support to TransportModal (issue #918)
Transports (flight/train/car/cruise) now support file attachments identical to the reservation modal — upload on create/edit, link existing files, and unlink. The Files tab and Assign File modal now differentiate between bookings and transports with separate sections and type-specific icons. Translations added for all 15 locales.
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.
**#541 — File downloads broken in PWA standalone mode**
Replace getAuthUrl + window.open pattern with blob-based fetch using
credentials:include. The old approach minted a 60s single-use ephemeral
token then called window.open, which handed the URL to the system browser
on Android/iOS — losing the PWA cookie jar and producing "invalid or
expired token". The new approach fetches the file directly inside the
PWA WebView as a blob URL, so no auth handoff occurs.
New helper client/src/utils/fileDownload.ts with downloadFile and openFile.
Updated FileManager, ReservationsPanel, ReservationModal, PlaceInspector,
CollabNotes.
Security hardening in fileDownload.ts:
- assertRelativeUrl() guard prevents credentials being sent to external hosts
- openFile() checks blob.type against a safe-inline allowlist; HTML, SVG and
other script-capable MIME types are forced to download instead of being
opened inline, preventing same-origin XSS via blob URLs
- resp.ok check covers all non-2xx responses, not just 401
**#505 — PWA offline session lost on reload**
Wrap authStore with Zustand persist middleware, serializing only
{user, isAuthenticated} to localStorage key trek_auth_snapshot.
maps_api_key is intentionally excluded from the snapshot.
On cold start with no network: persist hydrates isAuthenticated:true,
App.tsx clears isLoading and calls loadUser({silent:true}), ProtectedRoute
renders the dashboard immediately. The network error from loadUser leaves
isAuthenticated intact so no login redirect occurs.
On 401 or logout: store state is cleared, persist writes
{isAuthenticated:false} — stale snapshot does not grant offline access
after session expiry.
Date-only strings parsed with new Date(dateStr + 'T00:00:00') were
interpreted relative to the local timezone, causing off-by-one day
display for users west of UTC. Fixed across 16 files by parsing as
UTC ('T00:00:00Z') and displaying with timeZone: 'UTC'.
Add UI controls for configuring auto-backup schedule with hour, day of
week, and day of month pickers. The hour picker respects the user's
12h/24h time format preference from settings.
Add TZ environment variable support via docker-compose so the container
runs in the configured timezone. The timezone is passed to node-cron for
accurate scheduling and exposed via the API so the UI displays it.
Fix SQLite UTC timestamp handling by appending Z suffix to all timestamps
sent to the client, ensuring proper timezone conversion in the browser.
Made-with: Cursor