Commit Graph

1330 Commits

Author SHA1 Message Date
Maurice c1d61c98f0 fix(extract): backfill booking code/total and harden the reference match
Apply the deterministic confirmation-code and total fill to vendor-template results too (not just model output), and require the captured reference to contain a digit so a bare 'Confirmation'/'Reference' label no longer grabs the next prose word.
2026-06-28 11:53:19 +02:00
Maurice c7f5694f63 feat(extract): add Expedia and rental-broker booking templates
Pull the hotel/rental fields these vendors print in a stable text layout (name, address, stay/pickup dates, price, reference) deterministically, so the import stops depending on the local model for them. Handles German long/abbreviated months and English dates incl. 12-hour and comma forms.
2026-06-28 11:53:19 +02:00
Maurice d0b4052c5d fix(import): create linked costs and accommodations from reviewed bookings
Reviewing an imported booking saves it through the normal reservation
form, which dropped the parsed price (so no linked cost was created) and
only created the accommodation when both nights matched a trip day.
Carry the parsed price into a linked cost on save, and create the
accommodation from whichever day the check-in/out dates resolve to.
2026-06-28 11:53:19 +02:00
Maurice 1c81e8b959 feat(import): parse bookings in the background with a progress widget
Parsing a booking can take a while on a CPU host, so don't hold the
upload modal open for it. The async import endpoint returns a job id
right away; the parse runs server-side (one at a time per user) and
pushes progress over the user's WebSocket, and a small widget in the
bottom corner tracks it while the user keeps navigating and editing.
A finished job opens the per-item review from the widget.
2026-06-28 11:53:19 +02:00
Maurice 8f1c99a07a feat(extract): drive local parsing through a layered extraction router
The single-shot prompt was unreliable on multi-leg flights and longer
documents, and slow on a CPU host. For the local provider, run a small
router instead:

- deterministic vendor templates first, with no model call at all
- exactly one grammar-enforced call per document via Ollama's native
  `format` (flights as a flat array of legs, everything else as one flat
  reservation, the type picked from keywords or a union schema)
- booking-wide fields (booking reference, total price, the overnight
  arrival day) filled deterministically from the text afterwards, and
  dates coerced to ISO so a natural-language date can't slip through

Recommend qwen2.5 in the AI-parsing settings instead of NuExtract.
2026-06-28 11:53:19 +02:00
Maurice 5fdd4aa153 feat(import): review each parsed booking before it's saved
Instead of writing parsed items straight to the trip, the import opens the
normal edit modal pre-filled for each one, so you can check and fix it before
saving — useful when a model guesses a wrong date or address. Hotels gained an
editable address field; on save an existing place is matched by name, otherwise
the reviewed address is geocoded and a new place is created.
2026-06-28 11:53:19 +02:00
Maurice 22801938b5 fix(admin): tidy the AI parsing settings and recommend the 2B model
The provider picker is the shared CustomSelect now and the form is split into
clear sections rather than a flat stack of inputs. NuExtract 2.0 2B is the
recommended default — fastest on a CPU-only host and MIT licensed; the 4B
carries a non-commercial licence, so it's no longer flagged as recommended.
2026-06-28 11:53:19 +02:00
Maurice 8640100312 feat(extract): drive NuExtract with its native template
NuExtract isn't an instruct model — fed a plain chat prompt it just echoes the
schema back. Detect a NuExtract model by id and talk to it the way the model
cards document: the JSON template inlined in a single user message, no system
prompt, no json_schema, temperature 0. Its flat result is mapped back to the
same KiReservation shape the rest of the pipeline already uses, so nothing
downstream changes; every other model keeps the generic prompt.

Money is taken as a verbatim string and parsed locally (German "1.580,22 €"
otherwise comes back as 1.49772), a rental car's pickup/return ride the from/to
fields so a stray form label doesn't become the location, and a lodging with no
name falls back to its address instead of being dropped.
2026-06-28 11:53:19 +02:00
Maurice e666313865 fix(extract): refresh accommodations after a booking import
A freshly imported hotel links to an accommodation that lives outside the
trip store, so loadTrip alone left the reservation edit modal with blank
place/date fields. Reload the accommodations list once the import finishes.
2026-06-28 11:53:19 +02:00
Maurice aa72d527c9 feat(extract): create a linked cost from the booking price on import
When a confirmation carries a total price, record it as a real expense
linked to the reservation (in the matching Costs category) instead of
leaving the amount in metadata only. Gated on the Costs addon.
2026-06-28 11:53:19 +02:00
Maurice 684ac3b442 feat(extract): capture seat, class, platform, price + event venue contact
Request and map root-level seat/class/platform and a total price/currency into reservation metadata (shown on the card; price reuses the existing label). Read both the root and reservationFor and tolerate common field-name aliases (priceAmount, priceCurrencyISO4217Code, fareClass, ...) since models name these inconsistently. Also capture event/attraction venue telephone + url onto the auto-created place, matching lodging/restaurant.
2026-06-28 11:53:19 +02:00
Maurice f049229e25 perf(extract): cap LLM input at 4000 chars for CPU-only speed
On a GPU-less host the model's prompt-eval time scales with input length and dominates total latency. Booking details sit at the top of a confirmation, so capping the extracted text at 4000 chars (was 8000) roughly halves extraction time (~50s warm for a capable local 7B model) with no loss of fields on real hotel/rental confirmations. Tunable if a long multi-segment itinerary needs more.
2026-06-28 11:53:19 +02:00
Maurice 38565c3c6d feat(extract): fill transport/booking fields, geocode endpoints, assign days
- rental car: request+map dropoffLocation, emit pickup->return from/to endpoints, set a location string (G1/G2/G3). - geocode endpoints (stations/stops/terminals/rental desks) on confirm via Nominatim; mapper now emits coordless named endpoints and confirm persists only the geocoded ones (G6). - assign every dated booking to the nearest trip day so it still shows when slightly out of range, and keep hotel accommodation from vanishing when a check date misses (G5/G10). - fix bus mislabelled as train + add bus_number metadata (G7/G8), flag malformed boats (G9), accept root start/end time for events (G11). - raise the local-LLM timeout to 300s for CPU-only Ollama.
2026-06-28 11:53:19 +02:00
Maurice a1cbc11169 fix(extract): make AI imports reliable and fast on local models
client: the import call inherited the global 8s axios timeout and aborted long LLM extractions even though the server finished it; remove the timeout. server: raise the OpenAI-compatible LLM timeout 60s->180s (a cold Ollama model can take ~45s to first token). server: cap extracted text to 8000 chars before the LLM - multi-page T&C tails (30k+ chars) overflowed the context window, truncating the relevant head and making CPU inference crawl; booking details sit at the top.
2026-06-28 11:53:19 +02:00
Maurice b859ae8b00 fix(extract): auto-run the AI fallback when the addon is enabled
Booking import only fell back to the LLM when each user flipped an 'always retry with AI' toggle, so by default files kitinerary returned nothing for just failed. Run the fallback automatically whenever the AI Parsing addon is on (fallback-on-empty); drop the now-redundant per-user toggle and its setting.
2026-06-28 11:53:19 +02:00
jubnl ae14a6c860 feat(extract): extract data using LLM 2026-06-28 11:53:19 +02:00
Maurice 41c541828f fix(setup): warn when ADMIN_EMAIL/ADMIN_PASSWORD are ignored, ship reset-admin
The first-run seeder only applies ADMIN_EMAIL/ADMIN_PASSWORD on an empty
database and then silently ignores them. People add the vars after the first
boot, or pull a fresh image without clearing ./data, restart, and cannot log
in with no hint why (#1339). The default is a generated password (not the
.env.example placeholder), printed once in the first-run box. Now: warn loudly
when the vars are set but a user already exists, and warn on a partial
(one-of-two) config instead of quietly falling back.

Also ship the reset-admin recovery script in the image -- it was never COPYed in
despite the wiki referencing it. node server/reset-admin.js resets/creates
admin@trek.local with a generated password (RESET_ADMIN_EMAIL/RESET_ADMIN_PASSWORD
overridable), picks a free username so it cannot trip UNIQUE(username), and sets
must_change_password.
2026-06-28 11:10:40 +02:00
Maurice 37f1fff367 Merge main into dev after the v3.1.3 release 2026-06-28 10:59:53 +02:00
Maurice 0c1c534435 docs(wiki): document the snap Docker + no-new-privileges startup failure 2026-06-28 10:30:37 +02:00
github-actions[bot] 0631e34a79 chore: bump version to 3.1.3 [skip ci] v3.1.3 2026-06-27 19:10:04 +00:00
Maurice 8a013f6fa9 fix(build): bump the client to vite 8.1.0 so the amd64 image builds
The 3.1.3 docker build failed on linux/amd64 while bundling the client
(arm64 happened to pass): vite 8.0.16 pins rolldown 1.0.3, but tsdown
pulls rolldown 1.1.2, and on amd64 npm hoists the 1.1.2 native binding so
vite's 1.0.3 rolldown loaded a mismatched one ("builtin:vite-wasm-fallback
does not match any variant of BindingBuiltinPluginName"). Moving the client
to vite 8.1.0, which expects rolldown 1.1.2, lines the bundler up with the
hoisted binding. Verified by building the client in a clean linux/amd64
node:22 container.
2026-06-27 21:09:05 +02:00
Maurice 7c3440f139 Revert "chore: bump version to 3.1.3 [skip ci]"
This reverts commit 4ceea09e31.
2026-06-27 20:49:58 +02:00
github-actions[bot] 4ceea09e31 chore: bump version to 3.1.3 [skip ci] 2026-06-27 18:15:27 +00:00
Maurice 03cdb4d276 fix(files): reject cross-trip reservation/place/assignment links
A member of one trip could point a file at a reservation, place or
day-assignment belonging to another, private trip — on upload, on a
metadata update, or through the file-link endpoint. The reservation join
in the file list and the links list then returned that trip's reservation
title, disclosing it across the trip boundary and letting an attacker
enumerate foreign reservation titles by their id.

The file already had to belong to the caller's trip; now the linked
reservation/place/assignment must too. findForeignLinkTarget checks each
supplied id against the trip (assignments via day -> trip) and the upload,
update and link handlers reject a cross-trip reference with 400 before it
is stored. Same-trip links and clearing a link are unchanged.
2026-06-27 20:14:52 +02:00
Maurice f0877a2e7d Replace the 3.0 upgrade notices with a thank-you / support modal
The 3.0 "what's new" notices have served their purpose, so swap them for a single thank-you notice that comes back once on every fresh install and version bump. It carries Buy Me a Coffee and Ko-fi buttons and only shows on desktop. Adds a per-version recurring mode (new dismissed_app_version column) plus external-link CTAs to support it; the 3.0.14 whitespace-collision admin notice stays active.
2026-06-27 20:14:52 +02:00
Maurice aa91f009ad fix(costs): freeze the FX rate so settled expenses don't reopen when rates drift (#1335)
Settle-up transfers are stored as fixed amounts, but a foreign-currency expense was re-converted with live rates on every settlement calc. When the rate drifted, the fixed transfer no longer cancelled the re-valued expense and a few-cent residual re-opened the settled position. Foreign-currency expenses now freeze the live rate at entry time into the existing budget_items.exchange_rate column, and the settlement converts with that frozen rate when working in the trip currency. Legacy rows (exchange_rate = 1) keep using live rates, so historical data is unchanged until re-edited; rate fetch failures fall back to live rates.
2026-06-27 20:14:52 +02:00
Maurice 2277f28a57 fix(airtrail): import the airline name, not the ICAO code (#1334)
AirTrail returns each airline as {icao, iata, name}, but the import reduced it to the ICAO/IATA code, so an imported flight showed e.g. 'EWG' instead of 'Eurowings'. The picker and the stored reservation now use the airline name (falling back to the code when AirTrail has none). The raw code is kept in metadata.airline_code so the writeback to AirTrail still sends a code, not a name (#1240), and the change-detection snapshot hash stays on the code so existing flights don't spuriously re-sync.
2026-06-27 20:14:52 +02:00
Maurice 1ec2d62b1c fix(reservations): keep dated bookings on their date when the trip range shifts (#1288)
Changing a trip's start date positionally re-dates the day rows (keeping their ids), so a dated booking's day_id stayed glued to a now-re-dated day and the booking visually shifted by the offset — until you re-opened and saved it. After a date-range change, non-hotel bookings are now re-anchored to the day matching their absolute reservation_time (the same derivation create/update already use). Bookings whose date falls outside the new range are left untouched; hotels and the relative positional shift of places/notes are unaffected.
2026-06-27 20:14:52 +02:00
Maurice 649735726f fix(map): pin the GL basemap label language to the UI language (#1299)
On a GL map (Mapbox Standard) the basemap labels fell back to the browser/OS locale, so place and country names showed stacked in several scripts (e.g. 'India / भारत / India') regardless of the chosen language. Pin Mapbox Standard's basemap label language to the user's UI language via the basemap 'language' config property, mapping the few TREK codes that differ (br->pt, gr->el, zh/zhTw->zh-Hans/zh-Hant). Applies to both the trip map and the journey map; classic and MapLibre styles are left unchanged.
2026-06-27 20:14:52 +02:00
Maurice 4e91fbca48 fix(places): guide single-place links to the right importer (#1304)
Pasting a single-place Google Maps share link (.../maps/place/...) into the list import failed with a cryptic 'Could not extract list ID from URL'. When the link is a single place it now returns a clear message telling the user to paste it into the place search box instead; other unrecognised URLs keep the existing list-link message.
2026-06-27 20:14:52 +02:00
Maurice 4cb9b18cc6 fix(atlas): assign border places by polygon, not just bounding box (#1331)
getCountryFromCoords picked the country with the smallest bounding box containing the point, so a place just across a border (e.g. Strasbourg, which sits inside both the FR and DE boxes) landed in the wrong, smaller-box country. When more than one country box matches, it now disambiguates with the real admin0 polygon via point-in-polygon, smallest-box-first; a micro-territory with no admin0 polygon (HK, MO, SM, VA, ...) keeps the smallest-box win, and an unmatched point falls back to the old behaviour. The common single-candidate case is unchanged.
2026-06-27 20:14:52 +02:00
Maurice f3b54166fb test(planner): cover the single-place route-tools visibility gate (#1330)
Asserts the route tools appear for one located place when a bookend accommodation exists, and stay hidden without one, guarding the #1330 visibility change.
2026-06-27 20:14:52 +02:00
Maurice 8c63235cd2 test(share): cover the translated untitled-day label (#1296)
Renders the public share page in German with a titleless day and asserts the i18n label 'Tag 1', guarding the t('dayplan.dayN') fix against a regression to a hardcoded English string.
2026-06-27 20:14:52 +02:00
Maurice 3554fde8d6 fix(dashboard): persist the currency & timezone widgets so an upgrade keeps them (#1311)
The currency and timezone widgets stored their state only in browser localStorage, so a (docker) upgrade that clears site storage reset them to defaults — unlike every other preference, which is saved server-side. Persist them through the per-user settings store (no schema change; the settings table takes arbitrary keys) and migrate any existing localStorage values on first load so users keep their picks. dashboard_timezones is left unset by default so the widget can tell 'never chosen' from an explicitly emptied list.
2026-06-27 20:14:52 +02:00
Maurice eb0ab4001d fix(planner): show the route tools for a single place when optimizing from accommodation (#1330)
The day's route tools were gated on having 2+ stops, so a day with one located place and accommodation optimization on hid them — even though the map already draws the hotel -> place -> hotel route. Treat a lone located place as routable when a bookend hotel with coordinates exists, mirroring what the map renders. Purely additive to the existing 2+ case.
2026-06-27 20:14:52 +02:00
Maurice 497d8e854f fix(map): draw the hotel-to-hotel leg on a transfer day with no activities (#1297)
On a day whose only content is checking out of one accommodation and into another, there are no waypoints for the hotel bookends to attach to, so no line was drawn. Add the A->B leg directly when both bookend hotels are real (excluding the day-1 arrival fallback per #1321) and distinct, so an ordinary same-hotel rest day still draws nothing.
2026-06-27 20:14:52 +02:00
Maurice e6fe14cac2 fix(pwa): use the self-contained app icon for the favicon so it shows on dark tabs (#1328)
icon-dark.svg is a black logo on a transparent background and is invisible on a dark browser tab strip (e.g. Edge dark mode). Point the favicon at icon.svg, which carries its own dark gradient background and reads on both light and dark chrome; icon-dark.svg keeps its in-app light-mode use.
2026-06-27 20:14:52 +02:00
Maurice 2a8caf6e7d fix(share): translate the day label on the public share page (#1296)
Untitled days on the public share page rendered as a hardcoded English 'Day N' instead of the dayplan.dayN key used everywhere else, so they stayed English regardless of the viewer's language. The key is already translated in every locale.
2026-06-27 20:14:52 +02:00
Maurice 005e0c109d fix(maps): make Overpass endpoints configurable and harden the POI search (#1309)
Builds on @Hardik-369's instance-specific User-Agent idea and reworks the rest
of the #1309 fix:

- keep the unique User-Agent (buildUserAgent) — a shared UA gets the public
  Overpass mirrors to rate-limit harder; it appends the configured instance
  URL and is applied to every Nominatim/Overpass/Wikimedia call
- add OVERPASS_URL so an operator behind locked-down egress (e.g. a Kubernetes
  cluster) can point the explore search at an internal/self-hosted Overpass
  instance instead of the public mirrors
- keep the per-endpoint timeout default at 12s but make it tunable via
  OVERPASS_TIMEOUT_MS for slow self-hosted instances; non-positive/invalid
  values fall back to the default rather than 502-ing every search at a 0ms cap
- log each endpoint's failure reason before the 502 so blocked egress is
  diagnosable instead of a bare "Overpass request failed"

Adds unit tests for the User-Agent, endpoint and timeout resolution plus the
all-mirrors-down path, and documents the two new env vars in .env.example, the
wiki and the Helm chart.
2026-06-27 20:14:52 +02:00
Hardik-369 e54ea2f17d fix: memoize User-Agent and prevent localhost leak; bump timeout to 30s
- Memoize USER_AGENT via IIFE so it's computed once, not per-request
- Only append instance URL when APP_URL or ALLOWED_ORIGINS is explicitly
  configured; skip the getAppUrl() localhost fallback
- Bump OVERPASS_TIMEOUT_MS to 30000 (above the observed 25.7s TTFB)
2026-06-27 20:14:52 +02:00
Hardik-369 544a76d2da fix(maps): increase Overpass timeout and add instance-specific User-Agent
The POI search endpoint (/api/maps/pois) returned 502 errors because:

1. OVERPASS_TIMEOUT_MS (12s) was shorter than mirror response times
   (kumi.systems takes ~25.7s to first byte). Increased to 25s to match
   the [timeout:20] query timeout.

2. The static User-Agent string was indistinguishable between instances,
   making rate-limiting and throttling more likely. The new userAgent()
   function appends the instance's APP_URL so each deployment identifies
   itself uniquely, following Overpass API best practices.
2026-06-27 20:14:52 +02:00
Maurice a5ba246cb8 test(i18n): account for the Swedish locale in SUPPORTED_LANGUAGES
The Swedish translation added 'sv' as the 21st language but left the
FE-COMP-I18N-009 length assertion at 20, so the full client suite went
red on this branch. Bump the count to 21 and add an 'sv' sample.
2026-06-27 20:14:52 +02:00
Maurice 0b2780ead2 test: make the Google Maps ftid path honest + cover the URL helper
The Places API googleMapsUri is a cid-style URL with no ftid, so the
search/getPlaceDetails fixtures had stored a fabricated ftid. Switch them
to real cid URLs and assert google_ftid is null — the precise
query_place_id link still fixes the wrong-spot bug — and document the
behaviour on googleFtidFromMapsUrl.

- add a direct googleFtidFromMapsUrl test: extracts a real /place ftid,
  returns null for a cid URL, rejects malformed/hostile values
- add placeGoogleMaps.test.ts covering the whole fallback chain
  (ftid -> place_id -> details URL -> coords) and the hostile-ftid rejection
- PlaceInspector: use a freshly-fetched ftid when the place hasn't stored one
2026-06-27 20:14:52 +02:00
Azalea 91fcaa50f6 Use Google Maps feature IDs for place map links 2026-06-27 20:14:52 +02:00
Azalea 9669642c62 feat(maps): add MapLibre OpenFreeMap support (#1317)
Adds MapLibre GL with OpenFreeMap as a tokenless third map provider
alongside Leaflet and Mapbox: a provider abstraction with style presets,
CSP + service-worker entries for tiles.openfreemap.org, and the
map_provider allow-list entry. Mapbox-only APIs stay gated behind the
mapbox provider, and existing Mapbox/Leaflet users are unaffected.

Maintainer review follow-ups folded in: the new map-settings strings are
translated across all locales; the GL engine is lazy-loaded so
Leaflet-only installs don't download it; MapLibre gets its own
maplibre_style slot so switching providers no longer overwrites a custom
Mapbox style; and the MapLibre render path plus the OpenFreeMap
style-guards are covered by tests.
2026-06-27 20:14:52 +02:00
Maurice 7531badbe8 i18n(sv): add the settings.distance key (Avståndsenhet)
Keeps the Swedish locale in parity with the rest after the distance-unit
setting (#1300) landed on the release branch.
2026-06-27 20:14:52 +02:00
Andreas Olsson 424018fc66 feat: swedis translation 2026-06-27 20:14:52 +02:00
Maurice 9d8af4b357 i18n(nl): fix doubled "In" typo in packing.importTitle
"InInpaklijst importeren" → "Inpaklijst importeren".
2026-06-27 20:14:52 +02:00
eindpunt 5b3f77f11d Added new Dutch translations and some corrections 2026-06-27 20:14:52 +02:00
Azalea e04cf85bef feat(planner): seek places sidebar on map selection 2026-06-27 20:14:52 +02:00