Commit Graph

821 Commits

Author SHA1 Message Date
jubnl 2fa075a076 fix(shared): align todo contracts with the wire format the client sends
The optional metadata fields (category, due_date, description,
assigned_user_id — plus priority on update) now accept explicit null: the
client clears fields by sending null, which the schemas rejected. checked
accepts the legacy 0/1 numeric form alongside boolean (the integration net
pins it). Drops the client-side casts that papered over the too-narrow
types.
2026-07-26 14:54:53 +02:00
Maurice b5ebffa115 fix(dashboard): keep a trip the hero fell back to in the grid
The hero picks the trip that is running, else the next one coming up, else
just trips[0] — and that pick was then removed from the grid below. The first
two cases are deliberate: the trip is already on screen, showing it again
right underneath would be noise. The trips[0] fallback is a different thing.
It fires precisely when nothing qualifies, so it borrows a trip that belongs
in the grid, and removing it there leaves the user with a hero and an empty
list. Someone whose trips are all finished sees "No trips yet" under a card
of the trip they just came back from.

Split the two: only a trip that earned the hero is taken out of the grid.
Undated trips were caught by this too, and worse — getTripStatus returns
null rather than 'past' for them, so a lone dateless trip appeared in no
tab at all.

The empty state was the second half of the report. It equated "planned is
empty" with "you have no trips" and told people to create their first one.
Gate it on there being no trip at all, which is the condition the mobile
dashboard already uses.

Mobile shares useDashboard, so it is fixed along with the desktop.

Closes #1706
2026-07-26 12:45:54 +02:00
Maurice c8369bd14b fix(plugin): clear brokered session state on logout
sessionStorage outlives a logout inside the same tab, so the keys stay readable
after the next user signs in. They are namespaced by user id, which stops the new
user's plugins from reading them, but the data is still sitting in the tab — the
same reason logout already drops the appearance snapshot, the SW caches and the
user-scoped offline DB. Purge them there too.

Three things around it, from reading the handlers next to the new ones:

- The inactive-plugin purge ran before the store was updated, so a sessionStorage
  failure cost us the plugin list we had just fetched. Commit first, purge after.
- The session handlers answered without checking requestId, unlike confirm and
  geolocation — an answer the frame can never match is worth skipping.
- The mock host threw plain Errors while the real bridge rejects with .code set,
  and checked the key before the trip scope where the host does the reverse, so a
  plugin's error handling could pass its tests and still break in the app.
2026-07-26 11:48:15 +02:00
geracobo 2068133de2 feat(plugins): add quota-limited session storage API 2026-07-26 11:48:15 +02:00
geracobo cc8e4d480e fix(plugins): corrected non-sequential test numbers for PluginFrame 2026-07-26 11:48:15 +02:00
geracobo 9d8d0b1653 feat(plugins): add session storage api for plugin bridge 2026-07-26 11:48:15 +02:00
geracobo 16793761be fix(plugin): include blur booking codes on the plugin context 2026-07-26 11:48:06 +02:00
Maurice c4f4fdd794 fix(booking-import): cover the AI retry and keep its source files
The widget's new healthApi.features() call left BackgroundTasksWidget.test.tsx
red — the api/client mock never exported healthApi, so both existing tests threw
on mount. The mock now carries it, pending by default like the poll backstop, and
the retry itself finally has tests.

Writing them turned up two gaps. The retry never called saveImportFiles, so the
files only lived in memory: reloading during the retry parse left the review with
no source document to attach to the bookings it creates, unlike every import
started from the modal. And the button stayed live while the request was in
flight, so a second click started a second force-ai job on the same files.
2026-07-26 11:47:59 +02:00
Xre0uS 6854436c24 feat(booking-import): retry a failed import with AI parsing 2026-07-26 11:47:59 +02:00
Maurice eae2ade740 fix(booking-import): only resolve leg days from local_date on a prefill
Resolving every waypoint day from endpoint.local_date also changed the edit path,
where it inverts the precedence the form uses everywhere else: local_date is
denormalised and the server only restamps it in the day_id branch of insertDay,
and only when reservation_time is set. Dragging a booking to another day rewrites
day_id/end_day_id and leaves the endpoints alone, so local_date points at the old
day — and reopening the form and pressing Update silently moved the booking back.

Gate the lookup on a prefill, where there is no saved day to lose, and the edit
path behaves exactly as it did before. Same change in the mobile sheet, which
shares the seeding code and would otherwise still show empty day selectors on an
imported booking.
2026-07-26 11:47:52 +02:00
Xre0uS 68ebc700cb fix(booking-import): pre-fill leg dates from endpoint local_date 2026-07-26 11:47:52 +02:00
Maurice 828c1db489 fix(offline): start the tile prefetch once the app is idle
syncAll runs straight off the back of a successful login, while the app
is still mounting its first screen. Kicking off a bulk background
download at that exact moment is the worst possible timing even with the
prefetch throttled, so hold the tiles back until requestIdleCallback
says the browser has nothing better to do.

Files keep their old behaviour — they are a handful of requests, not
thousands.
2026-07-26 10:54:22 +02:00
Maurice 3c67afeade fix(offline): throttle the map tile prefetch
The prefetch dispatched every tile of a trip's bounding box in one
synchronous loop — up to MAX_TILES per trip. A trip spanning a few
countries enumerates thousands of tiles at zoom 10 alone, so logging in
fired ~8k no-cors requests back to back and kept them coming for half a
minute. All of them go through the Workbox fetch handler, which then has
to write a cache entry plus an expiration record per tile, so everything
the app itself requested queued up behind the burst and the UI just sat
there.

Run the queue through a small worker pool instead, and let it stop early
when the user goes offline or logs out.

Two things fall out of that:

- Tiles already in Cache Storage are now skipped before the request is
  made, so a resumed or repeated prefetch over a warm cache is nearly
  free.
- {s} has to be a pure function of the tile coordinates for that lookup
  to hit. The rotating counter gave the same tile a different host on
  every run, which also meant caching it up to four times.
2026-07-26 10:54:22 +02:00
Maurice a3e2ff9fa5 fix(mobile): offer the currency on the dashboard trip sheet
The mobile create/edit sheet never showed the currency, so every trip
started from the dashboard silently took the server default of EUR and
could only be corrected from the trip planner, which reuses TripFormModal
and does have the field.

Same control as the desktop form — searchable select over currenciesWith(),
gated on trip_edit like the rest of the sheet.
2026-07-25 22:04:03 +02:00
jubnl 8a46c56a96 test(client): dismiss the CTA-style support modal in e2e setup 2026-07-25 16:30:33 +02:00
Maurice 39d4dc2675 feat(vacay): thin the mobile person card down to the number you act on
Used, carried over, comp days and remaining all fit a desktop sidebar; on a
phone they collapsed into four scraps of 9px text fighting for one row. Only
the balance is left, as a pill tinted by how much of it there is — the rest is
still a tap away in the entitlement view.

The toolbar chip says "Company" rather than "Company Holiday": it sits beside
two other buttons and the building icon already carries the meaning. The
allowance stepper reads "Days" instead of "days / year", reusing the label the
desktop entitlement tile has always used — the year is on the pill above it.
2026-07-25 14:49:38 +02:00
Maurice 8b6a80fce0 feat(login): rebuild the branding panel and the sign-in moment around it
The panel dropped the eight feature tiles and the self-hosted line — they read
as a spec sheet on a screen whose job is to get you in. What is left is the
mark, the tagline and one line of copy, centred over the map, each with a soft
dark halo so the dots cannot eat the type. The tagline and subtitle never wrap:
they scale with the panel instead, since it is a fixed share of the viewport.

The takeoff after sign-in used to tell a different story than the screen it
came from — cloud ellipses, a lone plane, a contrail. It now picks the panel up
and escalates it: the same map, every route departing at once, the logo landing
as the network completes, then a soft hand-off into the app.
2026-07-25 14:49:38 +02:00
Maurice 45ce365920 feat(login): a living world map behind the sign-in panel
Replaces the drifting plane silhouettes with the thing TREK actually does:
routes lighting up between cities across a dot map of the world. Arcs leave a
city, a light travels the curve, the destination lands, and the arc retires to
make room for the next.

The map is drawn once into an offscreen layer and blitted per frame, so only
the handful of live arcs cost anything — no shadowBlur in the frame path. The
same component covers the sign-in moment via `variant="takeoff"`, where the
network fires as one cascade and stays lit instead of cycling.

Reduced motion gets the map as a still frame.
2026-07-25 14:49:38 +02:00
Maurice 0d9032224d feat(login): bake TREK's coastlines into a dot grid for the sign-in screen
The login screen is unauthenticated, so it cannot call the Atlas endpoint the
rest of the app uses for country geometry. This is that same admin-0 bundle,
sampled along the coastlines onto a 300x150 grid and delta-encoded — 15 KB
instead of a 28 MB fetch nobody is allowed to make yet.
2026-07-25 14:49:38 +02:00
dependabot[bot] b955998905 chore(deps-dev): bump sharp from 0.33.5 to 0.35.0 (#1690)
* chore(deps): bump dompurify from 3.4.11 to 3.4.12 (#1681)

Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.11 to 3.4.12.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.4.11...3.4.12)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.4.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump sharp from 0.33.5 to 0.35.0

Bumps [sharp](https://github.com/lovell/sharp) from 0.33.5 to 0.35.0.
- [Release notes](https://github.com/lovell/sharp/releases)
- [Commits](https://github.com/lovell/sharp/compare/v0.33.5...v0.35.0)

---
updated-dependencies:
- dependency-name: sharp
  dependency-version: 0.35.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-25 13:14:41 +02:00
Maurice b37996ec39 feat(vacay): make the toolbar show the marker it places (#1074)
The half-day button carried a ½ glyph and comp/flex a clock, neither of which
appears anywhere on the calendar. Both now show what a click actually puts on
the day: the orange dot for a half day, a hatched disc in the selected person's
colour for comp.

The digit on an all-comp cell also needed help. The hatch lets the surface
through between its stripes, so a white number sank into it — a shadow carries
the contrast now and every logged day keeps the same digit colour. Mixed days
still have a solid segment under the number and get none.
2026-07-25 12:02:16 +02:00
Maurice f1d520bbc2 feat(vacay): show comp days in the stats and fold the card away (#1074)
The server has returned comp_used since the leave type landed, but nothing
showed it — comp days simply vanished from the sidebar. They now sit beside
the tiles rather than inside the used/left arithmetic, which is the point of
them.

Carry-over is labelled from the window the server counted that row over, not
from the viewer's own setting, so a fused plan with mixed leave years does not
tell one member their days came "from 2025" when they came from a period that
started in July.

The card also folds now. It is the longest thing in the sidebar once several
people are fused in, and the state belongs to the person looking at it, so it
lives in localStorage rather than on the plan.
2026-07-25 12:02:16 +02:00
Maurice d984e09473 feat(vacay): pick a leave year and roll the grid over with it (#737)
Calendar, fiscal (month + day) or from the hire date, per user — it sits with
the plan settings but belongs to the person, so fused members each keep their
own and the grid follows whoever is looking at it.

Both grids render twelve months from the window start with year rollover
instead of a fixed Jan–Dec, and the mobile month navigation moves through slots
of that window, so stepping past December lands in January of the same period
rather than jumping back to the start of the calendar year. The month effect
keys on the window's shape rather than the settings object, which the store
replaces with an equal copy on every refresh — otherwise any sync would throw
away the month the user had open.

Known edge for a start day past the 1st: the cards are month-aligned, so the
first days of the start month are drawn but count toward the previous period.
Counting stays day-exact.
2026-07-25 12:02:16 +02:00
Maurice e1c5daeccb feat(vacay): mirror the leave-year window in the client (#737)
The server owns the arithmetic entitlements are computed with, but the grid has
to render the same twelve months and the overlays have to filter against the
same range, so the window resolves on both sides. yearWindow.ts is that mirror,
including the Feb-29 clamp and the anniversary-without-a-hire-date fallback.

The store loads the settings before the year list picks a default — with a
shifted year the current period is not necessarily the current calendar year.
Holidays are fetched per calendar year the window touches and clipped back to
what the grid draws; a school break straddling New Year comes back from both
fetches, so markers are de-duplicated per day. The label is part of that
identity, otherwise two calendars left on the same colour collapse into one.
2026-07-25 12:02:16 +02:00
Maurice 2672a7a741 test(vacay): catch the suites up with the comp/flex column
The kind column changed four things the tests still asserted the old way:
toggleEntry gained a parameter, the toolbar gained the comp modifier, shared
calendar rows carry kind, and a multi-person cell is now segment overlays
instead of one split gradient. splitBackground went with it — nothing but the
test still referenced it.

The toolbar tests picked the half-day button by index, which the new button
shifted; they select by name now so the next modifier does not break them.
2026-07-25 12:02:16 +02:00
Maurice c475e5b04c feat(vacay): comp/flex leave type (#1074) + configurable-year backend (#737)
Comp/Flex days (#1074) — complete. A second leave type that does not deduct from the vacation entitlement (flextime / overtime offset). New kind column on vacay_entries, excluded from usedDays via a CASE sum; toggleEntry carries kind through service/nest/store with change-detection; desktop and mobile get a comp toolbar toggle next to the half-day one (orthogonal, so a half comp day is free); comp segments render as a diagonal hatch of the person colour, per-segment for split days (Version B). comp_used is surfaced separately in getStats. Translated into all 23 locales.

Configurable vacation year (#737) — backend only. Per-user vacay_user_settings (calendar / fiscal / anniversary), resolveYearWindow, and usedDays / compUsedDays / getStats / carry-over now computed over the leave-year [start,end) window. 'calendar' resolves byte-identically to the old date-prefix behaviour, so nothing changes for existing users and the feature is inert until the settings UI lands. Frontend (settings selector + calendar window rollover), the settings endpoint, window-aware getEntries and holiday loading are still to do — see the PR description.
2026-07-25 12:02:16 +02:00
Konstantinos Thermos c61203e607 fix(reservations): restrict all booking dates to the trip span 2026-07-24 19:16:55 +02:00
Maurice 8769ed084e refactor(reservations): move desktop traveler assignment into the edit/create modals
The always-on card picker is gone — travelers are assigned in the booking and transport modals now, matching mobile. The card shows assigned travelers read-only as pills in the grey field box, TravelerChips is replaced by a shared TravelerPicker, and the toolbar filter gets gradient avatars with an accent ring and check badge.
2026-07-24 16:50:41 +02:00
Maurice 2a5c54c025 feat(reservations): traveler filter on the mobile bookings and transports tabs 2026-07-24 16:50:41 +02:00
Maurice bc06125d6e test(reservations): server + client coverage for traveler assignment 2026-07-24 16:50:41 +02:00
Maurice e89dae5528 feat(reservations): traveler filter + transit-card badge (desktop) and full mobile traveler UI (cards + sheet pickers) 2026-07-24 16:50:41 +02:00
Maurice 781c693434 feat(reservations): traveler badge and inline member/guest picker on booking cards (desktop) 2026-07-24 16:50:41 +02:00
Maurice 791468e5b3 fix(airtrail): also treat an endpoint-count > 2 booking as a local multi-leg shape, mirroring the server so a locally-grown chain is not mislabelled removed 2026-07-24 12:28:49 +02:00
fbnlrz 3dc9ac6ac6 fix(airtrail): stop labelling a merged multi-leg import "removed in AirTrail"
A multi-leg flight imported "as one flight with a layover" is detached
from sync by design — AirTrail has no single flight to round-trip a
layover chain to, so it is created with sync_enabled=0 (#1535). The
reservations badge collapsed every sync_enabled=0 AirTrail row into "Not
synced" with the tooltip "This flight was removed in AirTrail", which is
false for a merged booking that was never removed (#1646).

Give the multi-leg case (metadata.legs.length > 1) its own state: show
the normal AirTrail badge with a truthful tooltip explaining it stays a
one-time import. A genuinely removed single-leg flight still shows "Not
synced". Adds reservations.airtrail.layoverHint across all locales.
2026-07-24 12:28:49 +02:00
yu 617ffae351 feat(journey): add mobile quick capture with location and weather 2026-07-24 12:06:29 +02:00
Maurice 60abcf6598 feat(dashboard): drop the play circle from the beacon, keep the copy and the badge 2026-07-24 11:49:11 +02:00
Maurice 51f8afdb60 feat(dashboard): retire the 4.0.0 moment after Aug 23 — viewable-until badge + a permanent dismiss X 2026-07-24 11:49:11 +02:00
Maurice 2cec06161d fix(dashboard): drop the frame shake — the boom and the shatter carry the climax on their own 2026-07-24 11:49:11 +02:00
Maurice 0834282bfd feat(dashboard): five signature text moments + widget-matched corners
- The finale condenses out of light: ~1000 particles rise from the world and
  gather into the anthem's letterforms before the crisp DOM text takes over;
  the language cascade waits for the word to settle.
- Replaced fear-act lines decay letter by letter — the words of fear fall
  apart instead of vanishing.
- The climax boom rocks the whole frame for half a second, in sync with the
  sub impact and the border shatter.
- Hope-act lines reveal sentence by sentence, in speech rhythm.
- The standing anthem breathes — a barely-there scale and glow pulse.
- Beacon corners now use the dashboard tool widgets' --r-xl radius.
2026-07-24 11:49:11 +02:00
Maurice ffda4de77d feat(dashboard): the anthem IS the ending + beacon final polish + navbar fix
The show now ends standing: KEINE ANGST with the full 23-language cascade
stays up over the lit world, music carrying on until the traveler closes it —
no credits hand-off. Skip jumps straight to that standing frame.

Beacon: dismiss × removed, copy block centered on the canvas, the title gets
its quiet jewelry treatment — ivory-to-gold fill, a traveling sheen and a slow
breathing glow.

The overlay now portals to document.body: the sticky dashboard sidebar forms
its own stacking context, so the fixed navbar (z-200 at root) used to float
above the show regardless of the overlay's z-index.
2026-07-24 11:49:11 +02:00
Maurice 1cf1a9ca42 fix(dashboard): end screen drops the inspiration credit and the close button — anthem, three lines, done 2026-07-24 11:49:11 +02:00
Maurice 406229e701 fix(dashboard): flash words become stamped title cards — chromatic fringes, micro-jitter, hard cut, no zoom 2026-07-24 11:49:11 +02:00
Maurice d2d8b65827 fix(dashboard): soften the pivot into 'but you have traveled' + audio review fixes
The transition out of the fear act was too brutal: the silence now arrives as
a fast pull-away with the hall tail ringing out instead of a 100ms kill, the
pivot line trades its impact boom for a slow warm A1 swell (the first calm
breath), and the line itself breathes in over 3.4s.

Audio review fixes: the wind's swell LFO moves to a series tremolo stage so
the release can actually silence it before the cut (it used to warble through
the silence and click on stop); the heartbeat's noise layer becomes the
intended high-passed click instead of a dull lowpassed thud; dispose() pulls
the master to zero before the deferred context close so leaving the show
cannot pop.
2026-07-24 11:49:11 +02:00
Maurice ba2582f9c8 feat(dashboard): beacon goes cinema-poster — centered copy, iconic play circle
Copy sits centered on the night-world canvas over a corner-pooling scrim; the
title grows monumental tracking, a gold sheen and a soft glow; the text CTA
becomes a slowly pulsing golden play circle that fills on hover. The version
eyebrow and the red live dot are gone — title, one line, play. Nothing else.
2026-07-24 11:49:11 +02:00
Maurice 920628d63c feat(dashboard): show polish — readable lines, gliding arcs, a real hall
- Lines get a layered soft glow plus a radial pool of darkness under the text
  zone, so they hold up over the burning border map without a hard box.
- Arc heads now interpolate fractionally along the curve with ease-out growth
  (the whole-segment quantization read as stutter), in the show and the beacon
  teaser alike.
- The score gains a convolution reverb with a procedurally generated impulse
  response; the fear act trades raw saws for an organic bed (sine sub,
  drifting fifth, breathing noise rumble, a semitone grind in the dread act),
  the shimmer runs through a feedback echo, impact tails bloom in the hall,
  the heartbeat stays close and dry.
2026-07-24 11:49:11 +02:00
Maurice 10bfad97da feat(dashboard): cinematic beacon — living arc teaser, sheen title, scrim
The beacon is now a dark cinema card: a small canvas paints a breathing
night-world with golden travel arcs on loop, colored glows drift over it, the
title carries a gold sheen sweep and the copy sits on a bottom scrim. Core
colors ride inline so no dashboard widget CSS can wash the card out again.
2026-07-24 11:49:11 +02:00
Maurice c541c158df fix(dashboard): keep the generic line up through the stats slot for travelers with no places yet 2026-07-24 11:49:11 +02:00
Maurice aec541c573 feat(dashboard): make the KEINE ANGST show hit harder
Visual: city lights now come alive in the opening and die one by one while the
borders burn; three staccato words strobe with the heartbeat before the cut;
at the climax the borders no longer fade — they shatter into drifting sparks.
The traveler's own places ignite gold in the release act, with their real
numbers in the line ('{p} places. {c} countries. And not once did the world
hurt you.'). All heavy geometry is decimated to a vertex budget and pre-baked
into offscreen layers, so the frame path is a handful of drawImage calls — the
per-frame border stroke that dropped the show to ~1 fps is gone.

Sound: heartbeat with a click transient, wind textures, a doubled noise riser,
cue-driven sub-bass impacts and a gliding D–A–f#m–E progression with a sub
root under the anthem instead of the static pad.

Also: dashboard FAB hidden behind the show, suspended-AudioContext recovery
(Safari), tab-hidden pause for clock and audio, aborted geo fetch on close,
valid beacon markup (dismiss as sibling button), RTL-aware beacon, real
reduced-motion static frame, and native-speaker fixes for nl/sv/br/fr/it/es/
tr/gr copy.
2026-07-24 11:49:11 +02:00
Maurice c6d96872ae feat(dashboard): 4.0.0 release moment — KEINE ANGST
A one-release statement against racism, hatred and fascism, told the way TREK
knows the world: through travel. A pulsing beacon in the dashboard sidebar
(desktop only, dismissible) opens a ~80s full-screen show — the world as a map
of dots whose borders burn red and then dissolve under a growing web of travel
connections, driven by a fully procedural Web Audio score (heartbeat, drone,
A-major release; no audio assets, no copyrighted material) and hard-cut lines
in all 23 TREK languages, closing on the anthem cascade and a credit to the
song that sparked it (Danger Dan — Keine Angst).

Self-contained in components/FourZero (own copy dictionary, no shared i18n
churn); wired into the dashboard with two marked lines. Removing the folder
plus those two lines retires the moment after the release.
2026-07-24 11:49:11 +02:00
Konstantinos Thermos e100c52676 fix(maps): localize map POI pin titles to the user's language 2026-07-24 11:36:30 +02:00