mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 21:31:46 +00:00
1ed00b67adda0aff0b2922012e087f37934967cb
189 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3e9626fce9 |
feat(places): enrich list-imported places via the Places API (#886) (#1161)
* feat(places): enrich list-imported places via the Places API (#886) Google/Naver list imports only carry a name and coordinates, so the places open as bare pins — the Maps tab jumps to coordinates, with no photo, address or open/closed. Add an opt-in "Enrich places via Google" toggle to the list-import dialog, shown only when a Google Maps key is configured. When enabled, after the (fast, unchanged) import the server runs a background pass that re-resolves each place by name — biased to and validated against the imported coordinates so a common-name search cannot overwrite the wrong place — and fills the empty address/website/phone/photo columns plus the resolved google_place_id, pushing each row over the live sync. Opening hours and the proper Maps link then work on demand from the stored id. Enrichment only fills empty fields, runs detached so a long list never blocks the import, and no-ops when no key is configured. * fix(places): use the ToggleSwitch component for the enrich toggle Match the rest of the app — the import-enrichment opt-in used a raw checkbox; swap it for the shared ToggleSwitch (text left, switch right) like the settings toggles. |
||
|
|
3398da633b |
fix(planner): make route tools reachable in mobile day plan sheet (#1142)
* wiki: update dev env * wiki: small precision in dev env * fix(planner): make route tools reachable in mobile day plan sheet On mobile, selecting a day closes the plan sheet immediately, so the route tools footer (Route toggle / Optimize / routing profile) - gated on the selected day - was never reachable. Desktop was unaffected. - Add showRouteToolsWhenExpanded prop to DayPlanSidebar: when set, route tools render on any expanded day with 2+ assigned places - Make handleOptimize accept an explicit dayId (defaulting to selectedDayId, preserving desktop behavior) - Keep the distance/duration pill gated on the selected day, since routeInfo belongs to the selected day's calculated route - Enable the prop on the mobile plan sheet in TripPlannerPage * fix(planner): correct route-tools prop doc and dev-environment wiki - Reword the showRouteToolsWhenExpanded JSDoc to list the controls the footer actually renders (Route toggle / Optimize / travel profile); there is no "Open in Google Maps" action in that block. - Wiki: drop the non-existent server test:parity script, document the real shared i18n:parity checks, and fix the i18n note (the translation layer already lives in @trek/shared, it is not "upcoming"). --------- Co-authored-by: jubnl <jgunther021@gmail.com> Co-authored-by: Maurice <mauriceboe@icloud.com> |
||
|
|
31f99f0e4e |
Various fixes: 2FA autofocus, viewer-timezone times, duplicate place guard (#1159)
* fix(auth): autofocus the 2FA code input when the MFA step appears (#767) * fix(notifications): show notification and admin times in the viewer timezone (#1149) SQLite CURRENT_TIMESTAMP is UTC but the string has no Z, so the client parsed it as local time. Normalize in-app notification created_at to ISO-UTC, and stop forcing the admin user table to render in the server timezone. * fix(places): warn before adding a duplicate place (#1152) Manually adding a place did not check the existing pool, so the same POI could land in Unplanned twice. Flag a likely duplicate by Google Place ID, name or near-identical coordinates and require a confirming second click to add anyway. |
||
|
|
56655d53b4 |
AirTrail integration: import flights & two-way sync (#214) (#1158)
* feat(admin): register AirTrail as an integration addon
Off by default; toggle lives in Admin -> Addons with a Plane icon. The
per-user connection (URL + API key) follows in integration settings.
* feat(integrations): add per-user AirTrail connection
Settings -> Integrations gains an AirTrail section: instance URL + Bearer
API key (encrypted at rest via apiKeyCrypto), a self-signed-TLS opt-in and
a test-connection check. Served by a small Nest controller under
/api/integrations/airtrail, gated on the airtrail addon and SSRF-guarded.
The key is per-user, so it only ever returns that user's own flights.
* feat(transport): import flights from AirTrail
Adds an AirTrail Import button next to Manual Transport that lists the
user's AirTrail flights and highlights the ones inside the trip dates.
Selected flights become reservations linked to their AirTrail origin
(external_* columns), deduped against flights already in the trip, then
broadcast to every member. The mapping resolves airports, airport-local
times and flight metadata; the linkage is what the two-way sync rides on.
* feat(transport): badge AirTrail-linked flights as synced
Linked reservations show an 'AirTrail synced' badge, or 'no longer
synced' once the flight is gone from AirTrail.
* feat(transport): keep TREK and AirTrail flights in sync both ways
A scheduled poll reconciles each connected owner's flights: field edits
(detected by snapshot hash, since AirTrail has no updated_at) flow into
the linked reservation and broadcast live; a flight deleted in AirTrail
keeps the TREK row but stops syncing. Editing a linked flight in TREK
pushes back to AirTrail under the importer's credentials, preserving the
existing seat manifest; if the owner disconnected the link detaches so the
poll can't revert the local edit. Deleting in TREK never touches AirTrail.
* i18n(airtrail): add AirTrail strings across all locales
* test(airtrail): cover flight mapping, timezones and snapshot hashing
* fix(airtrail): reduce airline/aircraft objects to codes
The flight list/get response returns airline and aircraft as joined
objects ({icao, iata, name, ...}), not bare codes. Mapping them straight
through produced '[object Object]' titles and stored objects in metadata,
which crashed reservation rendering. Extract the ICAO/IATA code instead,
and title flights by their flight number.
* fix(airtrail): clear error on non-JSON responses, tolerate /api in URL
A misconfigured instance URL made AirTrail serve its SPA/login HTML, and
the raw JSON.parse failure surfaced as 'Unexpected token <'. Surface an
actionable message instead, and strip a pasted trailing /api so the base
URL still resolves.
* feat(transport): sync AirTrail edits on trip open, not just on the poll
Add a per-user on-demand sync (POST /integrations/airtrail/sync) triggered
when a connected user opens a trip, so AirTrail-side edits appear right away
instead of waiting up to a full poll cycle. Lower the background poll from 15
to 5 minutes as a safety net.
* fix(transport): refresh imported AirTrail flights without a reload
loadTrip doesn't fetch reservations, so a freshly imported flight only
appeared after a full page reload — use loadReservations instead. Also show
flight dates in the user's locale format (e.g. 13.06.2026) rather than the
raw ISO string.
* style(settings): align AirTrail connection with the photo-provider layout
Match the Immich section: stacked URL/key fields, a ToggleSwitch for
self-signed TLS, and a Save / Test-connection row with a status badge.
* feat(transport): add a seat field when editing flights
The transport editor only offered a seat field for trains; flights had
none even though imports store metadata.seat. Show and persist a seat for
flights too.
* style(transport): match the AirTrail button height to Manual Transport
* feat(transport): put the flight seat next to flight number and sync it to AirTrail
Move the seat from a standalone row to the per-leg flight details (beside
the flight number), stored per leg in metadata.legs[].seat with the first
leg mirrored to metadata.seat. On push, set the seat number on the user's
own AirTrail seat (the one with a userId), leaving co-passengers untouched;
import/poll read that same seat back.
* refactor(planner): move the AirTrail trip-open sync into useTripPlanner
Page containers must not own state/effects (lint:pages). Same logic,
relocated from the page into its data hook.
* test(db): pin the region-reconciliation test to its schema version
The test re-ran 'the last migration' assuming the reconciliation is last;
it no longer is once later migrations are appended. Pin to version 135 and
re-run from there (the appended migrations are idempotent).
|
||
|
|
e224befde7 |
Map/planner/dashboard polish and small community features (#1155)
* feat(planner): reorder days in a modal instead of a dropdown The day-reorder control opened a small anchored dropdown; move it into the shared Modal (portal, dimmed backdrop, Esc/backdrop close) so it matches the Add activity dialog. Drag handles, up/down arrows and the day badges are unchanged. * feat(map): explore reliability, Mapbox popups + compass, region-biased search POI explore: clamp oversized viewports, query the Overpass mirrors in parallel (first valid response wins) with a per-request timeout and a short-lived cache, and surface a retry when every mirror fails - so it returns results at any zoom instead of timing out. Mapbox renderer: add the place/POI hover popups (name, category, address, photo) the Leaflet map already had, plus a compass pill next to the explore pill that resets the view to north. /api/maps/search: accept an optional locationBias to fix foreign-region bias and expose Google's place types in the result. * feat(dashboard): list-view and mobile polish Use the Archived status label for the filter and show Open dates for trips without dates; drop the unused settings button next to the view toggle. Desktop list view renders the date as a stat-style block separated from the counts. Mobile list rows are stacked (slim cover banner + centred date), trip actions stay visible (touch has no hover), and the hero card's hover lift is disabled on touch; small spacing fix under the sidebar. * feat: small community-requested options Raise the plan-note subtitle limit to 250 characters and add more note icons. Expose is_archived and cover_image on the update_trip MCP tool. Add place coordinates to the PDF export. Allow creating a category from an existing to-do, and add a show/hide toggle on the admin password fields. * test(shared): bump day-note subtitle limit assertion to 250 * test: align specs with the new search param order and archive label Keep lang as the 3rd positional arg of the maps search controller so the existing unit test stays valid, and forward locationBias as the 4th. Add the now-used Popup to the MapViewGL mapbox mock, switch the dashboard archive-filter query to the Archived label, and expect the 4-arg search call. |
||
|
|
f46cc8a98e |
Reorder whole days and insert a day (#589) (#1148)
* feat(days): reorder whole days and insert a day at a position Adds reorderDays + insertDay to the day service and a PUT /days/reorder route (plus an optional position on create). Day rows stay stable so a day's assignments, notes, bookings and accommodations ride along by id; on a dated trip the calendar dates stay pinned to their slots while the content moves across them, and each booking's date is re-stamped onto its day's new date (time-of-day preserved) so day_id stays consistent. Renumbering uses the two-phase write to avoid the UNIQUE(trip_id, day_number) collision, and a move that would invert an accommodation's check-in/out span is rejected. * feat(planner): reorder days from a toolbar popup, and add days A new toolbar button opens a popup listing the days; drag a row by its grip or use the up/down arrows to reorder, and add a day from there. Reorders apply optimistically with rollback and sync over WebSocket; the day headers are left untouched, so the existing place drop-targets are unaffected. * i18n: add day-reorder strings across all languages |
||
|
|
1378c95078 |
Explore places on the map, planner route fixes, and instance-wide Mapbox (#1147)
* feat(maps): add an OSM POI search endpoint (category within a viewport) New /api/maps/pois queries OpenStreetMap via Overpass for places of a category (restaurants, cafes, hotels, sights, …) inside a bounding box. OSM-only by design — it never calls Google, even when a Google key is configured. * feat(map): explore nearby places on the trip map (OSM category pill) A floating, icon-only pill over the planner map lets you toggle a POI category and see those OpenStreetMap places in the current view; clicking a marker opens the add-place form pre-filled (name, address, website, phone). Single-select with a 'search this area' action after the map moves. Renders on both the Leaflet and Mapbox maps, and can be turned off in settings (discussion #841). * fix(planner): anchor timed places when optimising and route transports by location - The day optimiser no longer reshuffles places that have a set time — they stay anchored to their time, like locked places. - The route now uses a transport's departure/arrival location as a waypoint when it has one (e.g. a flight's airport), instead of breaking the route at every booking; transports without a location are ignored for routing but still show their leg's distance/duration under the booking. * feat(admin): instance-wide Mapbox defaults in default user settings Admins can set a shared Mapbox token (plus style, 3D and quality) as instance defaults, so the whole instance can use Mapbox without each user pasting their own key. Users without their own value inherit it via the existing admin-defaults merge; the shared token is stored encrypted (discussion #920). |
||
|
|
bb477645a3 |
Support multi-leg (layover) flights (#1146)
* feat(transport): support multi-leg (layover) flights in the booking form
A flight booking can now hold an ordered chain of airports (e.g. FRA -> BER ->
HND) instead of a single departure/arrival pair. The route is entered as a list
of waypoints with a '+ add stop' button; each stop carries its own arrival and
departure time plus the airline/flight number of the segment leaving it, while
the whole booking keeps one price.
Stored without a schema change: the existing reservation_endpoints rows carry the
ordered waypoints (from/stop/to by sequence) and a metadata.legs array holds the
per-leg detail. Top-level metadata (departure_airport/arrival_airport/airline/
flight_number) mirrors the first and last leg, so a single-leg flight persists
exactly as before and legacy readers keep working.
* feat(planner): show each flight leg as its own day-plan entry, ordered by time
A multi-leg flight now expands into one entry per leg (BER -> FRA, then FRA ->
HND), each on its own day with its own times, instead of a single span. Each leg
is an addressable slot (reservation id + leg index) so places and notes can be
dropped into the layover gap between legs; the per-leg position is persisted in
metadata.legs[i].day_positions and survives a reload.
Day-plan items are now ordered chronologically: anything with a time (a place's
time, a flight leg, a timed note) sorts by that time, and untimed items inherit
the time of the item before them so they stay where they were placed.
* feat(planner): show the full multi-stop route in the bookings panel
The route row now lists every waypoint (FRA -> BER -> HND) by sequence instead of
just the first and last airport.
* feat(map): draw multi-leg flights as connected legs with a marker per airport
Both the Leaflet and Mapbox overlays now render a flight over all its waypoints:
one great-circle arc per leg and a marker at every airport, with the label
showing the full route and the summed distance. A single-leg flight is unchanged.
Also drops the floating stats badge that was drawn on transport arcs.
* fix(map): centre a clicked place above the bottom inspector panel
Selecting a place panned/flew it to the dead centre of the screen, where it sat
behind the detail card. Both overlays now bias the target into the visible area
above the bottom panel (Leaflet offsets the pan by the inspector inset; Mapbox
passes the padding to flyTo).
* feat: show the full multi-stop flight route in PDF and calendar export
The PDF day list and the ICS export now render the whole route (FRA → BER → HND)
for a multi-leg flight instead of just the first and last airport, falling back to
the flat metadata for single-leg flights. The ICS keeps a single event per booking.
* feat(import): group connecting flight legs into one multi-leg booking
When a booking confirmation contains several flight legs sharing a PNR that
connect at the same airport with a short layover (under 24h), they are now
imported as a single multi-leg booking (from/stop/to endpoints + metadata.legs)
instead of one booking per leg. A round trip (same PNR, multi-day gap) stays two
separate bookings, and a single flight is unchanged.
* i18n: translate the new flight-route strings into all languages
* i18n: translate the Costs page into every language
The Budget → Costs rework left the new costs.* strings untranslated in every
non-English locale (they fell back to English). Translate them across all
supported languages.
* Revert "fix(map): centre a clicked place above the bottom inspector panel"
This reverts commit
|
||
|
|
e65acb3de7 |
Fix a batch of reported bugs (#1145)
* fix(maps): fall back to OSM/Wikipedia for place photos and normalize non-standard language codes (#1137) * fix(auth): refuse password reset for OIDC/SSO-linked accounts (#1129) * fix(docker): ship server/assets (airports + atlas geo) in the runtime image (#1133, #1119) * fix(unraid): point the template at a PNG icon Unraid can render (#1073) * fix(offline): serve cached file blobs when offline or on network failure (#1046, #1069) * fix(map): centre the selected pin in the visible map area above the bottom panel (#1125) * fix(pdf): render persisted place-photo proxy URLs as images (#1130) * fix(planner): show the selected place category in the edit form (#1134) * fix(dashboard): collapse list-view trip cards to a compact row on mobile (#1132) |
||
|
|
49b3af8b0d |
feat: optimize routes around accommodation, confirm note deletions (#1123)
Optimize day routes around the accommodation
When a day has an accommodation set, the route optimizer now treats it as
the day's home base: it optimizes a loop that leaves the hotel and returns
to it, so the stop nearest the hotel comes first. On a transfer day -
checking out of one hotel and into another - the route runs from the first
hotel to the second instead.
The optimizer also gained a 2-opt pass on top of the nearest-neighbor
ordering, which removes the crossings the greedy pass used to leave behind.
A new display setting ("optimize route from accommodation", on by default)
lets you turn the anchoring off.
Confirm before deleting notes
Deleting a plan note or a collab note now asks for confirmation first. On
phones and tablets the edit and delete icons sit close together and were
easy to mis-tap, which deleted notes with no way back.
|
||
|
|
247433fb2a |
feat(costs): rework Budget into Costs — Splitwise-style, multi-currency, mobile (#1106)
* fix(journey): authorize reads of the journey share link GET /api/journeys/:id/share-link now requires journey access (canAccessJourney), matching the create/delete share-link routes and the get_journey_share_link MCP tool. Returns no link when the caller lacks access to the journey. * feat(costs): rework Budget into Costs — Splitwise-style, multi-currency, mobile Renames the Budget addon to "Costs" (UI only) and reworks it into a Tricount/ Splitwise-style cost tracker: multiple payers per expense, equal split across chosen members, settle-up with persisted history + undo, 12 fixed categories, per-expense currency with live FX conversion to a user-set display currency (Settings -> Display), and locale-correct money formatting. Adds a desktop and a dedicated mobile layout. A migration backfills existing budget items (single payer, split members, currency). Closes #551 (per-expense currency). Also switches the app font to self-hosted Poppins (Geist for secondary subtext), replacing the Google Fonts CDN dependency. * fix(costs): neutral dashboard dark palette + liquid glass, full page width, entry-count badge - Dark mode used a warm oklch palette that read brownish; switch to the neutral zinc tokens used by the dashboard (#121215 bg, #f4f4f5 ink) and add a subtle backdrop-blur glass on cards. - Costs now uses the full available page width on desktop instead of a 1280px cap. - Render the expense count next to the Expenses title as a badge. - Adapt budget/journey unit tests to the new payer-based settlement model and the Costs rename (category default 'other', Costs tab/CostsPanel). * fix(costs): drop the entry-count badge, always show row edit/delete actions Removes the count badge next to the Expenses title and makes the per-row edit/delete actions permanently visible (no longer hover-only) on desktop too. * feat(costs): currency-native money formatting, custom select/date, rename addon to Costs - Format every amount in its own currency convention (symbol position, grouping and decimal separators) regardless of app language, via a currency->locale map (EUR -> '12,00 €', USD -> '$12.00', JPY -> '¥12', ...). Previously Intl used the app locale, so EUR showed the symbol in front under an English UI. - Use TREK's CustomSelect (searchable, with symbols) and CustomDatePicker in the add/edit expense modal instead of the native <select>/<input type=date>. - Rename the 'Budget Planner' add-on to 'Costs' in the admin list (display only; id/tables/permissions/MCP stay 'budget') via seed + a migration for existing DBs. * feat(auth): configurable session duration via SESSION_DURATION Adds a SESSION_DURATION env var (ms-style strings: 1h, 7d, 30d, ...) controlling how long a session stays valid before re-login. It drives both the trek_session JWT exp claim and the cookie maxAge from one source, so they never drift. Invalid values warn at startup and fall back to the default (24h — unchanged). The MFA challenge token and MCP OAuth tokens keep their own TTL. Implements the request from discussion #946. Documented in the env-var wiki page, .env.example and docker-compose.yml. |
||
|
|
6ef3c7ae6b |
feat(reservations): native booking-confirmation import via KDE KItinerary (#1102)
* feat(reservations): native booking-confirmation import via KDE KItinerary
Adds a two-step preview → confirm flow for importing booking emails,
PDFs, PKPass and HTML confirmations. The server invokes the KDE
kitinerary-extractor binary, maps JSON-LD schema.org output to TREK
reservation shapes, and persists via the existing createReservation
pipeline (accommodations, budget, places, WebSocket broadcasts).
- NestJS BookingImportModule: preview + confirm endpoints under
/api/trips/:tripId/reservations/import/booking{,/confirm}
- KitineraryExtractorService: spawns the binary, filters stderr noise,
handles QDateTime (@value) timezone-aware datetimes
- kitinerary-mapper: FlightReservation, TrainReservation, BusReservation,
BoatReservation, LodgingReservation, FoodEstablishmentReservation,
RentalCarReservation, EventReservation → typed preview items
- BookingImportService: auto-creates place rows; geocodes venues without
coordinates via Nominatim (name+address → address → name fallback);
resolves day IDs for accommodation linking
- BookingImportModal: drag-and-drop multi-file upload, preview cards
with type icons, per-item exclude toggle, confirm step
- Shared Zod contracts: BookingImportPreviewItem, PreviewResponse,
ConfirmRequest, ConfirmResponse — consumed by controller, service,
API client and modal
- Dockerfile: node:24-trixie-slim runtime; amd64 downloads KDE static
binary + locales; arm64 installs libkitinerary-bin + symlinks to
fixed path; ENV KITINERARY_EXTRACTOR_PATH set for both arches
- /api/health/features exposes { bookingImport: boolean } so the UI
hides the Import button when the binary is absent
- i18n keys (English), wiki docs, API.md, README one-liner
* i18n: add booking import translations for all 19 non-English locales
Adds 17 reservations.import.* keys and undo.importBooking to ar, br, cs,
de, es, fr, gr, hu, id, it, ja, ko, nl, pl, ru, tr, uk, zh, zh-TW.
* chore: enforce i18n parity
* docs(wiki): add KItinerary local setup instructions to dev environment guide
|
||
|
|
abe1c549bd |
feat(transport): add bus, taxi, bicycle, ferry and other transport types (#1105)
Closes #718. Adds five new transport reservation types alongside the existing flight/train/car/cruise: bus, taxi, bicycle, ferry and a generic 'transport_other' catch-all. The new types are treated as first-class transports everywhere — the transport modal, day plan, route calculation, map overlays, file grouping and the PDF export — and are translated across all 20 locales. A dedicated 'transport_other' value is used for the catch-all so existing 'other' bookings are not reclassified as transport. |
||
|
|
9bec97fc19 |
Fix a batch of reported bugs: Atlas regions, planner overlays, imports, Safari modals (#1094)
* Start the Journey date picker week on Monday (#1078) The Journey entry date picker started the week on Sunday (firstDow = getDay(), headers Su-first) while every other picker (CustomDateTimePicker, VacayCalendar) starts on Monday. Align it: Monday-first leading offset ((getDay()+6)%7) and Mo-first weekday headers. * Fix Taiwan resolving to CN-TW in the Atlas country search (#1049) natural-earth gives Taiwan ISO_A2='CN-TW' (a subdivision-style value) with ADM0_A3='TWN'. The dynamic A2_TO_A3 augmentation added 'CN-TW'->'TWN', which then overwrote the legitimate TWN->TW entry in the reverse map, so Taiwan's country option resolved to 'CN-TW' — unresolvable by Intl.DisplayNames (no name, broken flag, not searchable). Only augment A2_TO_A3 with real 2-letter codes. * Drop empty leftover dateless days when a trip gets a shorter dated range (#1083) generateDays kept all unused dateless placeholder days after switching to an explicit (shorter) date range, so day_count (COUNT(*) FROM days) stayed inflated. Delete the empty leftovers (no assignments/notes/accommodations) like the dateless path already does, while preserving any that still hold content. Adds TRIP-SVC-017. * Render GPX and route overlays once the Mapbox style has loaded (#1036) The GPX and route geojson effects ran before the map 'load' event had attached their sources, so on the first paint they hit the early return and never re-ran. Add mapReady to their dependencies so they fire again the moment the sources exist. * Convert HEIC trip and journey covers to JPEG before upload (#1085) HEIC/HEIF covers coming straight off an iPhone could not be rendered in the preview or stored as a usable image. Route both cover pickers through normalizeImageFile, the same conversion the journal entry editor already uses, so the file becomes a JPEG before it leaves the browser. * Name GPX routes and tracks after their source file so multiple imports stick (#1054) Unnamed routes and tracks all fell back to the same generic 'GPX Route' / 'GPX Track' label, so the name-based import dedup dropped every one after the first - importing several files (or one file with several tracks) only kept a single place. Derive the default name from the source filename with an index suffix when a file holds more than one geometry, thread the filename down through the controller, and let the import modal take more than one file at a time. Adds PLACE-SVC-037/038. * Namespace the modal backdrop class so content blockers stop hiding it (#1027) Generic class names like .modal-backdrop sit on the cosmetic filter lists that content blockers (1Blocker, EasyList Annoyances) ship, and get hidden with display:none. The shared Modal - used by New Trip and Add Place - carried that class, so Safari users running such a blocker saw the modal silently fail to open with no error and no network request. Rename it to .trek-modal-backdrop. * Highlight GB regions by resolving England/Scotland/Wales/NI to finer admin-1 codes (#1067) A zoom-8 reverse geocode of a UK place only resolves to the constituent country (GB-ENG/SCT/WLS/NIR), but Natural Earth's admin-1 polygons for GB are counties and boroughs (GB-LND, GB-MAN, GB-CON, ...). Those four codes match no polygon, so places in England never highlighted in the Atlas while CH/IT/NL/etc. worked. When a GB lookup lands on a constituent country, re-resolve it at a finer zoom where Nominatim exposes the county/borough code the polygons actually carry. Other countries keep the exact zoom-8 behaviour. Adds ATLAS-UNIT-021. * Surface the real place-search error instead of a generic toast (#1092) When a place search or detail lookup fails, the backend already forwards the upstream reason - including descriptive Google Places API messages such as 'Places API (New) has not been used in project ... or it is disabled'. The planner discarded it and always showed 'Place search failed', so a key that is mis-enabled, unbilled, or pointed at the legacy API instead of Places API (New) looked like an unexplained silent failure. Show the server-provided message when present, and stop the Atlas bucket-list search from swallowing its error without a trace. * Await the async cover normalization in the TripFormModal paste test (#1085) handleCoverSelect now normalizes the pasted file before previewing it, so URL.createObjectURL is called a microtask later. The assertion moves into waitFor; a non-HEIC file still passes through unchanged. |
||
|
|
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.
|
||
|
|
324d930ca3 |
remove route_calculation setting, always use OSRM routing (#1064)
The per-user route_calculation toggle was a second, hidden on/off layer on top of the day footer's show-route button, and made it easy to end up with straight-line routes for no obvious reason. Drop the setting entirely: routing is always on, the footer toggle stays the single switch. Old stored values are simply ignored (settings are key-value, no migration needed). |
||
|
|
e050814c42 |
feat(planner): real road routes (OSRM) with travel-time connectors (#1060)
* feat(planner): real road routes (OSRM) with travel-time connectors Replace the straight-line "as the crow flies" route with real OSRM road geometry (FOSSGIS routed-car/-foot) and an Apple-Maps style render (blue casing under a lighter core) on both the Leaflet and Mapbox GL maps. Routes are off by default and toggled per session, with a driving/walking mode switch in the day footer. Each day shows per-segment travel time/distance connectors between places, computed from the OSRM legs and split at transport bookings. Also redesigns the day header for visual consistency: vertical number+weather capsule, name with a divider before the date, subtle hotel/rental pills that stay on one line, and a hover-revealed 2x2 action square (edit / add transport / add note / collapse). Drops the Google Maps button. * test(planner): update route hook tests for calculateRouteWithLegs |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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 |
||
|
|
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. |
||
|
|
afce302b59 | fix: restore price and budget category fields in TransportModal | ||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
290f566daa |
fix(planner): eliminate drag-and-drop jank in trip planner
- Suppress trek-stagger animation on the day list while a drag is active so nth-child delays (0–320 ms) no longer re-fire on every hover change - Replace sibling drop-indicator <div> injections with borderTop/borderBottom on the target row to prevent nth-child index shifts during drag - Dedup setDragOverDayId calls in onDragOver handlers so setState is only invoked when the active day actually changes - Move initTransportPositions out of getMergedItems (render path) into a useEffect to stop mid-drag setState cascades |
||
|
|
da39b570eb |
feat(mcp): align MCP surface with current app state
- Add Journey addon tools (list, get, entries, contributors, suggestions, available trips, create/update/delete journey and entries, reorder, contributors CRUD, preferences, share link management) - Add Journey resources (trek://journeys and sub-resources) - Split transport (flight/train/car/cruise) into dedicated tools with endpoints[] and needs_review support; narrow reservation types to non-transport only - Add airport lookup tools (search_airports, get_airport) under geo:read - Add import_places_from_url and bulk_delete_places to places tools - Add journey:read/write/share OAuth scopes (27 total) with translations across all 15 locales - Default end_day to start_day when creating a transport (MCP + UI) - Fix MCP.md drift: addon gates, removed files resource, corrected get_trip_summary description, todos under Packing addon |
||
|
|
25bdf56d16 |
add mapbox gl option, gps location, journey reorder + polish
- Mapbox GL provider alongside Leaflet for trip and journey maps (opt-in in settings with token, style presets incl. 3D on satellite, quality mode, experimental badge). - GPS "blue dot" with heading cone on mobile; three-state FAB (off / show / follow), geodesic accuracy circle, desktop-hidden since browser IP geo is too coarse for navigation. - Marker drift fix: outer wrap no longer carries inline position/transform, so mapbox's translate keeps the pin pinned at every zoom and pitch. - Journey map popup (mapbox-gl): Apple-Maps-style tooltip on marker highlight/click showing entry title + location / date subline. - Journey feed reorder: up/down controls to the left of each entry reorder sort_order within a day. Server endpoint, optimistic store update, rollback on failure. - Journey entry editor: desktop modal now centers over the feed column only, backdrop still blurs the whole page (map included). - Scroll-sync guard on journey: marker click locks the sync so smooth-scroll can't steer the highlight to a neighbouring entry mid-animation. - Misc: map top-padding aligned with hero, live/synced badges replaced by a compact back-button in the hero, skeleton entries no longer pollute the journey map, journey detail no longer shows map on mobile path when combined view is active. |
||
|
|
4db6cbef22 | add Emil-style UI polish pass (animations, shared components, feel) | ||
|
|
777b68f87b |
fix tests for sidebar/settings refactor + weather archive fallback
- DayPlanSidebar: add aria-label to undo button, replace title with aria-label
so tests can still locate buttons by accessible name after tooltip refactor
- tests: switch getByTitle("Add Note") to getByLabelText
- tests: find undo button via aria-label (new expand/collapse button also uses
width:30, breaking the old style-based lookup)
- PlacesSidebar tests: loosen "All" button regex to account for count badge
- DisplaySettingsTab tests: use getByRole for Auto button (two "Auto" spans
coexist for mobile/desktop); handle multiple English matches in lang test
- weatherService tests: past-date case now expects an archive fetch instead
of an immediate no_forecast error
|
||
|
|
66a7de09c1 |
dayplan toolbar polish + weather archive fallback
- weather: add archive API branch in getWeather for past dates (previously returned no_forecast, making the day-strip widget show "—") - dayplan: add expand/collapse-all toggle between ICS and Undo with animated icon swap (ChevronsUpDown <-> ChevronsDownUp) - dayplan: drop the trip title + date range block from the sidebar header (already shown in the page header), toolbar now right-aligned |
||
|
|
38f4c9aecb |
refine places sidebar: filter counts, compact select UI, tooltip component
- replace "Auswählen" button with small Check↔X toggle next to category dropdown - move bulk-action bar below search, icon-only buttons (Select all, Delete) - filter tabs as pill buttons with per-filter count badges - shared Tooltip component (portaled, delayed) replaces native title - apply tooltip to select toggle, bulk actions, add note, add transport - rename places.importFile: "Datei importieren" -> "Dateimport" |
||
|
|
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 |
||
|
|
4f01a10277 | Merge branch 'dev' into feat/selective-file-import-perf | ||
|
|
6a718fccea |
feat(import): selective GPX/KML element import and performance improvements
Add type-selector UI in the file import modal letting users choose which GPX elements (waypoints, routes, tracks) or KML/KMZ elements (points, paths) to import. KML LineString placemarks are now imported as path places with route_geometry. Performance improvements: - Extract MemoPlaceRow with React.memo and contentVisibility:auto to cut unnecessary re-renders in PlacesSidebar - Add weatherQueue to cap concurrent weather fetches at 3 - Replace sequential per-place deletes with a single bulkDelete API call (new DELETE /places/bulk endpoint + deletePlacesMany service) - Memoize atlas/photo/weather service calls to avoid redundant requests - Add multi-select mode to PlacesSidebar for bulk operations Add large GPX/KML/KMZ fixtures for integration/perf testing and two profiler analysis scripts under scripts/. |
||
|
|
71637a8483 |
fix(tests): restore packing panel inline header + update tests for ui changes
- PackingListPanel accepts inlineHeader prop (default true) to keep its
legacy title and inline import button; ListsContainer passes
inlineHeader={false} since the toolbar now owns those controls
- ReservationModal tests look for the renamed 'Car' button (was 'Rental Car')
- Budget total-budget test asserts against the split integer/decimal
spans that replaced the single text node
|
||
|
|
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. |
||
|
|
5e9c8d2c43 |
fix(bookings): client test failures after map overlay refactor
- Make useEndpointPane tolerant when map mock lacks getPane/createPane - Add useMapEvents to react-leaflet mock in MapView.test - Rewrite RESMODAL-042 to use the new AirportSelect flow (airline and flight number only; airport codes are now saved as endpoints, not metadata) |
||
|
|
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. |
||
|
|
6eb3ab38fb |
fix(ui): hide scrollbars on mobile, keep styled bars on desktop
Scrollbars on mobile caused layout shift (content pushed left). Hidden via media query on mobile; desktop retains thin styled scrollbars. Also removes inline scrollbarWidth override in DayPlanSidebar that bypassed the CSS rule. |
||
|
|
9f3a88223d |
fix: update ReservationModal test for check-in time range fields
Use getAllByText for check-in labels since both "Check-in" and "Check-in until" now match the /Check-in/i pattern. |