All projects

Sbb lost found

Report something lost on a train while you are still on it, bound to the actual journey, so staff can search before the vehicle reaches the depot.

demo

Problem

The current lost and found process in Swiss public transport has a low recovery rate due to the time delay between reporting a lost item and the train staff receiving the notification

Solution

A real-time platform that allows passengers to report lost items and train staff to receive notifications while the item is still on board, increasing the recovery rate

Mission

To increase the recovery rate of lost items in Swiss public transport by providing a real-time platform for passengers to report lost items and for train staff to receive notifications.

Vision

To become the standard solution for lost and found in Swiss public transport, making it easier for passengers to recover their lost items and reducing the workload for train staff.

Technical context

The stack has not been written down yet.

Investigate this project with Loki →

Sign in to ask about architecture, evidence, delivery risks and next steps in your workspace.

Changelog

  1. September 24, 2026

    Re-confirmed the timetable blocker is still live and traced a local-only verify crash to leftover gitignored build output from the removed microservice layout; no code change.

  2. September 22, 2026

    Re-confirmed the pending blocker is still live today (2026-09-22) and stopped without picking a task, per Pick rule 0 and the backward brake; no code change.

  3. September 20, 2026

    Re-verified the pending blocker still reproduces on the live site today and stopped the autopilot loop rather than manufacture work; no code change.

  4. September 19, 2026

    Rule 0 fired — surfaced the pending blocker (live demo timetable expired, needs a human-run prod GTFS import) and picked no task; confirmed PR #86 merged and the tree matches origin/master.

  5. September 18, 2026

    Closed four stale TODO.md launch/security items that the #63 rebuild and #82 had already fixed, updated the matching AGENTS.md sections, and recorded (with live evidence) that the demo's hand-loaded timetable ran out; PR #86 has CI green.

  6. September 8, 2026

    Fixed four live UI elements that rendered nothing, including the Fundservice entry point in the profile — the primary way into this product's whole feature. Tailwind cannot put an alpha channel into a bare var(--token), and every colour in this config is one, so every opacity-modifier utility written against them emitted no rule at all: measured on the deployed site, the Fundservice button's computed backgroundColor was rgba(0, 0, 0, 0) and the production stylesheet contained zero `brand\/` or `app-success\/` rules. Also dead: the TripCard priority chip (fill and border) and the tinted circle in the report confirmation. Fixed with derived tokens (--brand-surface, --brand-surface-hover, --brand-edge, --app-success-surface) built from their base colour with color-mix, so a tenant still sets only --brand and the tints follow it. The same change closed a white-label bug: the report modal used raw bg-red-50, so under the neutral blue Nordbahn brand the selected category/location rendered PINK (measured live: blue border rgb(11,79,143) around a rgb(254,242,242) fill) — now the same --brand-surface as every other highlight. Guarded by a new check in contrast.test.ts that rejects the modifier syntax outright; that guard found a fourth instance in LostItemModal I had missed by reading, and I confirmed all four new utilities actually emit by grepping the built stylesheet rather than trusting the green suite. Merged as d63cd5c (#60), CI and Deploy green, verified live after deploy: the CTA and the selected state both render the brand at 10% (color(srgb 0.043 0.310 0.561 / 0.1)), / and /staff serve 200. Separately exercised the full four-step demo flow against production (report -> crew sees it as Neu/Dringend -> crew answers Gefunden with a note -> passenger's record updates live cross-tab, no reload): working, after #58's six dependency bumps and #59's token changes. Corrected my own over-classification from last turn: the "T1 brand-red 4.29:1 on milk" finding only affects the sbb build variant, which is NOT deployed — the live site serves data-tenant="nordbahn" (#0b4f8f, 7.68:1 on milk), so it is latent, not a live defect. Two small live findings left unfixed and recorded in the roadmap with reasons: /favicon.ico 404s on every page load (the correct icon is tenant-dependent — a design decision, not a file to guess at), and the deprecated apple-mobile-web-app-capable meta tag.

  7. September 8, 2026

    Fixed a production defect where two of the four status badges rendered white text on no background. Root cause was not a colour choice: `lib/` was missing from Tailwind's content globs, and this codebase deliberately keeps UI class names in config (lib/labels.ts owns NOTIFICATION_STATUS_CONFIG), so `bg-app-granite` and `bg-amber-500` — which appear nowhere else — were never emitted. Confirmed against the DEPLOYED stylesheet before the fix (bg-app-granite 0, bg-amber-500 0, while bg-app-success/bg-brand survived only because app/ also uses them), so "Nicht gefunden" and "In Bearbeitung" were unreadable for real users. Fixed three genuine contrast failures alongside it: --app-success #00973B was 3.83:1 in both roles it plays (white-on-green in the toast and "Gefunden" badge, green-on-white on the demo figures) → #007A30 at 5.49:1; the amber badge was white-on-amber at 2.15:1 → charcoal on --app-warning at 8.49:1, which also removed the only raw Tailwind palette colour in the config; --app-smoke (3.32:1 white / 3.07:1 milk) was used as text in MyReports and the .input-app placeholder → --app-granite. Added lib/__tests__/contrast.test.ts, which reads tokens out of globals.css and computes ratios instead of holding a hand-maintained list: it checks text against both surfaces (--app-metal passes on white at 4.54:1 and fails on milk at 4.20:1), checks badge pairs for every tenant, rejects badge colours that are not tokens, and asserts Tailwind scans every directory the test scans. All five checks were verified by reintroducing the bug each one guards and watching it fail. Merged as fe2211d (#59), CI and Deploy green, verified live: https://sbb.orangecat.ch/ and /staff serve 200 and the production stylesheet now carries all four badge backgrounds, --app-success:#007a30, and placeholder{color:var(--app-granite)}. Two findings deliberately left for the founder and recorded in the roadmap: text-brand is 4.29:1 on milk under the sbb tenant (trademark colour — a design decision about where brand text may sit, not a value I can change), and bg-red-50 in LostItemModal hardcodes a red selection tint that ignores the active tenant's blue brand.

  8. September 8, 2026

    Unstuck the dependency pipeline, which had been hard-blocked for three days. PR #49 (5 dependency bumps) went red on 2026-09-05 from the `@parcel/watcher` allowBuilds gap; #55 fixed that on 2026-09-06, but #49 stayed red because the follow-up `@dependabot rebase` was answered "already up-to-date with master" at 2026-09-06T04:08:51Z — the exact second #55 merged, so the bot evaluated the pre-fix master and lost the race by one second. Confirmed the diagnosis before acting by rebasing #49 onto current master locally (clean, no conflicts) and running `pnpm run verify` green, proving only the *remote* branch was stale. Escalated to `@dependabot recreate` (the remedy the bot's own reply names and the earlier session never used); it rebuilt against current master as PR #58 — six updates rather than five — the fleet auto-merge sweep merged it as 9ef2c40, CI and Deploy both green, https://sbb.orangecat.ch/ and /staff serving 200 with the current build confirmed in the served chunk (`Meine Verlustmeldungen` present in /_next/static/chunks/10-21148d325d396d40.js). Re-ran `pnpm run verify` on master at 9ef2c40 with the full six-update set: green. No open PRs remain. Root cause and the "rebase != recreate" lesson recorded in the roadmap, along with a founder-decision proposal to teach the fleet-owned sweep to unstick this class (deliberately not made unilaterally — it is another repo and would change behaviour for all 22 callers).

  9. September 6, 2026

    Fast-forwarded stale local master to origin/master (8 commits behind, including the npm→pnpm migration and multi-tenant white-label refactor — discarded a local package-lock.json diff that was npm-install noise on a file the pnpm migration already deleted). Full `pnpm run verify` passed clean on current HEAD. Root-8 sweep found a genuinely broken adjacent flow: PR #49 (dependabot jest bump to 30.5.1) has failed CI 3 times with `ERR_PNPM_IGNORED_BUILDS` because jest-haste-map 30.5.1 pulls in a new `@parcel/watcher` native-build dependency that isn't in pnpm-workspace.yaml's allowBuilds allowlist, hard-blocking the repo's dependency auto-merge pipeline. Diagnosed the exact transitive dependency via the PR's lockfile, reproduced the failure and the fix in a scratch copy (confirmed install fails without the allowlist entry, succeeds with it), then added `'@parcel/watcher': true` to allowBuilds on branch fix/allow-parcel-watcher-build, pushed, opened PR #55 (https://github.com/bitbaum/sbb-lost-found/pull/55) with CI green, and commented `@dependabot rebase` on PR #49 so it picks up the fix once #55 merges.

  10. September 3, 2026

    Diagnosed and closed out a stale dispatch (previous run had timed out on branch fix/wire-frontend-to-api-hooks at ae1e0f8, but that content had already shipped as PR #40/86a4b03 six commits back — confirmed byte-identical diff, fast-forwarded local master to origin/master at ed98e9d). Ran the full `npm run verify` gate (format:check, typecheck, lint, tests, tenant-SSOT check) against current HEAD: it initially failed at format:check because this box's NODE_ENV=production silently stripped devDependencies (prettier missing), a known recurring box artifact — re-ran `npm install --include=dev` and verify then passed clean end-to-end across every workspace. Did a targeted rule-8 sweep for adjacent-broken-things (SSOT hex-color grep, swallowed catches, orphaned components/hooks, MyReports/useDemoReports wiring) and found the codebase fully wired with no half-finished flows; also confirmed CI green and the live deploy (sbb.orangecat.ch/ and /staff) both serving 200 at current HEAD. No code change was needed or made this run.

  11. September 1, 2026

    Gave the passenger app a persistent record of its own reports — "Meine Verlustmeldungen" in the Reisen tab lists what was reported, where, when, the status badge from the same NOTIFICATION_STATUS_CONFIG the crew cards use, and the crew's note when there is one, reading the same handover the toast reads (no second copy of the answer) and updating live; before this the crew's answer existed only as a four-second toast, so a passenger whose screen was asleep had no way to learn the outcome. Verified in a browser against the dev server with no backend (reported "Kopfhörer, weiss, in Ladecase", answered Gefunden with a note from /staff in a second tab, badge and note appeared without a reload, and the record survived a full reload of /), then merged as ed98e9d (#45) with CI and Deploy green, https://sbb.orangecat.ch/ and /staff serving 200 and the strings "Meine Verlustmeldungen" and "Rückmeldung des Personals" present in the deployed chunk.

  12. September 1, 2026

    Closed the last scripted hop of the demo flow — the crew's answer now reaches the passenger: lib/demo-bus.ts runs both ways (answerReport writes the answer onto the notification it answers, so there is one key, one shape and no second copy to drift), /staff publishes when it answers a handed-over report, / announces it as a success toast for "Gefunden" and an honest Fundbüro message for "Nicht gefunden" instead of the old "Personal sucht aktiv…" that claimed a search already called off, answers already in storage seed the seen set so a reload does not replay old news, and the three passenger-facing strings moved to lib/labels.ts; answering a handed-over report now also survives a reload of /staff, which it did not before. Verified in a browser against the dev server with no backend running (three reports on /, answered from /staff in a second tab, both outcome toasts observed in the passenger tab), then merged as 7cbc326 (#44) with CI and Deploy green, https://sbb.orangecat.ch/ and /staff serving 200 and the new label present in the deployed bundle.

  13. September 1, 2026

    Closed the demo's headline flow — /staff was staging a notification hardcoded into the page on a 5s timer, so whatever a visitor reported on / the crew saw "Schwarze Laptop-Tasche, Wagen 7, Platz 45"; lib/demo-bus.ts now carries the actual report to the crew view over same-origin localStorage + the storage event (published by LostItemModal only when the reporting service did not take the report, so a real backend never gets a duplicate), the alert dialog renders the arriving notification instead of a literal, the staged fallback moved to mock-data.createDemoIncomingNotification() derived from mockActiveTrip and stands down once a real report arrives, and .playwright-mcp/ is now ignored by prettier so a browser pass cannot redden `verify`. Verified end-to-end in a browser against the dev server with no backend running, then merged as 651ce50 (#43) with CI and Deploy green and https://sbb.orangecat.ch/staff serving 200.

  14. August 30, 2026

    Wired app/page.tsx, app/staff/page.tsx and LostItemModal to the existing lib/hooks/useApi.ts hooks (useCurrentTrip, useTrips, useReportLostItem, useDriverNotificationsApi) instead of each page hand-rolling its own setTimeout-based mock simulation — the hooks/api layer CLAUDE.md documents as "hooks handle fallback gracefully" had zero call sites before this. Fixing the wiring surfaced and fixed a real dormant bug in useDriverNotificationsApi.updateNotification (optimistic update no-op'd on the very first call because it read from `localData`, which starts null and only that function ever sets it). Verified end-to-end with a headless-browser pass against the dev server (backend not running, so real requests fail over to mock as designed): passenger report-lost flow and staff found/not-found buttons both confirmed working, before/after screenshots taken to confirm the updateNotification fix. Branch fix/wire-frontend-to-api-hooks pushed, PR opened: https://github.com/bitbaum/sbb-lost-found/pull/40

  15. August 28, 2026

    Ran the rule-8 scan on the flow adjacent to last run's change and confirmed the report-a-lost-item flow does close — but found it closes because it never touches the API layer: lib/api.ts, lib/hooks/useApi.ts and lib/hooks/useWebSocket.ts (734 lines) form a closed import loop with zero call sites outside lib/, both pages read lib/mock-data directly and simulate with setTimeout, so the "hooks handle fallback gracefully" claim in CLAUDE.md:209 and the dossier's "real-time architecture" describe a path that does not run; recorded the finding and the delete-vs-wire fork in the roadmap, and corrected the 9010971 handoff's overstated severity (the localhost URL shipped as inert config, not live traffic — no requests were ever fired).

  16. August 28, 2026

    The deployed demo at sbb.orangecat.ch shipped baseUrl:"http://localhost:3001" inlined in its client bundle, so every visitor's browser aimed its data calls at port 3001 of their own machine and the page only filled in because useApiWithFallback caught the failure and swapped in mocks — root cause was one value with three disagreeing definitions (next.config.js 3001/3001 won at build time over lib/config.ts 3000/3003 and a second process.env read in lib/api.ts) plus a demo flag written `env === 'true' || true` that was always true and had no reader; removed the next.config.js env block (Next inlines NEXT_PUBLIC_* already), moved dev values to .env.development which never reaches a production build, made demo mode a real function of the environment, wired the previously-dead DEMO_MODE constant to gate request() and connectWebSocket(), and locked it with 14 tests — merged as 9010971, Deploy green (#33141467160), and the live bundles now contain 0 localhost URLs with baseUrl resolving to "" (root 200).

  17. August 28, 2026

    Auto-merge on master had been failing on every scheduled run since 2026-08-27 20:24Z because the GitHub account rename maonakamoto -> catomean broke the `uses:` reference to the central reusable workflow (runs list by file path with zero jobs — an unresolvable `uses:`); the fix existed only as three commits stranded on the already-merged ci/central-automerge branch, so I rebuilt them onto master as PR #17, verified green, merged (8c77476), and confirmed live: Auto-merge now runs green again at 8c77476 (#33140857527, #33140880424) and Deploy — which carried the identical stale owner in deploy.yml and was a latent outage on the next push — completed green (#33140824615), site root 200.

  18. August 22, 2026

    Investigated the repeat-timeout escalation. Root cause of the confusion: local branch `fix/pwa-manifest-tenant-ssot` (HEAD 1c510e0) was checked out but was a stale duplicate of already-merged PR #15 (master had moved to it via squash-merge as 70792a2, then one commit further to f9776b4/#16) — cherry-check confirmed patch-equivalence, so force-deleted the dead branch and fast-forwarded to origin/master. Then ran the full `npm run verify` pipeline step-by-step end to end (types build, lint --workspaces + frontend, typecheck --workspaces + frontend, reporting-service jest, frontend jest, tenant SSOT check) to positively confirm none of it hangs — every step completed in seconds, all green. No blocker file, no roadmap file, 0 TODO/FIXME/HACK, no silently-swallowed catch blocks found in a targeted scan.

  19. August 18, 2026

    Repeat dispatch fired with no new user input and identical repo state (HEAD still 9140d95, clean tree, no stash) as the immediately-prior cycle that already concluded nothing high-impact remains — re-ran the full investigation would only re-derive the same answer, so this cycle confirms state is unchanged rather than re-scanning from scratch.

  20. August 18, 2026

    Re-verified the repo-state fix from the prior cycle held (still on master, fast-forward synced with origin, working tree clean, no stash) and swept for new T0/T1 candidates — tsc clean, 0 TODOs, last 3 commits span 4 different dirs (no gravity well), and services/matching's placeholder ILIKE+category scoring is a working (if simple) implementation, not a broken stub. Found nothing new; no commits made this cycle.

Roadmap and changelog come from this project's canonical development record, read October 29, 2026.