Commit Graph

458 Commits

Author SHA1 Message Date
Maurice b27793f99a fix(appearance): shorten the Auto color-mode label to 'Auto' on mobile 2026-06-29 13:59:00 +02:00
Maurice 813db0ca6e feat(appearance): show per-size text controls inline with examples
The four size-class sliders (Large/Medium/Normal/Small) are now always visible instead of behind a disclosure, each with a live sample rendered at that size and an example of what it affects (e.g. Normal = place names/descriptions, Small = addresses/labels).
2026-06-29 13:59:00 +02:00
Maurice 741639edf0 feat(appearance): granular per-size text scaling with live preview
The text-size control now adjusts each size class (Large / Medium / Normal / Small) independently as well as all-at-once. Inline px sizes are mapped to a class by their value, so the per-class sliders reach real content; each class variable = global factor x its per-class factor (no double-scaling with the root font-size that handles rem text). The settings UI gains a live preview that resizes as you drag, and the four size sliders sit behind a clear toggle.
2026-06-29 13:59:00 +02:00
Maurice bb8f4d4e5e fix(appearance): keep i18n key parity and update the scaled-emoji test
Add the new appearance settings keys (widget group titles, sidebar/density hints) to every locale so the strict key-parity check passes, and update the single-emoji chat test to expect the now-scalable calc() font size.
2026-06-29 13:59:00 +02:00
Maurice fac043c691 fix(appearance): clearer widget settings, density hint, solid surfaces with transparency off
Dashboard widget settings are grouped by where they sit on the dashboard (below the hero / right sidebar / bottom of page); the right-sidebar master toggle now nests its individual widgets and greys them out when the sidebar is off, instead of a confusing flat list mixing the master with its children. Density gains an explanatory hint plus a real compact spacing effect. Transparency-off also solidifies the Atlas glass panels and tooltip, Leaflet zoom controls and GL popups — class-based surfaces via CSS, the Atlas inline panels via a noTransparency flag.
2026-06-29 13:59:00 +02:00
Maurice a3f395e5ac fix(appearance): scale inline px font sizes so text-size reaches all content
The global text-size control only set the root font-size, which scales rem-based text (navbar, menus) but not the dense inline px sizes used across the trip planner, budget, journey and panels — so place titles and addresses stayed fixed. applyAppearance now also exposes the factor as --fs-scale-text, and a codemod wraps inline numeric fontSize in calc(<px> * var(--fs-scale-text, 1)) across components and pages (map popups and PDF excluded). Sizes are byte-identical at 100%; the control now visibly resizes the actual content.
2026-06-29 13:59:00 +02:00
Maurice a7334a9060 feat(settings): appearance settings tab
New Appearance tab with color mode (moved out of Display), color-scheme swatches, a custom accent picker with a live WCAG contrast hint, transparency and reduce-motion toggles, density, a global text-size slider with advanced per-tier controls, and per-device dashboard widget toggles. Edits preview live and commit on a short debounce. i18n keys added across all locales, translated for German.
2026-06-29 13:59:00 +02:00
Maurice d6bba454e0 fix(llm): stop the browser autofilling the LLM base URL (#1301)
The AI-parsing base URL and model inputs had no autoComplete, so a
browser password manager could drop the saved login email into the
base URL field. In the admin addon config onBlur then fired a model
lookup against e.g. "admin@trek.local", which the server rejected
with 400. Mark the base URL and model inputs as type=url /
autoComplete=off in both the admin addon config and the per-user
connection section.
2026-06-28 21:38:46 +02:00
Maurice f24d44b4a3 feat(trips): download chosen Unsplash covers into uploads (#1277)
Previously a selected Unsplash photo was stored as a remote
images.unsplash.com hot-link, so covers broke offline and on link
rot. The trip PUT handler now fetches the picked image through the
SSRF guard and saves it under uploads/covers, rewriting cover_image
to the local path (502 if the download fails). Also debounces the
cover search so a slow earlier request can no longer overwrite newer
results, drops a dead userId parameter, and reverts an unrelated
vite proxy change.
2026-06-28 20:21:13 +02:00
Azalea 8c941b52f9 [+] Unsplash 2026-06-28 20:21:13 +02:00
Maurice c7e8a5614d feat(mobile): make the bottom-nav "+" context-aware per trip tab (#1349)
On mobile the bottom-nav "+" always created a new place (except on the Costs tab,
where it added an expense). It now matches the active trip tab: Bookings adds a
reservation, Transports adds a transport, Costs adds an expense, and everything
else (Plan, plus tabs that have no create modal — Lists / Files / Collab) keeps
adding a place.

Follows the existing ?create=<intent> pattern: BottomNav.useCreateAction emits the
per-tab intent, and useTripPlanner consumes create=reservation|transport to open
the booking / transport modals (both already mounted at page level). Place and
expense were already wired; this just extends the mapping.

Tests: 4 new BottomNav cases (plan/bookings/transports/costs → correct intent +
navigate target); client tsc clean, full client suite green (2855).

Implements mauriceboe/TREK#1349
2026-06-28 16:26:16 +02:00
Maurice c10b9cc202 fix(map): keep the mobile GPS button above the day-detail panel (#1348)
On mobile the location (GPS) FAB sat at bottom: calc(var(--bottom-nav-h) + 12px),
which only clears the bottom nav. When a day is selected, DayDetailPanel slides
up over the map from bottom: navh+20 and spans nearly full width at z-index
10000, covering the button's band — so the button was hidden behind it.

DayDetailPanel now publishes its live measured height to a root CSS var
--day-panel-h (ResizeObserver, reset to 0 on unmount), and both map renderers
lift the button above the panel when it's open, reusing the hasDayDetail prop
they already receive:

  hasDayDetail
    ? calc(var(--bottom-nav-h) + 20px + var(--day-panel-h) + 12px)
    : calc(var(--bottom-nav-h) + 12px)

Applied to both the Leaflet (MapView) and GL (MapViewGL) renderers. When the
panel closes, hasDayDetail is false and the offset falls back to the bottom-nav
value. Desktop is unaffected — the button is mobile-only.

Tests: new DayDetailPanel case asserting --day-panel-h is published and reset on
unmount; client tsc clean, full client suite green (2851).
2026-06-28 14:54:31 +02:00
jufy111 0d6737726d Added focus to search places in placeFormModal 2026-06-28 11:53:42 +02:00
Maurice f4b2143a59 feat(settings): use the shared custom dropdown for the AI parsing provider
Swap the native select for CustomSelect so the provider picker matches the rest of the app's styling (dark mode, portal dropdown).
2026-06-28 11:53:19 +02:00
Maurice fc1f29bb29 feat(settings): let users set their own AI parsing model
Adds an "AI parsing" section under Settings -> Integrations where a user can choose the LLM provider, model, base URL, API key and multimodal option used for booking extraction. This per-user config applies when an admin has not configured an instance-wide model. Reuses the existing encrypted user settings: the API key is stored encrypted, never prefilled, and a blank field keeps the stored one. Adds settings.aiParsing.* across all 20 locales.
2026-06-28 11:53:19 +02:00
Maurice 01e5859564 chore(extract): recommend only Qwen3-8B (drop Qwen2.5 from the curated list)
Qwen3-8B is the identified default; the prior Qwen2.5 entries are no longer needed in the pull list.
2026-06-28 11:53:19 +02:00
Maurice 6a70f4fc41 fix(import): persist source files in IndexedDB so attach survives a reload
The source document was only kept in memory on the background task, so a page reload during the (now always-LLM ~25s) parse lost it and the booking saved without its file. Store the uploaded files in IndexedDB keyed by job id; the review loads them from there when the in-memory copy is gone, and a 1h TTL prunes abandoned imports.
2026-06-28 11:53:19 +02:00
Maurice 27fbc241e8 fix(import): preview the parsed cost as linked in the review modal
During the per-item import review the booking isn't saved yet, so the Costs section showed an empty 'Create expense' even though a linked cost will be created on save. Show the parsed price (amount + category) as the pending linked expense so the user can verify it up front. Reuses existing i18n keys.
2026-06-28 11:53:19 +02:00
Maurice 3277965426 feat(extract): recommend Qwen3-8B as the local extraction model
A/B against the prior default (qwen2.5:7b) on CPU showed Qwen3-8B is both faster and more accurate on tricky/multilingual booking docs (correct Airbnb year+price, correct DisneySea admission date), once thinking is disabled — which the router now does. Feature it as the recommended pull, keep qwen2.5:7b as the fallback.
2026-06-28 11:53:19 +02:00
Maurice 4abe96fe01 feat(import): attach the parsed source document to each booking
Keep the uploaded files on the background task and hand them to the review flow, so each reviewed booking pre-fills its Files with the document it was parsed from (uploaded with the booking on save). The two modals also adopt the shared resolveDayId helper.
2026-06-28 11:53:19 +02:00
Maurice 459426ed43 fix(import): resolve an imported transport's day from its parsed dates
A reviewed transport (e.g. a rental car) arrived with only its parsed pick-up/return dates and no day_id, so the modal kept just the time and saved a bare "HH:MM" with no date. Resolve start/end day from the parsed dates (exact match, else nearest trip day) so the booking lands on the right days.
2026-06-28 11:53:19 +02:00
Maurice 519dc3b0d8 fix(import): keep the parse-progress widget across a reload
Persist the background-import tasks (id/trip/status only) and re-fetch each job's status on mount, so a parse still running when the page reloads keeps its widget instead of vanishing; expired jobs (404) are dropped and a restored 'done' task re-fetches its items.
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 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 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 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 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 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 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 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 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
Azalea e04cf85bef feat(planner): seek places sidebar on map selection 2026-06-27 20:14:52 +02:00
Maurice 3d65bb0c12 fix: address review feedback on the distance unit setting
- server: allow distance_unit as an admin default (+ value validation) so the
  Admin "Default User Settings" toggle persists instead of returning 400
- i18n: add settings.distance to all 20 locales and translate the labels
  through t() instead of hardcoding "Distance"
- route legs: include the unit in the OSRM cache key and recompute on a unit
  switch, so map and sidebar distances refresh and never mix units
- keep wind speed tied to the temperature unit — a distance setting must not
  silently flip existing Fahrenheit users from mph to km/h
- restore the sub-1km metres reading for metric, convert GPX elevation to feet
  for imperial, and format distances with a '.' decimal in every locale
- add units.test.ts
2026-06-27 20:14:52 +02:00
Matt Van Horn 94dca8cad7 feat: add distance unit (metric/imperial) display setting
Mirrors the existing temperature_unit pattern. Adds distance_unit to Settings,
a Display settings control, admin default, and a formatDistance helper applied
at distance render sites. Backward compatible (default metric). Closes #1300.
2026-06-27 20:14:52 +02:00
Maurice b1145e7e0a fix(map): drop the hotel leg to/from a transport endpoint on arrival days (#1321)
On the first day of a trip the morning hotel is only a check-in fallback
— you arrive from home, you didn't sleep there — so bookending the route
from that hotel to the flight/train departure point drew a phantom
hotel → departure leg, both on the map and in the day sidebar. The same
backwards leg showed up on a multi-day transport's arrival day, and its
mirror departure → hotel on an evening departure.

getDayBookendHotels now also reports whether the morning hotel is one you
actually slept in and whether you sleep in the evening hotel tonight. The
map and sidebar only draw a hotel↔transport bookend when that holds; a
hotel↔place leg is always kept, so the home-base loop and onward-travel
legs are unaffected. The optimizer keeps using the hotel values as before.
2026-06-27 20:14:52 +02:00
Maurice fe54f45d62 fix(map): draw the route line to and from the day's accommodation (#1275)
The map route ran first-activity to last-activity only, while the sidebar
already showed the hotel-to-first-stop and last-stop-to-hotel legs with
their drive times. Feed the day's accommodation bookends into the map
route too, reusing the same getDayBookendHotels lookup and the
"optimize from accommodation" gate, so the drawn line starts and ends at
the hotel, including single-activity and transfer days.
2026-06-23 21:23:39 +02:00
Maurice b36c9931b3 fix(costs): allow recording an expense with no split or payer (#1286)
Adding an expense required at least one participant, so a cost you only
want to record — e.g. a booking paid on-site later — could not be saved
without splitting it. Drop the participant requirement: with nobody
selected the expense saves as a recorded total, counted in the trip
total and shown as Unfinished, and kept out of settlements until
who-paid is filled in. The shared schema and server already supported
this case.
2026-06-23 21:23:39 +02:00
Maurice c1fe1d2d6a fix(packing): keep a custom category when its last item is removed (#1289)
Removing the only item of a user-created category deleted the whole
category. Turn that row back into the existing ... placeholder in
place instead, so the category keeps its position and colour; adding an
item reuses the placeholder slot. Deleting the placeholder (or the
category menu) still removes an empty category.
2026-06-23 21:23:39 +02:00
Maurice 48ebdff2d5 feat(planner): bring back the Google Maps route export button (#1255)
The day-plan route bar lost its Open in Google Maps action in the 3.1.0 redesign. A small button with the Google logo (monochrome, theme-aware) now sits next to the Route toggle and opens the day stops, in planned order, as a Google Maps directions link in a new tab.
2026-06-23 21:23:39 +02:00
Maurice abee2fc088 fix(costs): move the unfinished marker to the category icon on mobile (#1266)
A long expense title pushed the "Unfinished" pill into the price on narrow screens. On mobile the status now shows as a small marker on the category icon, freeing the title and price row; desktop keeps the labelled pill.
2026-06-23 21:23:39 +02:00
Maurice 7459067b2e fix(pdf): show photos for OSM places in the trip PDF (#1130)
The PDF photo pre-fetch only fired for places with a google_place_id, so OSM/Nominatim places (osm_id only) fell back to category icons even though they show photos in-app. Recover osm_id from the full places pool (the assignment projection drops it) and key the photo off google_place_id || osm_id || coords, matching the UI.
2026-06-23 21:23:39 +02:00
Maurice a2c552f04d fix(days): align note time limit to 250 and keep toasts above modal blur (#1252)
The day-note 'time' field capped at 150 server-side while the dialog and shared schema allow 250, so 151-250 char notes 400'd with a confusing 'time must be 150...' message. Raise the controller and MCP limits to 250. Also lift the toast container above modal overlays so the error toast isn't rendered behind the modal's backdrop blur.
2026-06-23 21:23:39 +02:00
jubnl 6ab6d79494 fix(budget): scale category bars relative to top category 2026-06-23 21:23:39 +02:00