Phase 7: SvelteKit + Capacitor Frontend

phase-mcd-tracker-7-sveltekit Phase

phase
Goal: Production SvelteKit + Capacitor app. Playground HTML/CSS promoted directly. All screens functional with real data. Deployed to cluster (web) + Capacitor-ready for iOS.
Owner: Dev agent (mechanical promotion) + Lucas (visual verification)
Repo: forgejo_admin/mcd-tracker-app
Depends on: Phase 6 (playground design locked), Phase 5a/5c/5d (backend gaps filled)

Scope

Mechanical promotion from playground per sop-frontend-experiment. The HTML/CSS is the source of truth — app.css used directly, HTML pasted into Svelte templates, hardcoded data replaced with {data.field}.

SvelteKit Routes (from playground alignment)

Playground Route Data (client-side fetch) Interactivity
index.html / None (public landing) Sign In → keycloak.login(). Create Account → keycloak.register(). In production, Keycloak handles auth UI — these are themed Keycloak pages.
signin.html /signin None — redirect to Keycloak keycloak.login() redirect. Page only exists as Keycloak theme reference. On iOS: WebView opens Keycloak directly.
register.html /register None — redirect to Keycloak keycloak.register() redirect. Same as signin — Keycloak theme reference.
home.html /home GET /dashboard + GET /stats (XP/level) XP banner (level, codes redeemed). Location cards sorted by proximity with 5-day expiry warnings. Near Me GPS. Search + sort. Tap location → /locations/{id}. Floating scan FAB.
location-detail.html /locations/{id} GET /locations/{id}/slots + GET /locations/{id}/codes Slot progress bar. Active codes with 5-day countdown + pulsing redeem button. Redeemed codes. Slot timeline (all 5 slots). Cashier overlay with receipt photo. Scan Receipt pre-selects this location.
scan.html /scan GET /locations/nearby {#if currentStep} 4-step wizard. Permission card. Camera (Capacitor). Survey code confirm. Copy + open survey (Capacitor browser/clipboard). BOGO code entry + location picker. Save → /scan/success.
save-success.html /scan/success None (data passed from scan) Code saved celebration. 5-day expiry warning. Slot status update. +1 XP animation. 'Use Now' → cashier overlay (direct, no navigation). 'Done' → /home.
redeem-success.html /redeem/success None (data passed from redeem action) Free Food celebration. Lifetime count (#13). 'What did you get?' chip picker (Big Mac, McChicken, etc. → redeemed_item field). +2 XP animation. Slot reopen date. 'Scan Another' or 'Done'.
history.html /history GET /receipts + GET /codes → merge + GET /stats Lifetime stats bar (scanned/earned/redeemed/expired). Date-grouped timeline. Color-coded events (scan=blue, earn=green, redeem=amber, expire=red). Tap event → /history/{id}.
event-detail.html /history/{id} GET /codes/{id} or GET /receipts/{id} with related data Full event context: type, timestamp, location (tappable), BOGO code details, survey code, receipt photo, slot impact, XP earned. Cashier overlay (state-aware: active → 'Mark as Redeemed', redeemed → 'Already Redeemed' with item + date).

Steps

  • Create mcd-tracker-app repo on Forgejo. SvelteKit scaffold (adapter-static, ssr: false for Capacitor SPA mode).
  • Copy app.css from playground → src/app.css (direct, no changes).
  • Promote 7 content pages: copy HTML → Svelte template, replace hardcoded data with {data.field}. Data loading via client-side fetch() in onMount (NOT +page.server.ts). Pages: / (landing), /home, /locations/[id], /scan, /scan/success, /redeem/success, /history, /history/[id].
  • Create 2 redirect stubs: /signinkeycloak.login(), /registerkeycloak.register(). These are not full page promotions — they call the Keycloak JS method and redirect. No HTML copy needed.
  • Auth: keycloak-js client-side OIDC (NOT Auth.js). Public client + PKCE. Initialize in +layout.svelte (onMount). Auth guard blocks rendering until resolved. / is public, all other routes require auth. See project-capacitor-mobile auth-decision for full config (redirect URIs, token management, platform detection).
  • Backend prereqs: COMPLETED (PR #17, merged 2026-03-16). GET /stats endpoint (XP, levels, lifetime counts) + redeemed_item field on redeem endpoint. 144 tests. No backend work needed.
  • Capacitor init: npx cap init, npx cap add ios, configure capacitor.config.ts.
  • Capacitor plugins: @capacitor/camera, @capacitor/geolocation, @capacitor/clipboard, @capacitor/browser, @capacitor/app.
  • Service onboarding: add mcd-tracker-app to pal-e-services + pal-e-deployments (namespace, Harbor project, ArgoCD app, kustomize overlay, Tailscale funnel).
  • Deploy to cluster, verify on phone — full flow: landing → auth → home → location → scan → save → use now → redeem → history → event detail.

Deliverables

  • PR #2 merged (2026-03-16): SvelteKit + Capacitor scaffold
  • 10 routes promoted from playground HTML (verbatim CSS, Svelte templates with data bindings)
  • keycloak-js PKCE auth with auth guard in +layout.svelte
  • src/lib/keycloak.js (OIDC wrapper) + src/lib/api.js (Bearer token fetch) + src/lib/overlay.svelte.js (shared overlay state)
  • 41 tests (Vitest + @testing-library/svelte): build, routes, modules, components, overlay
  • Dockerfile (nginx serving SPA) + .woodpecker.yaml (test + build-and-push)
  • Capacitor config initialized (capacitor.config.ts, iOS added)
  • PR #5 merged (2026-03-17): Docker Compose local dev stack. 4-service stack (postgres, keycloak, api, app). dev/realm-export.json with PKCE client + 3 test users. dev/seed-data.py test data seeder. .env.example. api.js and keycloak.js now use VITE_* env vars with production fallbacks. QA nit fixed: /healthz endpoint.
  • Remaining: docker compose up local validation, service onboarding (pal-e-services + pal-e-deployments), deploy to cluster, verify on phone
  • plan-mcd-tracker — parent plan
  • phase-mcd-tracker-6-playground — design source of truth
  • sop-frontend-experiment — promotion procedure
  • project-capacitor-mobile — Capacitor patterns + plugin catalog
  • convention-frontend-css — CSS rules