Phase 0 scaffolding for the frontend: SvelteKit + adapter-static in SPA mode
(fallback index.html, ssr disabled in the root layout — no Node process in
production, Caddy serves build/ directly per docs/PLAN.md), a login page and
auth store backed by /api/v1/auth/{login,me,logout}, an installable-PWA shell
(hand-written manifest, iOS meta/safe-area handling, an install-onboarding
banner), and a Dockerfile whose only job is to produce a buildable /app/build
artifact for deploy/ to consume.
Service worker notes, since the wiring isn't obvious from the diff:
- injectManifest, not generateSW: the caching policy needs to say "never
cache /api/*", which generateSW's declarative config can't express as
precisely as hand-written Workbox routes can.
- Uses the base `vite-plugin-pwa` plugin, not `@vite-pwa/sveltekit`'s
SvelteKit-specific wrapper. That wrapper's injectManifest build expects
SvelteKit's own built-in src/service-worker.{js,ts} convention to have
already transpiled the file — but that native build only permits importing
SvelteKit's own three virtual modules and hard-rejects `workbox-*` imports,
which our SW needs. The base plugin bundles src/service-worker.ts directly
instead, which works. SvelteKit's native service-worker convention is
explicitly disabled (`kit.files.serviceWorker` pointed at a path that
doesn't exist) so the two builds can't collide and silently clobber each
other's output — they do, if both are left enabled, and the failure mode is
silent (the SW builds fine, just precaches nothing).
- workbox-core/precaching/routing/strategies had to be added as direct
devDependencies even though workbox-build depends on them — pnpm doesn't
hoist transitive deps into the top-level node_modules, so the SW bundle
step couldn't resolve them otherwise.
- The SPA fallback index.html doesn't exist yet at service-worker-build time
(adapter-static writes it after all Vite plugins finish), so it can't be
glob-hashed into the precache manifest normally. It gets a synthetic
manifest entry instead, revisioned by a per-build-invocation timestamp
(see the swIndexRevision comment in vite.config.ts) so the cached shell
still invalidates correctly on every deploy.
Full rationale for each choice is inline as comments in vite.config.ts and
apps/web/README.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
bike-app
A self-hosted cycling app: syncs rides from a Bryton Rider 650, tracks mileage like Strava, and adds a spare-parts inventory and a maintenance record with mileage-milestone reminders.
Status: planning complete, no code written yet.
Why
Today the Rider 650 syncs over Bluetooth to the Bryton Active phone app, which forwards to Strava — but Active has no background sync, so you have to remember to open the app. Research found a better path that removes the phone entirely:
ride ends -> Rider 650 joins home Wi-Fi (Main Menu -> Data Sync)
-> uploads to Bryton cloud
-> this app's poller fetches the ORIGINAL FIT file
-> rides, wear tracking, and push reminders
That's also higher fidelity than the current route — Strava's API can only ever return smoothed streams, never the original file.
Docs
| File | What's in it |
|---|---|
docs/PLAN.md |
The full implementation plan: stack, schema, ingestion pipeline, auth, notifications, phased roadmap, CI/CD, risks, verification |
docs/RESEARCH.md |
Raw findings: the Bryton cloud protocol (endpoints, headers, auth), FIT library comparisons, maintenance interval tables, self-hostable geo services, Gitea Actions gotchas |
docs/DECISIONS.md |
Every decision taken, what was rejected, and why |
Planned stack
Python 3.12 / FastAPI / SQLAlchemy async / PostgreSQL 16 + PostGIS, procrastinate for jobs,
SvelteKit static SPA as an installable PWA, MapLibre GL, all behind Caddy in Docker Compose.
Source control and CI in self-hosted Gitea with an act_runner on the same box.
Four containers in v1, under 2GB RAM.
Next steps
- Verify on the Rider 650: does
Main Menu -> Data Syncupload automatically on joining Wi-Fi, or only on manual trigger? This determines how completely the phone leaves the loop. - Plug the 650 in over USB and
ls -Rthe mounted volume to confirm the real.fitpath (documented asBryton/Activities/, but worth confirming). - Grab a real
.fitfile from it and run it throughfitdecode— Bryton's encoder is not Garmin's, and the schema should be checked against reality before it's written. - Then Phase 0: scaffolding and CI (see the roadmap in
docs/PLAN.md).