Commit Graph

29 Commits

Author SHA1 Message Date
Maurice 20791a29a7 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.
2026-05-31 21:10:00 +02:00
Julien G. 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 8e05ba7. Prevents inverted
  start_day_id/end_day_id on trips with non-monotonic day IDs.

* Clearing accommodation_id on a hotel reservation now forces
  assignment_id to null in the save payload, removing the stale
  day-assignment link that had no UI path to clear.

* Migration: swaps inverted start_day_id/end_day_id pairs in
  day_accommodations where start.day_number > end.day_number,
  recovering existing corrupt rows from the pre-fix picker bug.

* Tests FE-PLANNER-RESMODAL-050/051/052 cover both fixes.

* fix: preserve line breaks and wrap long URLs in notes fields (#930)

Add remark-breaks to all reservation/place notes markdown renderers so
single newlines render as <br>, and add wordBreak/overflowWrap styles
so long unbroken URLs (e.g. booking.com tracking links) wrap correctly.

* fix: delete linked budget item when accommodation or reservation is deleted (#933)

Deleting an accommodation or reservation now removes any budget item
linked via reservation_id, preventing orphan entries in the Budget page.
Also fixes a pre-existing payload-shape bug where budget:deleted was
broadcast with {id} instead of {itemId}, breaking live updates for
collaborators when a reservation price was cleared.

Tests added: ACCOM-006, RESV-009b, BUDGET-004b.

* fix: restore scroll position in mobile Plan and Places sidebars on reopen (issue #932)

Both DayPlanSidebar and PlacesSidebar have their own internal scroll
containers (overflowY: auto). Scroll events don't bubble, so previous
attempts that tracked scrollTop on the outer portal div never fired.

Each sidebar now accepts initialScrollTop and onScrollTopChange props.
The internal scroll container saves its scrollTop via onScrollTopChange
on every scroll event, and restores it via useLayoutEffect on mount
(before the browser paints, so no visible flash).

TripPlannerPage holds the saved values in refs (mobilePlanScrollTopRef,
mobilePlacesScrollTopRef) and passes them through on each portal mount.

* fix(map): prevent auto zoom-out when opening/closing place inspector (issue #921)

Both Leaflet and Mapbox GL renderers now gate fitBounds strictly on fitKey
increments from the parent. Selecting or dismissing a place inspector changes
paddingOpts (via hasInspector) but no longer triggers a re-fit that zoomed
the map out to the full trip extent when no day was selected.

Also removes the zoom-12 visibility gate on Leaflet route info pills so they
render at all zoom levels when a route is active.

* fix: translate mobile bottom-nav tab labels (issue #931)

Replaced hardcoded English labels in BottomNav with t() lookups using the same translation keys as the desktop navbar (nav.myTrips, admin.addons.catalog.*.name).
2026-05-01 01:43:19 +02:00
Julien G. 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.
2026-04-23 19:49:36 +02:00
Maurice 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.
2026-04-21 21:42:48 +02:00
jubnl 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.
2026-04-21 19:54:43 +02:00
Maurice 1cc43f63df fallback day-number badge when a day has no date
If a trip has no dates set but a day has a custom title, the
dropdown showed only the title with no context. Fall back to
'Day N' as the badge so users can still tell which day it is.
2026-04-21 12:34:45 +02:00
Maurice 3450bd59f8 feat: show date badge on day selectors + i18n transport modal titles
Day selectors in the Transport, Reservation and Hotel-Day-Range
modals only showed the renamed day title once a day had a custom
name — hiding the actual date. Added an optional badge prop to
CustomSelect, rendered as a pill next to the label, and wired the
date badge onto all affected dropdowns. FileManager day section
headers got the same pill for consistency.

Also translated transport.addTransport and transport.modalTitle.*
in all 13 non-English language files; the keys existed but still
carried the English source string.
2026-04-21 12:28:43 +02:00
jubnl 16b81a8356 fix(bookings): preserve accommodation dates when place is unlinked or missing
- Remove NOT NULL constraint on day_accommodations.place_id (migration)
  and change ON DELETE CASCADE → SET NULL so deleting a place no longer
  cascades to the accommodation row
- Switch listAccommodations / getAccommodationWithPlace to LEFT JOIN so
  accommodations without a linked place are visible to the modal
- Relax create/update guards in reservationService to only require
  start_day_id + end_day_id, not place_id; place_id remains optional
- Client save guard now sends create_accommodation whenever FROM/TO days
  are set, regardless of whether a hotel place was selected
- Add re-hydration useEffect in ReservationModal to back-fill hotel
  fields from the accommodations prop when it arrives after modal opens
  (race between isOpen and the tripAccommodations fetch)
- Fix demo-seed TDZ crash: move db Proxy declaration before DEMO_MODE
  block so circular require in demo-reset resolves correctly
- Sidebar accommodation badge falls back to reservation title when
  place_name is null; click/cursor disabled for placeless accommodations
- listAccommodations now joins reservations to expose reservation_title
2026-04-20 23:09:05 +02:00
jubnl 3f61e1ca38 feat: add multi-day transport reservations with dedicated modal and route segmentation
Introduces a TransportModal for creating/editing flight, train, car, and cruise
reservations that span multiple days. Transport entries now break the map route
into disconnected segments so the polyline reflects actual travel legs.

- Add TransportModal with airport/location pickers, multi-day date range, and all transport types
- Extend DB schema with end_day_id on reservations (migration 110) and backfill from existing dates
- Refactor useRouteCalculation to emit [][][number,number] segments split at transport boundaries
- Update MapView, DayPlanSidebar, ReservationsPanel, TripPlannerPage to wire up transport flow
- Add transport i18n keys across all 15 languages
2026-04-18 06:10:33 +02:00
Maurice 530550455d feat(ui): unified toolbar design + redesigned budget widgets + polish
Trip planner now has a consistent rounded toolbar across bookings, lists,
budget and files. Each panel shows title, inline filter pills (with
counts where useful) and an accent action button on the right. Moved
per-tab controls into the toolbar — lists import, todo add, budget
currency/add-category, files trash/filters — and dropped the redundant
in-panel headers.

Budget sidebar redesigned: total-budget card with indigo-ringed avatars
and coloured split bar; settlement flows as paired avatar cards;
by-category donut rebuilt in SVG with per-category gradients. Both cards
now follow dark/light mode via a widgetTheme helper.

Todo: add-new-task is a portalled modal on desktop, the add-task input
bar is gone; new SORT BY section in the sidebar; inline category
creation in the task editor.

Reservations: pending / confirmed sections remember their collapsed
state per trip (localStorage).

Misc: per-trip connections toggle moved into the day-plan sidebar,
booking endpoints fixed to show on map for trains/cruises/cars as well,
label localStorage persistence, RESMODAL test updated to the new
airport-select flow.

i18n: the new booking / map / todo / budget strings are translated into
all 15 supported languages.
2026-04-17 23:25:38 +02:00
Maurice 8defc90e95 feat(bookings): show transport routes on map (#384, #587)
Adds from/to endpoints to flight/train/cruise/car reservations with
live map rendering. Flights use geodesic arcs and a curved duration +
distance badge; train/car/cruise render as straight or geodesic lines
with endpoint markers. Airports come from an embedded OurAirports
database (~3200 airports, offline-capable); train/cruise/car locations
via Nominatim. Per-trip connection toggle sits in the day plan
sidebar, persisted in localStorage. Clicking a map endpoint opens the
existing transport detail popup. New display setting toggles endpoint
labels on the map. Migration 105 adds the reservation_endpoints table
plus needs_review flag; existing flights are backfilled from their
IATA metadata on server startup.
2026-04-17 14:04:40 +02:00
Maurice 409a63633c feat: support check-in time ranges for hotel accommodations
- Add check_in_end column to day_accommodations (Migration 102)
- Server: create/update accommodation accepts check_in_end
- Bidirectional sync: check_in_end synced between accommodation
  and linked reservation metadata (check_in_end_time)
- DayDetailPanel: shows check-in range (e.g. "14:00 – 22:00"),
  new "Until" time picker in hotel form
- ReservationModal: new check-in-until field for hotel bookings
- ReservationsPanel: displays check-in range in metadata cells
- i18n: checkInUntil keys in all 15 languages

Closes #366
2026-04-16 00:23:00 +02:00
jubnl 8c7567faf3 fix(pwa): fix offline session redirect and file download auth (#505 #541)
**#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.
2026-04-14 21:48:25 +02:00
Maurice bd0b7746ab fix: support pasting numbers with comma decimal separator in budget and bookings
Handle European number formats (e.g. 1.150,32) by detecting the last
separator as decimal and stripping thousand separators. Applied to
budget inline edit cells, add item row, and reservation price field.

Fixes #498
2026-04-08 18:49:10 +02:00
Maurice 4ba6005ca3 fix(dayplan): resolve duplicate reservation display, date off-by-one, and missing day_id on edit
- Exclude place-assigned reservations from timeline to prevent duplicate display
- Use selected day's date instead of today when entering time without date
- Pass day_id when updating reservations, not only when creating
2026-04-06 12:56:54 +02:00
Maurice 66a057a070 fix(bookings): resolve date handling and file auth bugs
- Clear reservation_time fields when switching booking type to hotel (#459)
- Parse date-only reservation_end_time correctly on edit (#455)
- Show end date on booking cards for date-only values (#455)
- Add auth token to file download links in bookings (#454)
- Account for timezone offsets in flight time validation (#456)
2026-04-06 11:32:06 +02:00
jubnl 411d8620ba fix(reservations): reset stale budget category when it no longer exists
If the budget category stored in reservation metadata was deleted, the
form would re-submit it on next save, resurrecting the deleted category.
Now validates against live budget items on form init and falls back to
auto-generation when the stored category is gone.

Closes #442
2026-04-05 22:46:16 +02:00
jubnl 7e0fe3b1b9 fix(reservations): hide price/budget fields when budget addon is disabled
Closes #440
2026-04-05 22:30:13 +02:00
Maurice 38206883ff feat(budget): bidirectional sync between reservations and budget items
- Link budget items to reservations via reservation_id column
- Update budget entry when reservation price changes (not create duplicate)
- Delete budget entry when reservation price is cleared
- Sync price back to reservation when edited in budget panel
- Lock budget item name when linked to a reservation
- Add migration 73 for reservation_id on budget_items
2026-04-05 18:16:02 +02:00
mauriceboe b8058a2755 fix(reservations): budget category dropdown, localized auto-category, price input cleanup
- Budget category uses dropdown with existing categories instead of freetext
- Auto category uses translated booking type names (e.g. "Volo" in Italian)
- Remove number input spinner arrows, use decimal inputMode
- Add budget entry creation to PUT handler (update), not just POST (create)
- Error logging for failed budget entry creation
- i18n keys for all 13 languages
2026-04-05 00:13:07 +02:00
mauriceboe aa244dd548 feat(reservations): add price field with automatic budget entry creation
- Optional price and budget category fields on the reservation form
- When a price is set, a budget entry is automatically created on save
- Price and category stored in reservation metadata for reference
- Hint text shown when price is entered
- i18n keys for EN and DE
2026-04-04 23:59:30 +02:00
Maurice 1aea2fcee8 Merge pull request #361 from lucaam/add_span_days_feature
Support multi-day spanning for reservations
2026-04-04 13:58:16 +02:00
Maurice 897e1bff26 fix(dates): use UTC parsing and display for date-only strings (#351)
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'.
2026-04-03 21:18:56 +02:00
Luca 0115987e52 feat: support multi-day spanning for reservations (flights, rental cars, events)
- ReservationModal: add separate departure/arrival date+time fields with
  type-specific labels (Departure/Arrival for flights, Pickup/Return for
  cars, Start/End for generic types), timezone fields for flights
- DayPlanSidebar: getTransportForDay now matches reservations across all
  days in their date range; shows phase badges (Departure/In Transit/
  Arrival etc.) with appropriate time display per day
- ReservationsPanel: show date range when end date differs from start
- All 13 translation files updated with new keys
2026-04-03 16:55:45 +02:00
Gérnyi Márk 5f71b85c06 feat: add client-side permission gating to all write-action UIs
Gate all mutating UI elements with useCanDo() permission checks:
- BudgetPanel (budget_edit), PackingListPanel (packing_edit)
- DayPlanSidebar, DayDetailPanel (day_edit)
- ReservationsPanel, ReservationModal (reservation_edit)
- CollabNotes, CollabPolls, CollabChat (collab_edit)
- FileManager (file_edit, file_delete, file_upload)
- PlaceFormModal, PlaceInspector, PlacesSidebar (place_edit, file_upload)
- TripFormModal (trip_edit, trip_cover_upload)
- DashboardPage (trip_edit, trip_cover_upload, trip_delete, trip_archive)
- TripMembersModal (member_manage, share_manage)

Also: fix redundant getTripOwnerId queries in trips.ts, remove dead
getTripOwnerId function, fix TripMembersModal grid when share hidden,
fix canRemove logic, guard TripListItem empty actions div.
2026-03-31 23:36:16 +02:00
Maurice 533d6f84d8 fix: use user locale instead of hardcoded de-DE for number/date formatting — closes #144
- CurrencyWidget: format numbers with user's locale
- ReservationModal: date formatting uses locale
- TripPDF: locale fallback to browser default instead of de-DE
- holidays.ts: formatDate accepts optional locale parameter
2026-03-30 17:28:14 +02:00
Maurice b6f9664ec2 feat: multi-link files to multiple bookings and places — closes #23
Files can now be linked to multiple bookings and places simultaneously
via a new file_links junction table. Booking modal includes a file picker
to link existing uploads. Unlinking removes the association without
deleting the file.
2026-03-29 23:32:04 +02:00
Maurice e78c2a97bd v2.6.2 — TREK Rebrand, OSM Enrichment, File Management, Hotel Bookings & Bug Fixes
Rebrand:
- NOMAD → TREK branding across all UI, translations, server, PWA manifest
- New TREK logos (dark/light, with/without icon)
- Liquid glass toast notifications

Bugs Fixed:
- HTTPS redirect now opt-in only (FORCE_HTTPS=true), fixes #33 #43 #52 #54 #55
- PDF export "Tag" fallback uses i18n, fixes #15
- Vacay sharing color collision detection, fixes #25
- Backup settings import fix (PR #47)
- Atlas country detection uses smallest bounding box, fixes #31
- JPY and zero-decimal currencies formatted correctly, fixes #32
- HTML lang="en" instead of hardcoded "de", fixes #34
- Duplicate translation keys removed
- setSelectedAssignmentId crash fixed

New Features:
- OSM enrichment: Overpass API for opening hours, Wikimedia Commons for photos
- Reverse geocoding on map right-click to add places
- OIDC config via environment variables (OIDC_ISSUER, OIDC_CLIENT_ID, etc.), fixes #48
- Multi-arch Docker build (ARM64 + AMD64), fixes #11
- File management: star, trash/restore, upload owner, assign to places/bookings, notes
- Markdown rendering in Collab Notes with expand modal, fixes #17
- Type-specific booking fields (flight: airline/number/airports, hotel: check-in/out/days, train: number/platform/seat), fixes #35
- Hotel bookings auto-create accommodations, bidirectional sync
- Multiple hotels per day with check-in/check-out color coding
- Ko-fi and Buy Me a Coffee support cards
- GitHub releases proxy with server-side caching
2026-03-28 16:38:08 +01:00
Maurice 8396a75223 refactoring: TypeScript migration, security fixes, 2026-03-27 18:40:18 +01:00