mcd-tracker

mcd-tracker forgejo

Notes

Plan 1
  • Plan: mcd-tracker plan-mcd-tracker

    Plan: mcd-tracker

    Vision

    McDonald's BOGO coupon tracker — track survey codes across locations, know how many slots remain, see when slots reopen (rolling 30-day window). Multi-user from day one. Enterprise-grade: Keycloak auth, CI/CD pipelines, App Store distribution.

    Meta-vision: Full-lifecycle DORA Elite proof. Validate the pal-e-platform dev experience end-to-end: devops → backend → frontend → mobile. Every SOP exercised, every pipeline stage proven, every architectural pattern stress-tested. The app is the vehicle; the dev process is the deliverable.

    Projects & Repos Touched

    Project/Repo Platform Role in this plan
    mcd-tracker (this project) Forgejo Parent project — all repos below
    mcd-tracker-api Forgejo FastAPI backend (Phase 2-5)
    mcd-tracker-playground Forgejo HTML/CSS prototypes (Phase 6)
    mcd-tracker-app Forgejo SvelteKit frontend (Phase 7)
    mcd-tracker-ios Forgejo Swift/SwiftUI iOS app (Phase 9-10)
    pal-e-services Forgejo Service onboarding terraform (Phase 1, 7)
    pal-e-deployments Forgejo Kustomize overlays (Phase 1, 3, 7)

    Context

    First greenfield project built entirely on the hardened pal-e-platform. Previous projects (basketball-api, westside-app, pal-e-docs) were onboarded incrementally as the platform matured. mcd-tracker is the first to follow every SOP from project creation through App Store submission.

    What's already done:

    • [x] Project created in pal-e-docs (project-mcd-tracker)
    • [x] Project page created with user stories, architecture stubs, repo table
    • [x] Plan created (this note)
    • [ ] Board created
    • [ ] Architecture diagram notes created

    Previous Plan

    None. First plan for this project.

    Depends On

    • plan-pal-e-platform — platform must be stable (it is: 16/19 phases completed)
    • plan-pal-e-agency — SOPs and conventions must exist (they do: all 13 phases resolved)

    DecisionRationale
    Python FastAPI + Postgres (CNPG sidecar)Matches basketball-api pattern exactly. Proven stack.
    Dedicated Keycloak realm mcd-trackerSeparate user base. Enterprise-ready multi-tenancy.
    Rolling 30-day window, not calendar monthMcDonald's enforces per-code. Each usage starts an independent 30-day timer.
    DevOps first, code secondProduction namespace + CI + ArgoCD ready before first line of code.
    Receipt-first workflow (2026-03-16)Camera → OCR → survey → BOGO code → save. Two codes (survey + BOGO). Receipt is a first-class entity with photo proof.
    Capacitor for iOS, not Swift (2026-03-16)SvelteKit + Capacitor = one codebase for web + iOS. Native camera/GPS via plugins. Eliminates separate Swift app. Same CSS, same components, same data layer.
    Frontend: playground → SvelteKit+Capacitor (2 stages)Was 3 stages (playground → SvelteKit → Swift). Capacitor collapses SvelteKit+iOS into one stage.
    MacBook Air M1 as Woodpecker agentStill needed for Xcode builds. Capacitor generates Xcode project, agent runs npx cap sync && xcodebuild.

    Decision Rationale
    Python FastAPI + Postgres (CNPG sidecar) Matches basketball-api pattern exactly. Proven stack.
    Dedicated Keycloak realm mcd-tracker Separate user base. Enterprise-ready multi-tenancy.
    Rolling 30-day window, not calendar month McDonald's enforces per-code. Each usage starts an independent 30-day timer.
    5-day redemption window (2026-03-16) BOGO codes expire 5 days after earning. Two timers per code: 5-day use-it-or-lose-it + 30-day slot window.
    DevOps first, code second Production namespace + CI + ArgoCD ready before first line of code.
    Receipt-first workflow (2026-03-16) Camera → OCR → survey → BOGO code → save. Two codes (survey + BOGO). Receipt is a first-class entity with photo proof.
    Capacitor for iOS, not Swift (2026-03-16) SvelteKit + Capacitor = one codebase for web + iOS. Eliminates separate Swift app.
    Frontend: playground → SvelteKit+Capacitor (2 stages) Was 3 stages. Capacitor collapses SvelteKit+iOS into one stage.
    MacBook Air M1 as Woodpecker agent Needed for Xcode builds. Capacitor generates Xcode project, agent runs npx cap sync && xcodebuild.
    keycloak-js for SPA auth, not Auth.js (2026-03-16) Auth.js requires SSR. SPA mode for Capacitor means client-side OIDC only. See project-capacitor-mobile auth-decision.
    Location-centric UX, not flat code list (2026-03-16) Codes belong to locations. Home = location dashboard. Task-oriented, not data-dump.
    Gamification: XP + levels, not $ saved (2026-03-16) Codes redeemed tally + levels. Dollar amounts unreliable (prices change). Celebrate usage, not savings.
    Receipt intelligence (2026-03-16) Receipts contain rich data beyond survey codes. Future: item analysis, spending patterns, optimal visit timing. Scoped as Phase 14.

    Phases

    Phase 1: Service Onboarding (COMPLETED)

    Goal: Production namespace, Harbor project, ArgoCD app, and kustomize overlay ready — before any code exists.

    Owner: Dev agent

    Repos: forgejo_admin/pal-e-services, forgejo_admin/pal-e-deployments

    Forgejo Issue: TBD

    Steps:

    1. Add mcd-tracker entry to pal-e-services/terraform/k3s.tfvars var.services map:
      mcd-tracker = {
        forgejo_repo = "forgejo_admin/mcd-tracker-api"
        image_repo   = "mcd-tracker/api"
        port         = 8000
        funnel       = true
        source_repo  = "forgejo_admin/pal-e-deployments"
        source_path  = "overlays/mcd-tracker/prod"
      }
    2. tofu plan -lock=false → verify 6-7 new resources (namespace, Harbor project, robot accounts, pull secret, ArgoCD app, funnel ingress)
    3. tofu apply -lock=false
    4. Create kustomize overlay: pal-e-deployments/overlays/mcd-tracker/prod/
      • kustomization.yaml — base ref + rename patches + images transformer
      • deployment-patch.yaml — env vars (DATABASE_URL, KEYCLOAK_REALM_URL)

    Deliverables: pending

    Phase 2: Backend Scaffold + First Deploy (COMPLETED)

    Goal: FastAPI skeleton running in prod — health endpoint responding, CI green, ArgoCD syncing.

    Owner: Dev agent

    Repo: forgejo_admin/mcd-tracker-api

    Forgejo Issue: TBD

    Steps:

    1. Create mcd-tracker-api repo on Forgejo
    2. Scaffold FastAPI app (following basketball-api pattern):
      • src/mcd_tracker_api/main.py — FastAPI app + lifespan
      • src/mcd_tracker_api/config.py — Pydantic Settings with MCD_TRACKER_ prefix
      • src/mcd_tracker_api/routes/health.py/healthz endpoint
      • Dockerfile — multi-stage Python build
      • .woodpecker.yaml — test + build-and-push (kaniko)
      • pyproject.toml
    3. Activate Woodpecker → add Harbor secrets (harbor_username, harbor_password)
    4. Push → green pipeline → Harbor image → ArgoCD deploys
    5. Verify: curl https://mcd-tracker.tail5b443a.ts.net/healthz

    Deliverables: pending

    Phase 3: Data Model + Postgres (COMPLETED)

    Goal: Postgres sidecar deployed, SQLAlchemy models defined, Alembic migrations running, DB-backed health check.

    Owner: Dev agent

    Repos: forgejo_admin/mcd-tracker-api, forgejo_admin/pal-e-deployments

    Forgejo Issue: TBD

    Steps:

    1. Add Postgres sidecar to kustomize overlay (postgres.yaml — Postgres 16-alpine, 1Gi PVC, ClusterIP service)
    2. Add src/mcd_tracker_api/database.py — engine, SessionLocal, get_db()
    3. Add src/mcd_tracker_api/models.py — SQLAlchemy models:
      • Location — id, name, address, city, state, lat/lng (nullable), user_id (FK)
      • CouponUsage — id, location_id (FK), user_id (FK), code (string), used_at (timestamp), redeemed (bool), redeemed_at (nullable timestamp)
    4. Init Alembic, create first migration
    5. Update health endpoint to verify DB connection
    6. Deploy — verify migration runs on startup

    Data Model Notes:

    • No separate User table — user identity comes from Keycloak JWT (sub claim). Store keycloak_sub as FK-like reference on Location and CouponUsage.
    • Rolling window query: SELECT COUNT(*) FROM coupon_usage WHERE location_id = ? AND user_id = ? AND used_at > NOW() - INTERVAL '30 days'
    • Slot reopens: MIN(used_at) + 30 days from the 5 active usages at a location

    Deliverables: pending

    Phase 4: Keycloak Realm + Auth (COMPLETED)

    Goal: Dedicated Keycloak realm with user/admin roles, JWT validation in API, protected endpoints.

    Owner: Dev agent + manual Keycloak admin

    Repos: forgejo_admin/mcd-tracker-api

    Forgejo Issue: TBD

    Steps:

    1. Create mcd-tracker realm in Keycloak admin console
    2. Create realm roles: user, admin
    3. Create OIDC client: mcd-tracker-app (for SvelteKit) and mcd-tracker-ios (for Swift, public client + PKCE)
    4. Add src/mcd_tracker_api/auth.py — JWKS fetch, JWT decode, User dataclass, require_role dependency factory (copy basketball-api pattern)
    5. Add config: MCD_TRACKER_KEYCLOAK_REALM_URL
    6. Update deployment-patch.yaml with Keycloak env var
    7. Create test user + admin user in realm

    Deliverables: pending

    Phase 5: Core API Endpoints + Integration Tests (COMPLETED)

    Goal: Full CRUD API with rolling window logic, integration tests, deployed to prod.

    Owner: Dev agent

    Repo: forgejo_admin/mcd-tracker-api

    Forgejo Issue: TBD

    Steps:

    1. Routes:
      • POST /locations — save a McDonald's location (name, address)
      • GET /locations — list user's saved locations
      • POST /locations/{id}/codes — log a coupon code at a location
      • GET /locations/{id}/codes — list codes at a location
      • PATCH /codes/{id}/redeem — mark code as redeemed
      • GET /locations/{id}/slots — availability: slots remaining (0-5), next reopen date
      • GET /dashboard — all locations with slot status for current user
      • GET /admin/stats — aggregate stats (admin only)
    2. Rolling window logic:
      • Count active codes per location per user where used_at > NOW() - 30 days
      • Available slots = 5 - active_count
      • Next reopen = oldest active code's used_at + 30 days
    3. Integration tests (pytest, real Postgres):
      • CRUD operations
      • Window boundary: code at exactly 30 days ago should free a slot
      • 5-code limit enforcement
      • Multi-user isolation
      • Admin stats endpoint
    4. Deploy and verify via curl

    Deliverables: pending

    Phase 6: Frontend Playground (IN PROGRESS — CSS consolidated, @-comment specs added, pending phone approval)

    Goal: HTML/CSS mockups of all screens, verified on phone, approved by Lucas.

    Owner: Main session (Lucas iterates directly)

    Repo: forgejo_admin/mcd-tracker-playground

    Forgejo Issue: mcd-tracker-playground #4 (closed)

    Steps:

    1. Decide: folder experiment in html-playground or repo experiment (mcd-tracker-playground)
    2. Create screens (vanilla HTML/CSS, hardcoded data):
      • Dashboard — location cards showing slots remaining (e.g., "3 of 5 available"), next reopen countdown
      • Add Code — form: select location, enter code, submit
      • Location List — saved locations with add/edit
      • History — chronological list of codes with status (active/expired/redeemed)
    3. Mobile-first design — this is a phone app at heart
    4. Lucas verifies on phone via Tailscale funnel
    5. Iterate until design locks

    Deliverables: PR #5 merged (mcd-tracker-playground #4): consolidated ~1,000 lines of inline CSS into app.css, added @-comment integration specs to all 12 HTML files. SOP: sop-capacitor-mobile-lifecycle created. Pending: Lucas phone approval (Gate 1).

    Phase 7: SvelteKit + Capacitor Frontend (IN PROGRESS — scaffold deployed, Docker Compose merged, pending local validation)

    Goal: Production SvelteKit web app promoted from playground, deployed to prod with Keycloak auth.

    Owner: Dev agent

    Repos: forgejo_admin/mcd-tracker-app, forgejo_admin/pal-e-services, forgejo_admin/pal-e-deployments

    Forgejo Issue: TBD

    Steps:

    1. Create mcd-tracker-app repo on Forgejo
    2. SvelteKit scaffold with Auth.js + Keycloak OIDC (copy westside-app pattern)
    3. Direct port from playground: copy CSS → scoped styles, copy HTML → Svelte templates, replace hardcoded data with {data.foo} bindings
    4. Pages: / (dashboard), /add (log code), /locations (manage), /history, /admin (stats)
    5. Service onboarding for frontend: add mcd-tracker-app to k3s.tfvars + kustomize overlay
    6. Deploy, verify on phone

    Deliverables: pending

    Phase 8: Mac Woodpecker Agent (NOT STARTED)

    Goal: MacBook Air M1 running as a Woodpecker CI agent with local backend, capable of executing xcodebuild.

    Owner: Main session (manual Mac setup)

    Repo: forgejo_admin/pal-e-platform (agent config docs)

    Forgejo Issue: TBD

    Steps:

    1. Install Xcode on MacBook Air M1 (App Store)
    2. Install Woodpecker agent binary (brew install woodpecker-ci/tap/woodpecker-agent or direct download)
    3. Configure agent with local backend:
      WOODPECKER_SERVER=https://woodpecker.tail5b443a.ts.net
      WOODPECKER_AGENT_SECRET=<from k3s.tfvars>
      WOODPECKER_BACKEND=local
      WOODPECKER_FILTER_LABELS=platform=darwin
    4. Create launchd plist for auto-start on boot
    5. Install Fastlane: brew install fastlane
    6. Verify: agent appears in Woodpecker UI, test pipeline with labels: [platform: darwin] runs on Mac

    Deliverables: pending

    Phase 9: Capacitor iOS Build + Native Plugins (NOT STARTED — replaces Swift app)

    Goal: SwiftUI app with same screens as SvelteKit, Keycloak OIDC auth, building on Mac CI agent.

    Owner: Dev agent (code on archbox) + Mac agent (builds)

    Repo: forgejo_admin/mcd-tracker-ios

    Forgejo Issue: TBD

    Steps:

    1. Create mcd-tracker-ios repo on Forgejo
    2. SwiftUI app scaffold:
      • Dashboard view — location cards, slot counts, reopen countdowns
      • Add Code view — location picker, code input
      • Locations view — saved locations management
      • History view — code list with filters
    3. Keycloak OIDC auth via ASWebAuthenticationSession (public client + PKCE — mcd-tracker-ios client created in Phase 4)
    4. API client layer — same endpoints as SvelteKit, JWT Bearer auth
    5. .woodpecker.yaml with labels: [platform: darwin] — targets Mac agent
    6. Pipeline: xcodebuild testxcodebuild archive → export .ipa

    Deliverables: pending

    Phase 10: App Store Submission (NOT STARTED)

    Goal: mcd-tracker live on the App Store. Full CI/CD from git push to TestFlight to production.

    Owner: Main session + Dev agent

    Repo: forgejo_admin/mcd-tracker-ios

    Forgejo Issue: TBD

    Steps:

    1. Enroll in Apple Developer Program ($99/yr)
    2. Fastlane setup:
      • fastlane match — manage signing certificates + provisioning profiles
      • fastlane deliver — App Store metadata, screenshots
      • fastlane pilot — TestFlight uploads
    3. CI pipeline addition: after xcodebuild archive, run fastlane pilot upload
    4. TestFlight beta distribution — Lucas + testers validate
    5. App Store submission — metadata, screenshots, privacy policy, review
    6. Post-approval: pipeline does git push → test → build → TestFlight → (manual promote to App Store)

    Deliverables: pending

    Phase 11: Analytics + Dual-Channel Tracking (NOT STARTED)

    Goal: Track user behavior across web + iOS. Know how many people use the app, what they do, and which channel they prefer.

    Owner: Dev agent + Betty Sue

    Repos: mcd-tracker-app, pal-e-services (Umami onboarding)

    Depends on: Phase 10 (App Store live)

    Scope:

    • Deploy Umami or Plausible to cluster (pal-e-services onboarding — same pattern as any service)
    • Embed Umami tracking script in mcd-tracker-app (works on both web and iOS/Capacitor)
    • Add POST /events endpoint to mcd-tracker-api — custom product events (scanned receipt, redeemed code, slot checked)
    • App Store Connect already provides download/impression/crash metrics for free
    • Build a simple admin dashboard showing: active users, web vs iOS split, most popular actions, most used locations

    Phase 12: Monetization (NOT STARTED — when ~100 active users)

    Goal: Freemium model with paywalls. Free tier for casual use, paid for power users.

    Owner: Dev agent + Lucas

    Repos: mcd-tracker-api, mcd-tracker-app

    Depends on: Phase 11 (analytics — need to understand usage patterns before monetizing)

    Scope:

    • Define free vs paid tiers (e.g., free: 3 locations, paid: unlimited + push notifications + stats)
    • Stripe integration for web payments (proven pattern from basketball-api subscriptions)
    • Apple In-App Purchases for iOS (Capacitor plugin: RevenueCat or @capgo/capacitor-purchases)
    • RevenueCat for unified subscription management across web + iOS
    • Feature flags in API: check subscription tier before allowing premium features
    • Landing page + App Store description updated with pricing

    Phase 13: Gamification + XP System (NOT STARTED)

    Goal: Celebrate usage. XP system with levels, lifetime code tally, mascot character with context-aware reactions.

    Owner: Lucas (mascot design) + Dev agent (implementation)

    Repos: mcd-tracker-api, mcd-tracker-app

    Depends on: Phase 7 (frontend must exist)

    Scope:

    • API: GET /stats endpoint — lifetime codes redeemed, current level, XP progress, streak days
    • Level system: Level 1 (0-5 redeemed) → Level 2 (6-15) → etc. Titles like "BOGO Beginner" → "Free Food Fanatic" → "Coupon Legend"
    • Home page XP banner above location list ("Level 3 BOGO Hunter · 12 codes redeemed")
    • Mascot character — Lottie animations, ~8 states: idle, happy, excited, worried (code expiring), sleeping (no activity), celebrating (just redeemed), nudging (slot opened), waving (first visit)
    • Mascot appears on: home banner, redemption overlay, empty states, push notifications (future)
    • Design mascot as separate creative task — Lucas drives, AI tools (motchi.art, Lottie Tools) assist

    Phase 14: Receipt Intelligence (NOT STARTED)

    Goal: Extract maximum value from receipt photos. Receipts contain rich data beyond survey codes — items purchased, prices, timestamps, store numbers, payment methods.

    Owner: Dev agent + Lucas (product decisions)

    Repos: mcd-tracker-api

    Depends on: Phase 5b (OCR — deferred, currently manual code entry)

    Scope:

    • OCR pipeline: Tesseract (self-hosted) or Cloud Vision (API) — decision deferred to this phase
    • Receipt parsing: extract line items, total, tax, store number, timestamp, payment method
    • Insights: most ordered items, average spend per visit, spending by location, visit frequency patterns
    • Optimal timing: when are BOGO codes most likely to have open slots at each location?
    • Data model: ReceiptItem table (receipt_id FK, item_name, price, quantity)
    • Privacy-first: all data stays on our infra, user can delete anytime
    • Feed gamification: "You've tried 23 different menu items" or "Your most ordered: Big Mac (8x)"

    Key Files

    Phase File Repo Change
    1 terraform/k3s.tfvars pal-e-services Add mcd-tracker to var.services
    1 overlays/mcd-tracker/prod/* pal-e-deployments New kustomize overlay
    2 src/mcd_tracker_api/* mcd-tracker-api FastAPI scaffold
    3 src/mcd_tracker_api/models.py mcd-tracker-api Location + CouponUsage models
    3 overlays/mcd-tracker/prod/postgres.yaml pal-e-deployments Postgres sidecar
    4 src/mcd_tracker_api/auth.py mcd-tracker-api Keycloak JWT validation
    5 src/mcd_tracker_api/routes/* mcd-tracker-api All API routes
    5 tests/* mcd-tracker-api Integration tests
    8 com.woodpecker.agent.plist MacBook Air M1 launchd agent config
    9 mcd-tracker-ios/* mcd-tracker-ios SwiftUI app
    10 fastlane/* mcd-tracker-ios Fastlane config

    Verification

    • [ ] Phase 1: kubectl get ns mcd-tracker exists, ArgoCD app visible
    • [ ] Phase 2: curl https://mcd-tracker.tail5b443a.ts.net/healthz returns 200
    • [ ] Phase 3: Health endpoint shows DB connected, Alembic migration applied
    • [ ] Phase 4: Unauthenticated request returns 401, authenticated returns 200
    • [ ] Phase 5: Log 6 codes at one location → 6th rejected (limit 5). Wait 30 days (or mock time) → slot reopens. All integration tests green.
    • [ ] Phase 6: Lucas approves all screens on phone
    • [ ] Phase 7: Web app live, login works, can log and redeem codes
    • [ ] Phase 8: Mac agent visible in Woodpecker UI, test pipeline completes
    • [ ] Phase 9: iOS app builds on Mac CI, runs on physical iPhone
    • [ ] Phase 10: App live on App Store, pipeline: push → TestFlight in <10 min

    • QA nit (PR #13): Plan slug mismatch. Cosmetic.
    • QA observation (PR #13): postgres.yaml duplicated across overlays. Future debt.
    • Bug found: bug-board-item-labels-array — MCP tool labels type mismatch.
    • Lesson (Phase 2): K8s {SERVICE}_PORT collides with Pydantic Settings. Use server_port.
    • QA nits (PR #15, 4): failure counter, awk edge case, namespace-awareness, image tag.
    • Lesson (Phase 1a): bitnami/kubectl Go 1.25 x509 with k3s. Use alpine/k8s:1.32.4.
    • QA nits (PR #3, 7): .gitignore, CI Postgres (fixed), migration swallowed, dual engine, test creds, driver://, no updated_at.
    • Process (Phase 3): QA approved without CI tests running. Feedback: feedback_qa_ci_blockers.
    • QA blocker overridden (PR #5): auth.py DRY — copy-and-own correct for independent microservices.
    • QA nits (PR #5, 5): default URL, mock layering, noqa, global keyword, expired-token test.
    • QA nits (PR #7, 6): Pydantic field validation, hardcoded total_slots, admin query, magic number, active-only filter, test count.
    • QA nits (PR #9, 6): photo_path leaking in API response, hardcoded image/jpeg content-type, missing PVC for /data/uploads/receipts (needed before photos work in prod — track as 5a-ii), plan slug, 2 style items.
    • QA nits (PR #17, 2): Pydantic max_length hardening on string schemas. Level name mismatch ("Free Food Fanatic" vs "Free Food Fan").
    • QA blocker (PR #2 mcd-tracker-app, round 1): Zero test coverage. Fixed: 41 tests added (Vitest + @testing-library/svelte). Approved on re-review.
    • QA nits (PR #2, 6): Hardcoded success page data (scaffold placeholder), non-functional sort dropdown, @html usage (audit needed), missing error feedback UX, redundant build test in CI, test coverage gaps on complex pages. All tracked for Phase 7a.

    QA nits and deferred work will be tracked here as phases complete.

    • project-mcd-tracker — project page
    • service-onboarding-sop — Phase 1 procedure
    • convention-kustomize-overlay — Phase 1 overlay pattern
    • sop-frontend-experiment — Phase 6 playground workflow
    • plan-pal-e-platform — platform this project runs on
    • plan-pal-e-agency — SOPs and conventions this project follows
Board 1
Doc 5
  • mcd-tracker API observability

    What

    Replace the stub /metrics endpoint (hardcoded up 1) with real Prometheus instrumentation. Copy the pal-e-docs pattern exactly.

    Scope

    • Add prometheus-fastapi-instrumentator to dependencies
    • Wire in main.py — auto-instruments all endpoints (request rate, latency histograms, error rates by status code)
    • Remove manual /metrics stub in routes/health.py — the instrumentator handles it
    • Add custom business counters in route handlers:
      • mcd_receipts_uploaded_total
      • mcd_codes_saved_total
      • mcd_codes_redeemed_total
      • mcd_nearby_queries_total
    • Existing ServiceMonitor already scrapes /metrics on port 8000 every 30s — no deployment changes needed

    Pattern to follow

    pal-e-docs/src/pal_e_docs/main.py lines 60-63:

    Instrumentator(
        should_ignore_untemplated=True,
        excluded_handlers=["/healthz", "/metrics"],
    ).instrument(app)

    Depends on

    Nothing. Can start immediately.

  • Wire real camera + manual code entry

    What

    The scan flow in scan/+page.svelte is 100% mocked — fake camera, hardcoded survey code, simulated OCR delay. Replace with real functionality:

    • Wire @capacitor/camera for photo capture (web fallback: <input type="file" capture="camera">)
    • Upload photo to API via POST /receipts (endpoint exists from Phase 5a)
    • Manual survey code entry instead of fake OCR extraction
    • Real GPS-based location picker (already partially wired on home page)

    Why

    Can't validate the app end-to-end with mock data. Need real data flowing through scan → save → history → redeem.

    Scope

    Phase 7 work. Not doing receipt intelligence / OCR (that's Phase 14). Just making the flow functional with real camera + manual entry.

  • TODO: mcd-tracker Grafana dashboard + alert rules todo-mcd-observability-dashboard

    mcd-tracker Grafana dashboard + alerts

    What

    Golden signals dashboard for mcd-tracker in Grafana, plus basic alert rules in Alertmanager. Clone the pal-e-docs pattern.

    Scope

    • Clone pal-e-docs-golden-signals.jsonmcd-tracker-golden-signals.json
    • Update label selectors (namespace, service name)
    • Add business metrics panels: receipts uploaded, codes saved/redeemed, nearby queries
    • Deploy via pal-e-platform terraform (ConfigMap in monitoring namespace)
    • Alert rules: API down 5min, error rate >5% for 5min, p95 latency >2s

    Depends on

    todo-mcd-observability-api — needs the instrumented metrics endpoint deployed first.

    Repos touched

    pal-e-platform (terraform/dashboards/ + alert rules)

  • Smart proximity — query Postgres instead of Overpass

    What

    Replace the Overpass-dependent /locations/nearby endpoint with a Postgres spatial query against the pre-seeded mcdonalds_locations table. Haversine function already exists in services/geo.py.

    Why

    With locations in Postgres, "Near Me" becomes: SELECT * FROM mcdonalds_locations ORDER BY haversine(lat, lng, $1, $2) LIMIT 10. Sub-millisecond. Always works. No external API dependency.

    Approach

    • New endpoint or refactored /locations/nearby — queries mcdonalds_locations table
    • Haversine distance calculated in Python (already exists) or SQL
    • Still cross-references user's saved locations for slot info (existing match logic)
    • Frontend: no changes needed if response shape stays the same
    • Overpass client becomes optional — only used for seed/refresh, never runtime

    Depends on

    todo-mcd-preseed-locations — needs the table populated first.

  • Pre-seed McDonald's locations into Postgres

    What

    One-time batch load of McDonald's locations from Overpass/OSM into a mcdonalds_locations table in Postgres. This eliminates runtime dependency on the Overpass API, which is unreliable (504 timeouts on 25km radius queries).

    Why

    McDonald's locations don't change. Querying a public API in real-time for static data is architecturally wrong. Pre-seeding means the nearby feature always works, sub-millisecond, no external dependency.

    Approach

    • New table: mcdonalds_locations (osm_id, name, address, city, state, lat, lng, created_at, updated_at)
    • Alembic migration to create the table
    • Management script or one-time seed: batch Overpass query for Denver metro (or wider — full US is ~13k rows, trivial)
    • Optional: periodic refresh (monthly cron) to catch new openings/closures

    Depends on

    Nothing — can start immediately. Prerequisite for todo-mcd-smart-proximity.

Project Page 1
  • mcd-tracker project-mcd-tracker

    mcd-tracker

    Vision

    McDonald's BOGO coupon tracker. McDonald's receipt surveys give you a code redeemable for a buy-one-get-one sandwich. Most locations enforce a limit of 5 codes per month per location (rolling 30-day window per code used). This app tracks usage across locations so users always know how many codes they have left and when slots reopen.

    Meta-purpose: Full-lifecycle DORA Elite proof — validate the pal-e-platform development experience from devops → backend → frontend → mobile. Exercise every SOP, every pipeline stage, every architectural pattern. The app is the vehicle; the dev process is the deliverable.

    User Stories

    Who uses the expense tracker, what they need, and how we measure success. mcd-tracker is a personal tool for Lucas to track McDonald's receipt codes and spending.

    Role Story Success Metric story:X key
    Superuser (Lucas) I can deploy and manage the mcd-tracker stack (API + app) through the standard platform pipeline. Deploys via ArgoCD. Zero manual kubectl for routine operations. story:superuser-deploy
    User (Lucas) I can log a receipt code from my phone in <10 seconds — scan or type the code, confirm, done. Receipt entry takes <3 taps. Works on mobile (Capacitor). story:user-log-code
    User (Lucas) I can see my spending history, daily/weekly/monthly totals, and whether I'm on budget. Dashboard loads in <2s. Budget vs actual visible at a glance. story:user-view-spending
    User (Lucas) I can see which codes have been redeemed and which are still available, so I don't waste a trip. Code status (redeemed/available) is accurate and up-to-date. story:user-track-codes

    Active: Plan: mcd-tracker (plan-mcd-tracker) — 10 phases, all NOT STARTED. Workflow: devops → backend → frontend → mobile.

    Not yet created. Will be created when work begins. Workflow order:

    1. DevOps first — service onboarding via pal-e-services + k8s manifests in pal-e-deployments. Production namespace ready before code is written.
    2. Backend second — FastAPI + Postgres (CNPG sidecar pattern). Alembic migrations. Keycloak JWT auth. Integration tests. Deployed to prod.
    3. Frontend third — HTML/CSS in mcd-tracker-playground (pal-e-playground project). Once design locks, promote to SvelteKit in mcd-tracker-app. Deploy to prod.
    4. Mobile last — Swift/SwiftUI iOS app (mcd-tracker-ios). Same API. Woodpecker CI on Mac agent (local backend). Fastlane → TestFlight → App Store.

    board-mcd-tracker — Continuous kanban. Backlog → Todo → Next Up → In Progress → Done.

    Not yet created. Will be created when plan is created.

    SvelteKit app scaffolded + merged (2026-03-16). PR #2 on mcd-tracker-app: 10 routes, keycloak-js PKCE auth, 41 tests, Capacitor config, Dockerfile, CI pipeline. Playground live (12 pages). Backend: 13 endpoints, 144 tests. Next: service onboarding → deploy → verify on phone. Then Phase 7a (E2E tests) and Phase 10a (security hardening) before App Store.

    Milestones

    2026-03-16: Backend complete + audited. 12 endpoints, 130 tests, full repo audit passed. Playground live with receipt-first workflow.

    Architecture

    Three views of the system (all created and updated to reflect receipt-first workflow + Capacitor):

    1. Domain Model (arch-domain-mcd-tracker) — User, Location, Receipt, CouponUsage. Two codes: survey code (receipt) + BOGO code (coupon). Rolling 30-day window.
    2. Data Flow (arch-dataflow-mcd-tracker) — 7 flows: scan receipt + OCR, complete survey + save BOGO, redeem at counter, check availability, auto-detect location (GPS/Overpass), slot limit rejection, Keycloak auth.
    3. Deployment (arch-deployment-mcd-tracker) — archbox (dev) → Forgejo → Woodpecker (Linux + Mac agents) → Harbor → ArgoCD → k3s. Capacitor wraps SvelteKit for iOS. Dual-channel: web + App Store.

    • Stack: Python FastAPI + Postgres (CNPG sidecar) + Keycloak OIDC — matches basketball-api pattern exactly.
    • Auth: Dedicated Keycloak realm mcd-tracker. Roles: user, admin.
    • Rolling window: 5 codes per location per 30-day rolling window. Each code usage starts an independent 30-day timer.
    • Receipt-first workflow: Camera → OCR → survey → BOGO code → save. Two codes: survey code (from receipt) and BOGO code (from survey). Receipt entity links them.
    • Capacitor for iOS (not Swift): SvelteKit + Capacitor = one codebase for web + iOS. Native camera (@capacitor/camera) and GPS (@capacitor/geolocation) via plugins. Same code runs on both platforms — native APIs on iOS, browser fallback on web. Eliminates the need for a separate Swift app.
    • iOS pipeline: MacBook Air M1 as Woodpecker agent (local backend). Capacitor generates Xcode project. npx cap sync && xcodebuild → Fastlane → TestFlight → App Store.
    • Frontend progression: Playground (HTML/CSS) → SvelteKit (production web + Capacitor iOS). Two stages, not three.

    • Stack: Python FastAPI + Postgres (CNPG sidecar) + Keycloak OIDC — matches basketball-api pattern exactly.
    • Auth: Dedicated Keycloak realm mcd-tracker. Roles: user, admin.
    • Rolling window: 5 codes per location per 30-day rolling window. Each code usage starts an independent 30-day timer. When a timer expires, that slot reopens. Not calendar-month based.
    • iOS pipeline: MacBook Air M1 as Woodpecker agent (local backend). Swift code written on archbox (Linux), pushed to Forgejo, Mac builds via xcodebuild + Fastlane. $99/yr Apple Developer Program required for App Store.
    • Frontend progression: Playground (HTML/CSS) → SvelteKit (production web) → Swift (native iOS). Each stage validates UX before the next begins.

    RepoPlatformRoleStatus
    mcd-tracker-apiForgejoFastAPI backendLIVE — 8 endpoints, 58 tests
    mcd-tracker-playgroundForgejoHTML/CSS prototypes (served at playground.tail5b443a.ts.net/mcd-tracker/)Scaffolded — 7 pages, design iteration in progress
    mcd-tracker-appForgejoSvelteKit + Capacitor (web + iOS from one codebase). Native camera/GPS via Capacitor plugins.Not created

    Removed: mcd-tracker-swift — Capacitor wraps the SvelteKit app as a native iOS app. No separate Swift codebase needed.

    Repo Platform Role Status
    mcd-tracker-api Forgejo FastAPI backend Not created
    mcd-tracker-playground Forgejo HTML/CSS design experiments Not created
    mcd-tracker-app Forgejo SvelteKit frontend (promoted from playground) Not created
    mcd-tracker-ios Forgejo Swift/SwiftUI iOS app Not created

    Infrastructure

    Component Details
    Dev machine archbox — Arch Linux, 1.8T NVMe, x86_64
    iOS build server MacBook Air M1 — Woodpecker agent (local backend), Xcode, Fastlane, Capacitor builds (npx cap sync + xcodebuild)
    k8s namespace mcd-tracker (via pal-e-services onboarding)
    Container registry Harbor — mcd-tracker/api, mcd-tracker/app
    Keycloak realm mcd-tracker (separate from westside-basketball). App client is public (keycloak-js + PKCE, not Auth.js).
    CI Woodpecker — Linux agent (test/build/push) + Mac agent (npx cap sync + xcodebuild + Fastlane)
    GitOps ArgoCD — pal-e-deployments/overlays/mcd-tracker/prod

    Inbox

    Untriaged TODOs — discovered work awaiting scoping into the plan.

    Slug Summary Discovered
    empty
Review 1
  • Verdict: READY

    Template Completeness

    • [x] Lineage — links to plan-mcd-tracker Phase 7, references upstream playground issue
    • [x] Repo — forgejo_admin/pal-e-deployments
    • [x] User Story — clear: developer reviewing playground prototypes on phone
    • [x] Context — explains current state (pal-e-playground at /, westside at /westside/), the gap (404 for /mcd-tracker/), and why it matters (blocks playground gate)
    • [x] File Targets — two files to modify, four files explicitly excluded with reasons
    • [x] Acceptance Criteria — four verifiable curl checks
    • [x] Test Expectations — kubectl apply, rollout status, manual curl verification
    • [x] Constraints — references existing westside pattern to follow
    • [x] Checklist — standard PR/verify/no-unrelated-changes
    • [x] Related — links to mcd-tracker project, upstream issue #6, downstream issue #10

    File Targets

    • [x] overlays/playground/prod/deployment.yaml — verified: exists, currently has pal-e-playground and westside-playground hostPath volumes + volumeMounts. Adding a third volume follows the established pattern exactly.
    • [x] overlays/playground/prod/configmap.yaml — verified: exists, currently has location /westside/ block with alias /srv/westside-playground/. Adding location /mcd-tracker/ with alias /srv/mcd-tracker-playground/ follows the same pattern.
    • [x] ~/mcd-tracker-playground/ — verified: directory exists on host with 14 HTML files including index.html and scan.html (both referenced in acceptance criteria).
    • [x] Hub landing page (~/pal-e-playground/index.html) — verified: already links to /mcd-tracker/ at line 215. This is the broken link the ticket fixes.

    Repo Placement

    Correct. The Forgejo issue is filed on forgejo_admin/pal-e-deployments and both file targets live in that repo's overlays/playground/prod/ directory. Single-repo change, no cross-repo coordination needed.

    Dependencies

    • Upstream: ~/mcd-tracker-playground directory must exist on the k3s node. Verified present with content.
    • Downstream: mcd-tracker-playground #6 (scan page redesign) is blocked on this mount per the issue body. mcd-tracker-app #10 is downstream of the playground gate.
    • Board: No blockers on the board. Phase 7 (SvelteKit + Capacitor) is in_progress — this issue supports it. No other board items reference this issue or compete for the same files.

    Acceptance Criteria

    All four criteria are directly testable via curl after deployment. The test commands are real and verifiable. One minor note: the criterion for /mcd-tracker/ returning 200 will work because index.html exists in the playground directory and the nginx try_files directive will serve it. No gaps found.

    Blast Radius

    • Minimal. The change adds a new nginx location block and volume mount — purely additive. No existing configuration is modified.
    • Existing routes unaffected. The /, /guide/, and /westside/ routes remain unchanged. Acceptance criteria explicitly verifies these still return 200.
    • No similar bug elsewhere. The pattern is already proven with the westside mount. No other playground repos are waiting for mounts (checked the hub landing page — only pal-e, westside, and mcd-tracker are linked).
    • capacitor-dev overlay: Already has a /mcd-tracker/ location (proxy_pass for dev). This is a separate deployment in a different namespace — no conflict.

    Recommendation

    No action needed. Scope is clean, file targets verified, pattern is proven. This is a 1-point ticket and the scope matches — two file edits following an established pattern. Ready for agent execution.

Phase 20
  • Phase 7a: Location Discovery + Navigation phase-mcd-tracker-7a-location-discovery

    Goal: New users see nearby McDonald's immediately. Each location has a profile page with notes and a "Guide me there" button that opens native maps.

    Owner: Dev agent (API + SvelteKit) + Lucas (UX iteration via dev overlay)

    Repo: forgejo_admin/mcd-tracker-api, forgejo_admin/mcd-tracker-app, forgejo_admin/mcd-tracker-playground

    Depends on: phase-mcd-tracker-7-sveltekit (dev overlay operational)

    Scope

    User Stories

    Key Story Acceptance
    discover I open the app and see McDonald's near me, even with zero saved locations Home page shows nearby results from GET /locations/nearby on mount
    profile I tap a McDonald's and see its full profile — address, distance, my codes, my notes Location detail page renders for both saved and unsaved locations
    navigate I tap "Guide me there" and get turn-by-turn directions Opens Apple Maps (iOS) or Google Maps (web) via deep link
    notes I leave a personal note on a location (e.g. "ice cream machine works here") CRUD notes on location detail page

    API (mcd-tracker-api)

    • GET /locations/nearby — already exists. May need to work for users with no saved locations (currently returns saved_match data).
    • POST /locations/{id}/notes — NEW. Create a personal note. Fields: text, created_at.
    • GET /locations/{id}/notes — NEW. List user's notes for a location.
    • DELETE /locations/{id}/notes/{note_id} — NEW. Remove a note.
    • DB: new location_notes table (id, keycloak_sub, location_id, text, created_at)

    Playground (mcd-tracker-playground)

    • Update home.html — add "Nearby" section showing discovered (unsaved) locations
    • Update location-detail.html — add notes section, "Guide me there" button
    • Update @-comment specs to reflect new API endpoints and state

    SvelteKit (mcd-tracker-app)

    • Update /home — fetch nearby on mount (GPS → GET /locations/nearby), show results
    • Update /locations/[id] — render notes, "Guide me there" link
    • Notes CRUD on location detail page

    "Guide me there" — no API key needed

    Deep link, not an API call. Capacitor detects platform:

    const mapsUrl = Capacitor.isNativePlatform()
      ? `https://maps.apple.com/?daddr=${lat},${lng}`
      : `https://www.google.com/maps/dir/?api=1&destination=${lat},${lng}`;
    window.open(mapsUrl, '_blank');
    

    Deliverables

    PR #8 merged (2026-03-17): GPS-based location discovery on home page + "Guide me there" on location detail. Home page calls GET /locations/nearby on mount, renders results as location cards with distance in miles. Location detail has Google Maps deep link button.
    Remaining: Location notes (CRUD), profile page for unsaved locations, Apple Maps detection for iOS.

    • plan-mcd-tracker — parent plan
    • phase-mcd-tracker-7-sveltekit — depends on dev overlay
    • sop-capacitor-mobile-lifecycle — follows Vite-on-host local dev pattern
  • Phase 7: SvelteKit + Capacitor Frontend phase-mcd-tracker-7-sveltekit

    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

    1. Create mcd-tracker-app repo on Forgejo. SvelteKit scaffold (adapter-static, ssr: false for Capacitor SPA mode).
    2. Copy app.css from playground → src/app.css (direct, no changes).
    3. 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].
    4. 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.
    5. 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).
    6. 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.
    7. Capacitor init: npx cap init, npx cap add ios, configure capacitor.config.ts.
    8. Capacitor plugins: @capacitor/camera, @capacitor/geolocation, @capacitor/clipboard, @capacitor/browser, @capacitor/app.
    9. Service onboarding: add mcd-tracker-app to pal-e-services + pal-e-deployments (namespace, Harbor project, ArgoCD app, kustomize overlay, Tailscale funnel).
    10. 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
  • Goal: Full local dev stack in Docker Compose with Tailscale funnel — test auth, API, data binding, and Capacitor web fallbacks locally in seconds, not minutes via CI.

    Owner: Dev agent + Lucas (verification)

    Repo: mcd-tracker-app (docker-compose.yml lives here)

    Depends on: Phase 7 (app scaffold exists)

    Scope

    Lesson (2026-03-16): Phase 7 skipped local dev entirely. Six production hotfixes — wrong port, wrong registry, wrong realm, check-sso redirect, no CORS, client not public. All caught in seconds with docker compose up. The rule: if it doesn't work locally, it doesn't get pushed.

    Docker Compose services:

    • app — SvelteKit dev server (npm run dev), hot reload, port 5173. VITE_API_URL=http://api:8000
    • api — mcd-tracker-api (FastAPI), port 8000. DATABASE_URL pointing to local postgres. KEYCLOAK_REALM_URL pointing to local keycloak.
    • postgres — Postgres 16-alpine, seeded with test data (locations, codes, receipts)
    • keycloak — Keycloak dev mode, pre-configured mcd-tracker realm with mcd-tracker-app public client, test users, redirect URIs for localhost

    Environment variable:

    • Add VITE_API_URL to src/lib/api.js: const API_BASE = import.meta.env.VITE_API_URL || 'https://mcd-tracker.tail5b443a.ts.net'
    • Docker Compose sets it to http://localhost:8000 (or http://api:8000 for container-to-container)
    • Production build uses the default (no env var needed)

    Tailscale funnel:

    • Official URL like mcd-dev.tail5b443a.ts.net or subpath on playground
    • Accessible from phone on Tailscale network
    • Listed on playground.tail5b443a.ts.net hub landing page

    Seed data:

    • Script to populate test locations (5-10 McDonald's), test codes (mix of active, redeemed, expired), test receipts
    • 4 Keycloak test users: testuser, testuser2, testadmin, emptyuser
    • Realm export JSON for reproducible Keycloak setup

    Docker Compose → Kustomize mapping:

    • Same containers, same env vars, same ports — different orchestrator
    • docker-compose.yml is the local dev definition, kustomize overlay is the prod definition
    • Both stay in sync — changes to one should reflect in the other

    SOP gate:

    • Update sop-frontend-experiment and Phase 7 steps: "run docker compose up, test full flow locally, THEN push to prod"
    • Update project-capacitor-mobile local dev workflow section (already created this session)

    Deliverables

    • pending
    • plan-mcd-tracker — parent plan
    • phase-mcd-tracker-7-sveltekit — parent phase
    • project-capacitor-mobile — local dev workflow doc (updated this session)
    • todo-service-onboarding-validation — deployment validation TODO
  • Phase 7b: Keycloak Theme — Match App Design phase-mcd-tracker-7b-keycloak-theme

    Goal: Custom Keycloak login/register theme matching the MCD Tracker design system — Atkinson Hyperlegible, design tokens, light theme, mobile-first.

    Owner: Dev agent + Lucas (visual approval)

    Repo: Keycloak theme deployment (pal-e-platform or dedicated theme repo)

    Depends on: Phase 7 (app deployed, auth flow working)

    Scope

    • Create a custom Keycloak theme for the mcd-tracker realm
    • Login page: match signin.html playground mockup (logo, form fields, colors)
    • Registration page: match register.html playground mockup
    • Password reset page: same design language
    • Error pages: branded, not default Keycloak red
    • Use app.css design tokens: --color-bg: #fafafa, --color-primary: #0366d6, Atkinson Hyperlegible font
    • Mobile-first: must look good on 390px viewport
    • Deploy theme to Keycloak (volume mount or realm import)

    Deliverables

    • pending
    • plan-mcd-tracker — parent plan
    • phase-mcd-tracker-7-sveltekit — parent phase
    • project-capacitor-mobile — auth flow documentation
  • Phase 6: Frontend Playground phase-mcd-tracker-6-playground

    Goal: HTML/CSS prototypes of all screens, served at playground.tail5b443a.ts.net/mcd-tracker/, verified on phone, approved by Lucas.

    Owner: Dev agent (scaffold + infra) → Lucas (design iteration)

    Repo: forgejo_admin/mcd-tracker-playground

    Depends on: Phase 5 (COMPLETED — API response schemas define the fake data structure)

    Scope

    Follow sop-frontend-experiment repo experiment procedure exactly:

    1. Create Forgejo repo forgejo_admin/mcd-tracker-playground
    2. Clone locally to ~/mcd-tracker-playground
    3. Initial scaffold: 6 pages with hardcoded data ✓ (index, home, scan, codes, locations, history)
    4. CSS following convention-frontend-css: Atkinson Hyperlegible, design tokens, mobile-first ✓
    5. Add nginx subpath + hostPath mount ✓
    6. Verify at playground.tail5b443a.ts.net/mcd-tracker/
    7. Design iteration (in progress): Lucas reviewing on phone. Key changes identified:
      • Landing page redesign — full public experience (how it works, value prop, FAQ, trust signals) like westside-playground. Current splash is too thin for App Store.
      • Location-centric home — merge home + locations into one dashboard. Remove meaningless aggregate Quick Stats. Home becomes location cards sorted by proximity, each showing slot status.
      • Location detail page (NEW) — tap a location → see slot status, codes for THIS location, [Scan Receipt] pre-selecting this location. Codes belong to locations, not a flat list.
      • Bottom nav simplification — Home (location dashboard), Scan (receipt wizard), History (timeline). 'My Codes' absorbed into location detail.
      • Auth decision documented — keycloak-js client-side OIDC (not Auth.js SSR). See project-capacitor-mobile.
    8. Lucas approves final design on phone → design lock
    9. Register repo in project-frontend-playground Repos table
    10. Push final state to Forgejo

    After scaffold: Lucas iterates on design personally. Agents don't ship frontend without visual approval.

    Architecture Touch

    arch-dataflow-mcd-tracker — all 4 flows (log code, check availability, redeem, rejection) visualized as UI screens. No diagram update needed.

    Verification

    • playground.tail5b443a.ts.net/mcd-tracker/ loads on phone
    • All 4 screens render with hardcoded fake data
    • Mobile-first: no horizontal scroll on phone
    • CSS uses design tokens only (no hardcoded hex)
    • Lucas approves the design

    Deliverables

    • 12 playground pages with complete end-to-end user flow:
    • Landing page (public): hero, How It Works, value props, FAQ, dual CTA (sign in + register)
    • Auth flow: sign-in + register mockups (Keycloak themed, web vs iOS notes)
    • Home: location-centric dashboard with XP banner, 5-day expiry warnings, floating scan FAB
    • Location detail: slot progress, active codes with expiry countdown, cashier overlay with receipt photo
    • Scan wizard: 4-step flow → save-success celebration (+1 XP, Use Now → cashier overlay)
    • Redeem celebration: +2 XP, 'What did you get?' chip picker, lifetime count
    • History: date-grouped timeline, color-coded events, lifetime stats bar
    • Event detail: full context + state-aware cashier overlay (active vs redeemed)
    • CSS: page-header scoping fix, 3-tab bottom nav (Home/Scan/History)
    • Pushed to Forgejo: mcd-tracker-playground PR #3
    • plan-mcd-tracker — parent plan
    • sop-frontend-experiment — repo experiment procedure
    • convention-frontend-css — CSS rules
    • project-frontend-playground — playground project
  • Phase 7a: E2E + Integration Test Suite phase-mcd-tracker-7a-testing

    Goal: Full test coverage across unit, integration, and E2E layers. Playwright for real browser flows, MSW for API mocking, Vitest for components.

    Owner: Dev agent + Lucas (E2E test verification on phone)

    Repo: mcd-tracker-app

    Depends on: Phase 7 (app scaffold merged and deployed)

    Scope

    Integration tests (Vitest + MSW):

    • Mock Service Worker intercepts API calls, returns controlled responses
    • Home page: fetches GET /dashboard, renders location cards with correct slot counts
    • Location detail: fetches slots + codes, renders progress bar, code cards, expiry countdown
    • History: fetches codes + receipts + stats, renders date-grouped timeline with correct colors
    • Scan flow: wizard step progression, location picker population from GET /locations/nearby
    • Redeem: PATCH /codes/{id}/redeem sends redeemed_item, success page shows correct data
    • Auth guard: unauthenticated user redirected from protected routes, landing page accessible
    • Empty states: zero locations, zero codes, zero history — verify empty UX renders

    E2E tests (Playwright):

    • Full user flow: landing → sign in (Keycloak test user) → home → tap location → scan → save → use now → redeem → history → event detail
    • Mobile viewport (390px) — no horizontal scroll on any page
    • Visual regression: screenshot each page, compare against playground baseline
    • Lighthouse audit: mobile score > 90 on landing page
    • Auth flow: sign in, sign out, register (with Keycloak test accounts)
    • Error states: API timeout, 401 expired token, 404 location not found

    Test accounts (Keycloak mcd-tracker realm):

    • testuser@mcd-tracker.test — normal user with sample data
    • testuser2@mcd-tracker.test — second user (isolation testing)
    • testadmin@mcd-tracker.test — admin role
    • emptyuser@mcd-tracker.test — zero data (empty state UX)

    CI integration:

    • Vitest + MSW tests run in Woodpecker CI on every push (fast, no browser needed)
    • Playwright tests run as a separate CI step or manual trigger (needs browser, slower)
    • Chrome DevTools MCP for interactive debugging during development

    Deliverables

    • pending
    • plan-mcd-tracker — parent plan
    • phase-mcd-tracker-7-sveltekit — parent phase (scaffold must be deployed first)
    • project-capacitor-mobile — Capacitor testing considerations
  • Phase 10a: Pre-Launch Security Hardening phase-mcd-tracker-10a-security

    Goal: Harden the stack against bot abuse, credential stuffing, and API spam before the App Store makes the app publicly discoverable.

    Owner: Dev agent (API + Keycloak config) + Lucas (Keycloak admin console)

    Repo: mcd-tracker-api, Keycloak admin console (manual)

    Depends on: Phase 7 (app deployed), Phase 9 (iOS build working)

    Scope

    • Keycloak CAPTCHA on registration — enable reCAPTCHA or hCaptcha on the mcd-tracker realm registration flow. Blocks automated account creation.
    • Keycloak brute force detection — enable in realm settings. Lock account after N failed login attempts. Configure permanent vs temporary lockout.
    • API rate limiting — add middleware to mcd-tracker-api. 60 req/min per authenticated user. 10 req/min for unauthenticated endpoints (healthz only). Use slowapi or custom middleware.
    • CORS audit — verify Access-Control-Allow-Origin is locked to mcd-tracker-app.tail5b443a.ts.net and capacitor://localhost. No wildcards.
    • XSS audit — grep all Svelte files for {@html} usage with user-supplied data. SvelteKit escapes by default but {@html} bypasses it.
    • Input validation — verify all API string inputs have max_length constraints (Pydantic). Prevent oversized payloads.
    • Keycloak password policy — set minimum length, complexity requirements on mcd-tracker realm.

    Deliverables

    • pending
    • plan-mcd-tracker — parent plan
    • phase-mcd-tracker-10-app-store — parent phase (security gates App Store submission)
    • sop-network-security — platform security SOP
  • Phase 14: Receipt Intelligence phase-mcd-tracker-14-receipt-intelligence

    Goal: Extract maximum value from receipt photos. Items purchased, prices, timestamps, store numbers, payment methods — rich data beyond survey codes.

    Owner: Dev agent + Lucas (product decisions)

    Repo: mcd-tracker-api

    Depends on: Phase 5b (OCR — deferred, currently manual code entry)

    Scope

    • OCR pipeline: Tesseract (self-hosted) or Cloud Vision (API) — decision deferred to this phase
    • Receipt parsing: extract line items, total, tax, store number, timestamp, payment method
    • Insights: most ordered items, average spend per visit, spending by location, visit frequency patterns
    • Optimal timing: when are BOGO codes most likely to have open slots at each location?
    • Data model: ReceiptItem table (receipt_id FK, item_name, price, quantity)
    • Privacy-first: all data stays on our infra, user can delete anytime
    • Feed gamification: "You've tried 23 different menu items" or "Your most ordered: Big Mac (8x)"

    Deliverables

    • pending
    • plan-mcd-tracker — parent plan
    • phase-mcd-tracker-13-gamification — receipt data feeds gamification insights
    • phase-mcd-tracker-5a-receipt-model — receipt model + photo upload (foundation)
  • Phase 13: Gamification + XP System phase-mcd-tracker-13-gamification

    Goal: Celebrate usage. XP system with levels, lifetime code tally, mascot character with context-aware Lottie animations.

    Owner: Lucas (mascot design) + Dev agent (implementation)

    Repo: mcd-tracker-api, mcd-tracker-app

    Depends on: Phase 7 (frontend must exist)

    Scope

    • API: GET /stats endpoint — lifetime codes redeemed, current level, XP progress, streak days
    • Level system: Level 1 (0-5 redeemed) → Level 2 (6-15) → etc. Titles: "BOGO Beginner" → "Free Food Fanatic" → "Coupon Legend"
    • Home page XP banner above location list ("Level 3 BOGO Hunter · 12 codes redeemed")
    • Mascot character — Lottie animations, ~8 states: idle, happy, excited, worried (code expiring), sleeping (no activity), celebrating (just redeemed), nudging (slot opened), waving (first visit)
    • Mascot appears on: home banner, redemption overlay, empty states
    • Design mascot as separate creative task — Lucas drives, AI tools (motchi.art, Lottie Tools) assist

    Deliverables

    • pending
    • plan-mcd-tracker — parent plan
    • phase-mcd-tracker-14-receipt-intelligence — receipt data feeds gamification
  • Phase 10: App Store Submission phase-mcd-tracker-10-app-store

    Goal: mcd-tracker live on the App Store. Full CI/CD: push → TestFlight → promote.

    Owner: Main session + Lucas

    Repo: forgejo_admin/mcd-tracker-app

    Depends on: Phase 9 (TestFlight working)

    Scope

    1. App Store Connect setup: app record, categories, pricing (free)
    2. App icons (1024x1024 + all sizes)
    3. Screenshots (6.7" + 5.5" displays)
    4. Privacy policy URL
    5. App description, keywords (ASO)
    6. Submit for review
    7. CI pipeline: Fastlane deliver for metadata automation

    Deliverables

    • pending
    • plan-mcd-tracker — parent plan
    • project-capacitor-mobile — App Store requirements checklist
  • Phase 9: Capacitor iOS Build + Native Plugins phase-mcd-tracker-9-capacitor-ios

    Goal: iOS app builds on Mac CI, runs on Lucas's iPhone via TestFlight. Native camera + GPS working.

    Owner: Dev agent + Mac agent

    Repo: forgejo_admin/mcd-tracker-app

    Depends on: Phase 7 (Capacitor configured), Phase 8 (Mac agent running)

    Scope

    1. Add .woodpecker.yaml iOS build step with labels: [platform: darwin]
    2. Pipeline: npm run build → npx cap sync → xcodebuild archive → export .ipa
    3. Configure Xcode signing (Apple Developer Program required, $99/yr)
    4. Test Capacitor native plugins on device: camera (receipt photos), geolocation (Near Me), clipboard
    5. Fastlane pilot upload → TestFlight
    6. Lucas installs via TestFlight, validates on iPhone

    Deliverables

    • pending
    • plan-mcd-tracker — parent plan
    • project-capacitor-mobile — plugin catalog, build pipeline, Info.plist requirements
  • Phase 8: Mac Woodpecker Agent phase-mcd-tracker-8-mac-agent

    Goal: MacBook Air M1 running as a Woodpecker CI agent with local backend, capable of npx cap sync && xcodebuild.

    Owner: Main session (manual Mac setup)

    Repo: n/a (Mac configuration, documented in project-capacitor-mobile)

    Depends on: Phase 7 (SvelteKit + Capacitor app exists)

    Scope

    1. Install Xcode on MacBook Air M1
    2. Install Node.js, npm (for Capacitor builds)
    3. Install Woodpecker agent (brew or direct download)
    4. Configure: local backend, WOODPECKER_FILTER_LABELS=platform=darwin
    5. Create launchd plist for auto-start
    6. Install Fastlane: brew install fastlane
    7. Verify: agent visible in Woodpecker UI, test pipeline runs on Mac

    Deliverables

    • pending
    • plan-mcd-tracker — parent plan
    • project-capacitor-mobile — iOS build pipeline docs
    • arch-deployment-mcd-tracker — Mac agent in deployment diagram
  • Phase 5c: GPS Nearby Locations (Overpass API) phase-mcd-tracker-5c-gps-nearby

    Goal: Auto-detect nearest McDonald's via GPS coordinates + OpenStreetMap Overpass API. The location picker has a backend.

    Owner: Dev agent

    Repo: forgejo_admin/mcd-tracker-api

    Depends on: Phase 5 (COMPLETED)

    Scope

    Implements arch-dataflow-mcd-tracker#flow-5-auto-detect-location. Uses the Overpass API (OpenStreetMap) to find McDonald's near the user's GPS coordinates. Free, no API key, well-mapped for US chain restaurants.

    1. Add GET /locations/nearby?lat=39.74&lng=-104.99&radius=5000 endpoint:
      • Queries Overpass API: node["brand"="McDonald's"](around:{radius},{lat},{lng}); out;
      • Returns list of nearby McDonald's with name, address, lat/lng, distance from user
      • Checks if any match user's saved locations (by proximity threshold)
      • Sorted by distance ascending
    2. Add src/mcd_tracker_api/services/overpass.py — Overpass API client (httpx)
    3. Add distance calculation utility (Haversine formula for lat/lng distance)
    4. Update Location model: ensure latitude/longitude are required for new locations (nullable for legacy)
    5. Integration tests with mocked Overpass responses (don't hit real API in CI)

    • PR #11 merged (mcd-tracker-api) — GET /locations/nearby endpoint, Overpass API client (async), Haversine distance, saved location matching
    • 35 new tests — 16 integration (nearby endpoint), 14 unit (Overpass client), 5 unit (Haversine). All Overpass-mocked.
    • QA blocker fixed: pytest-anyio missing from dev deps — async tests weren't running in CI. Fixed before merge.
    • QA nits (5): slot logic duplication, Overpass missing way/relation, no user isolation test, hardcoded Overpass URL, possible negative available_slots.

    • pending
    • phase-mcd-tracker-5-core-api — parent phase
    • plan-mcd-tracker — parent plan
    • arch-dataflow-mcd-tracker#flow-5-auto-detect-location — GPS flow
  • Phase 5a: Receipt Model + Photo Upload phase-mcd-tracker-5a-receipt-model

    Goal: Receipt as a first-class entity — photo upload, Receipt model, CouponUsage linked to receipts, Alembic migration. The scan flow has a backend.

    Owner: Dev agent

    Repo: forgejo_admin/mcd-tracker-api

    Depends on: Phase 5 (COMPLETED)

    Scope

    Implements the updated domain model from arch-domain-mcd-tracker. Adds Receipt entity and updates CouponUsage.

    1. Add Receipt SQLAlchemy model:
      • id (PK), keycloak_sub (indexed), location_id (FK), photo_path (string), survey_code (string), survey_completed (bool, default false), captured_at (timestamp)
    2. Update CouponUsage model:
      • Add receipt_id (FK, nullable — existing codes don't have receipts)
      • Rename codebogo_code
      • Rename used_atearned_at
    3. Alembic migration for both changes (add Receipt table, alter CouponUsage columns)
    4. Add photo upload PVC mount to kustomize overlay (pal-e-deployments):
      • PVC: receipt-uploads (1Gi)
      • Mount: /data/uploads/receipts
    5. Add routes:
      • POST /receipts — multipart upload (photo + GPS coords). Saves photo to PVC, creates Receipt record, returns receipt_id + survey_code placeholder
      • GET /receipts — list user's receipts
      • GET /receipts/{id}/photo — serve receipt photo
    6. Update POST /codes to accept optional receipt_id
    7. Update Pydantic schemas for new/changed fields
    8. Integration tests for receipt upload, photo serving, receipt-to-code linking

    • PR #9 merged (mcd-tracker-api) — Receipt model, photo upload (multipart + UUID naming + 10MB limit), schema renames (code→bogo_code, used_at→earned_at), receipt_id FK on CouponUsage
    • PR #21 merged (pal-e-deployments) — receipt-uploads PVC (1Gi) + volume mount at /data/uploads
    • 76 tests passing — all existing tests updated for new field names + 18 new receipt tests
    • Migration 002 — reversible, handles existing data (nullable receipt_id)

    • pending
    • phase-mcd-tracker-5-core-api — parent phase
    • plan-mcd-tracker — parent plan
    • arch-domain-mcd-tracker — Receipt entity + updated CouponUsage
    • arch-dataflow-mcd-tracker#flow-1-scan-receipt — scan receipt flow
  • Goal: Full CRUD API with rolling window logic, integration tests, deployed to prod. The app is functional.

    Owner: Dev agent

    Repo: forgejo_admin/mcd-tracker-api

    Depends on: Phase 4 (COMPLETED — auth ready)

    Scope

    Implement all API routes from arch-dataflow-mcd-tracker. Rolling window logic is the core business rule: 5 codes per location per user per rolling 30-day window.

    Routes

    • POST /locations — save a McDonald's location (auth: user)
    • GET /locations — list user's saved locations (auth: user)
    • POST /locations/{id}/codes — log a coupon code at a location (auth: user, enforces 5-slot limit)
    • GET /locations/{id}/codes — list codes at a location (auth: user)
    • PATCH /codes/{id}/redeem — mark code as redeemed (auth: user)
    • GET /locations/{id}/slots — slot availability: remaining (0-5), next reopen date (auth: user)
    • GET /dashboard — all locations with slot status for current user (auth: user)
    • GET /admin/stats — aggregate stats across all users (auth: admin)

    Rolling Window Logic

    • Count active codes: WHERE expires_at > NOW()
    • Available slots = 5 - active_count
    • Next reopen = MIN(expires_at) from active codes
    • On POST /codes: check count first, reject with 409 if full
    • expires_at = used_at + timedelta(days=30) — set on insert

    Architecture Touch

    arch-dataflow-mcd-tracker — all 4 runtime flows implemented: log code, check availability, redeem, slot limit rejection. arch-domain-mcd-tracker#rolling-window-logic — the SQL queries from the diagram become SQLAlchemy queries.

    Verification

    • All routes respond correctly with auth
    • Log 5 codes → slots = 0. Log 6th → 409 with next reopen date
    • Redeem a code → redeemed_at set
    • Dashboard shows all locations with slot counts
    • Admin stats endpoint restricted to admin role
    • Integration tests cover all routes + edge cases

    • PR #7 merged (mcd-tracker-api) — 8 endpoints across 3 route modules + schemas.py
    • Rolling window LIVE — 5-slot limit with 409 Conflict + next_reopen date. expires_at = used_at + 30 days
    • Dashboard — single LEFT JOIN + GROUP BY, no N+1
    • Admin stats — require_role('admin') guarded
    • 58 tests passing (38 new) — real Postgres, mocked auth, covers rolling window edge cases
    • QA: 6 nits — field validation, hardcoded total_slots, admin query consolidation, magic number, active-only filter, test count in PR body

    • pending
    • plan-mcd-tracker — parent plan
    • phase-mcd-tracker-4-keycloak-auth — depends on
    • arch-dataflow-mcd-tracker — runtime flows
    • arch-domain-mcd-tracker — rolling window queries
  • Phase 4: Keycloak Realm + Auth phase-mcd-tracker-4-keycloak-auth

    Goal: Dedicated Keycloak realm with user/admin roles, JWT validation in API, protected endpoints ready for Phase 5 routes.

    Owner: Dev agent

    Repo: forgejo_admin/mcd-tracker-api

    Depends on: Phase 3 (COMPLETED — models + DB live)

    Scope

    1. Create mcd-tracker realm in Keycloak via admin API:
      • Realm roles: user, admin
      • OIDC client: mcd-tracker-app (confidential, for SvelteKit — redirect URIs for web app)
      • OIDC client: mcd-tracker-ios (public, PKCE — for Swift app)
    2. Add src/mcd_tracker_api/auth.py — copy basketball-api pattern:
      • JWKS fetch + cache from {realm_url}/protocol/openid-connect/certs
      • JWT decode + RS256 verification
      • User dataclass (sub, email, username, roles)
      • require_role() dependency factory
      • get_current_user() dependency
    3. Add config: MCD_TRACKER_KEYCLOAK_REALM_URL
    4. Update kustomize deployment-patch.yaml with Keycloak env var (pal-e-deployments)
    5. Create test user + admin user in realm
    6. Add auth tests: valid token → 200, no token → 401, wrong role → 403

    Architecture Touch

    arch-dataflow-mcd-tracker#auth-flow-keycloak-oidc — this phase implements the auth flow sequence diagram. Two OIDC clients (web confidential + iOS public/PKCE) as designed.

    Verification

    • Keycloak realm mcd-tracker accessible at https://keycloak.tail5b443a.ts.net/realms/mcd-tracker
    • Unauthenticated request to protected endpoint → 401
    • Authenticated request with valid JWT → 200
    • Wrong role → 403
    • CI tests pass

    • PR #5 merged (mcd-tracker-api) — auth.py with JWKS/JWT validation, User dataclass, require_role() dependency, 15 auth tests, 24/24 total passing
    • PR #17 merged (pal-e-deployments) — MCD_TRACKER_KEYCLOAK_REALM_URL env var in deployment patch
    • Keycloak realm createdmcd-tracker realm with roles (user, admin), two OIDC clients (mcd-tracker-app confidential, mcd-tracker-ios public/PKCE), test users (testuser, testadmin)
    • QA blocker overridden: QA flagged auth.py as DRY violation (copy of basketball-api). Override rationale: these are independent microservices with separate Keycloak realms — copy-and-own is the correct pattern for cross-service auth. A shared library would create coupling between independent apps. See epilogue.

    • pending
    • plan-mcd-tracker — parent plan
    • phase-mcd-tracker-3-data-model — depends on
    • arch-dataflow-mcd-tracker — auth flow diagram
  • Phase 3: Data Model + Postgres phase-mcd-tracker-3-data-model

    Goal: SQLAlchemy models defined, Alembic migrations running, DB-backed health check, deployed to prod.

    Owner: Dev agent

    Repo: forgejo_admin/mcd-tracker-api

    Depends on: Phase 2 (COMPLETED — FastAPI scaffold live, postgres pod running)

    Scope

    Add database layer to the existing FastAPI scaffold. Postgres sidecar is already deployed (Phase 1). This phase connects the app to it and defines the domain model from arch-domain-mcd-tracker.

    1. Add src/mcd_tracker_api/database.py — SQLAlchemy engine, SessionLocal, Base, get_db() dependency
    2. Add src/mcd_tracker_api/models.py — SQLAlchemy models per domain diagram:
      • Location — id, keycloak_sub, name, address, city, state, latitude (nullable), longitude (nullable), created_at
      • CouponUsage — id, location_id (FK), keycloak_sub, code, used_at, redeemed (bool), redeemed_at (nullable), expires_at (computed: used_at + 30 days)
    3. Add SQLAlchemy + psycopg2-binary + alembic to pyproject.toml dependencies
    4. Init Alembic: alembic init alembic/, configure alembic/env.py to use settings.database_url
    5. Create first migration: alembic revision --autogenerate -m "initial schema"
    6. Add migration-on-startup to lifespan in main.py: alembic.command.upgrade(config, "head")
    7. Update /healthz to verify DB connection (SELECT 1)
    8. Add integration test: verify models create tables, health endpoint shows DB connected

    Architecture Touch

    arch-domain-mcd-tracker — this phase implements the entity model. The domain diagram was designed for these exact models. No diagram update needed unless implementation diverges.

    Verification

    • curl https://mcd-tracker.tail5b443a.ts.net/healthz shows DB connected
    • Alembic migration runs on pod startup (check logs)
    • Tables exist in postgres: location, coupon_usage
    • CI green with integration tests

    • PR #3 merged (mcd-tracker-api) — database.py, models.py (Location + CouponUsage), alembic init + migration, DB-backed /healthz, 9 integration tests
    • CI fix: Added Postgres service container to .woodpecker.yaml (caught by QA, fixed before merge)
    • Pipeline #4 GREEN — 9 tests passing against real Postgres in CI
    • Models match domain diagram — arch-domain-mcd-tracker implemented exactly

    • pending
    • plan-mcd-tracker — parent plan
    • phase-mcd-tracker-2-backend-scaffold — depends on
    • arch-domain-mcd-tracker — entity model this implements
  • Phase 1a: CI Validation for pal-e-deployments phase-mcd-tracker-1a-ci-validation

    Goal: Every kustomize overlay PR on pal-e-deployments is validated in CI before merge — kubectl kustomize + server-side dry-run.

    Owner: Dev agent

    Repo: forgejo_admin/pal-e-deployments

    Depends on: None (can run in parallel with remaining Phase 1 work)

    Scope

    Add a .woodpecker.yaml to pal-e-deployments that runs on every PR:

    Step 1: Static kustomize validation

    for overlay in overlays/*/prod; do
      echo "Validating $overlay..."
      kubectl kustomize "$overlay" > /dev/null
    done

    Catches: broken YAML, missing base refs, invalid patch targets, missing resources.

    Step 2: Server-side dry-run

    for overlay in overlays/*/prod; do
      echo "Dry-run $overlay..."
      kubectl kustomize "$overlay" | kubectl apply --dry-run=server -f -
    done

    Catches: invalid field names, CRD mismatches, schema violations. Requires CI kubeconfig access (use node IP 10.0.0.217:6443, not 127.0.0.1).

    Step 3 (future scope): Ephemeral namespace validation

    Create a temporary namespace, apply the overlay for real, run a smoke test (e.g., wait for pod ready), tear down. This validates secrets, image pulls, and runtime behavior — not just schema. Defer to a later phase — dry-run=server covers 90% of failures.

    Implementation Notes

    • Woodpecker needs to be activated on pal-e-deployments (may already be — check)
    • CI kubeconfig: 10.0.0.217:6443 (not 127.0.0.1 — pipeline pods can't reach localhost)
    • CI needs a kubeconfig secret or service account token
    • Image: bitnami/kubectl:latest or bundle kubectl into a custom image
    • Only validate overlays that changed in the PR (optimization — use git diff --name-only)

    • PR #15 merged (pal-e-deployments) — .woodpecker.yaml with kustomize render + server-side dry-run
    • Image: alpine/k8s:1.32.4 (not bitnami — Go 1.25 causes x509 errors with k3s certs)
    • SOPS-aware: awk filter strips encrypted documents from dry-run validation
    • Pipeline #10 GREEN: 7/7 overlays validated (all existing services)
    • Woodpecker activated on pal-e-deployments (repo ID 29)

    • pending
    • phase-mcd-tracker-1-service-onboarding — parent phase
    • plan-mcd-tracker — parent plan
    • sop-platform-tf-changes — updated SOP with pre-merge patterns
    • todo-pre-merge-infra-validation — claude-custom hook TODO
  • Phase 1: Service Onboarding phase-mcd-tracker-1-service-onboarding

    Goal: Production namespace, Harbor project, ArgoCD app, and kustomize overlay ready — before any code exists.

    Owner: Dev agent

    Repo: forgejo_admin/pal-e-services, forgejo_admin/pal-e-deployments

    Depends on: None

    Forgejo Issue: forgejo_admin/pal-e-services #14

    Follow service-onboarding-sop and convention-kustomize-overlay to onboard mcd-tracker as a new service on the platform. This is pure infrastructure — no application code.

    1. Add mcd-tracker entry to pal-e-services/terraform/k3s.tfvars var.services map
    2. tofu plan -lock=false → verify 6-7 new resources
    3. tofu apply -lock=false
    4. Create kustomize overlay: pal-e-deployments/overlays/mcd-tracker/prod/

    Follow service-onboarding-sop and convention-kustomize-overlay to onboard mcd-tracker as a new service on the platform. This is pure infrastructure — no application code.

    1. Add mcd-tracker entry to pal-e-services/terraform/k3s.tfvars var.services map:
      mcd-tracker = {
        forgejo_repo = "forgejo_admin/mcd-tracker-api"
        image_repo   = "mcd-tracker/api"
        port         = 8000
        funnel       = true
        source_repo  = "forgejo_admin/pal-e-deployments"
        source_path  = "overlays/mcd-tracker/prod"
      }
    2. tofu plan -lock=false → verify 6-7 new resources (namespace, Harbor project, robot accounts, pull secret, ArgoCD app, funnel ingress)
    3. tofu apply -lock=false
    4. Create kustomize overlay: pal-e-deployments/overlays/mcd-tracker/prod/
      • kustomization.yaml — base ref (../../../bases/standard), rename patches (app → mcd-tracker-api), images transformer
      • deployment-patch.yaml — env vars (MCD_TRACKER_DATABASE_URL, MCD_TRACKER_KEYCLOAK_REALM_URL)

    Architecture Touch

    arch-deployment-mcd-tracker — this phase creates the infrastructure depicted in the deployment diagram. No diagram update needed (diagram was written with this phase in mind).

    Verification

    • kubectl get ns mcd-tracker — namespace exists
    • ArgoCD app mcd-tracker visible in ArgoCD UI (will be degraded/missing until first image push — expected)
    • Harbor project mcd-tracker exists with CI + pull robot accounts
    • Kustomize overlay renders: kubectl kustomize pal-e-deployments/overlays/mcd-tracker/prod/

    • PR #13 merged (pal-e-deployments) — kustomize overlay: kustomization.yaml + deployment-patch.yaml + postgres.yaml
    • tofu apply — 6/7 resources created: namespace, Harbor project, 2 robot accounts, pull secret, Tailscale funnel ingress
    • Remaining: Re-run tofu apply after overlay merge to create ArgoCD app. Create mcd-tracker-secrets k8s Secret.

    • pending
    • plan-mcd-tracker — parent plan
    • service-onboarding-sop — procedure
    • convention-kustomize-overlay — overlay pattern
    • arch-deployment-mcd-tracker — deployment diagram
  • Phase 2: Backend Scaffold + First Deploy phase-mcd-tracker-2-backend-scaffold

    Goal: FastAPI skeleton running in prod — health endpoint responding, CI green, ArgoCD syncing. ImagePullBackOff resolves.

    Owner: Dev agent

    Repo: forgejo_admin/mcd-tracker-api

    Depends on: Phase 1 (COMPLETED — namespace, Harbor, ArgoCD app all live)

    Scope

    Scaffold the FastAPI app following the basketball-api pattern exactly. Push first image to Harbor. ArgoCD auto-deploys. Health endpoint responds.

    1. Scaffold FastAPI app:
      • src/mcd_tracker_api/main.py — FastAPI app with lifespan
      • src/mcd_tracker_api/config.py — Pydantic Settings with MCD_TRACKER_ env prefix
      • src/mcd_tracker_api/routes/health.py/healthz endpoint
      • pyproject.toml — project metadata + dependencies (fastapi, uvicorn, pydantic-settings)
    2. Add Dockerfile — multi-stage Python build (builder + runtime), expose 8000
    3. Add .woodpecker.yaml — two steps: test (ruff + pytest) and build-and-push (kaniko to Harbor). Use ${CI_COMMIT_SHA} as image tag.
    4. Activate Woodpecker on mcd-tracker-api repo
    5. Add Harbor secrets to Woodpecker repo settings (harbor_username, harbor_password — get from tofu output)
    6. Push to main → green pipeline → Harbor image → ArgoCD auto-deploys

    Architecture Touch

    arch-deployment-mcd-tracker — this phase populates the API pod in the deployment diagram. arch-dataflow-mcd-tracker — health endpoint is the first runtime flow (trivial). No diagram updates needed.

    Verification

    • curl https://mcd-tracker.tail5b443a.ts.net/healthz returns 200
    • Woodpecker pipeline green on mcd-tracker-api
    • Harbor shows mcd-tracker/api image with SHA tag
    • ArgoCD app mcd-tracker healthy (no more ImagePullBackOff)
    • Pods: mcd-tracker (1/1 Running), postgres (1/1 Running)

    • Scaffold pushed to main — 2 commits (fc1edad + e596e33). 11 files: main.py, config.py, health route, Dockerfile, .woodpecker.yaml, pyproject.toml, tests
    • CI green — Woodpecker activated (repo ID 30), pipelines #1 + #2 green, Harbor secrets configured
    • Deployed — ArgoCD Image Updater auto-deployed. curl https://mcd-tracker.tail5b443a.ts.net/healthz{"status":"ok"}
    • Bug found + fixed: K8s injects MCD_TRACKER_PORT=tcp://10.43.x.x:8000 (service discovery), colliding with Pydantic Settings port: int. Renamed to server_host/server_port. Pattern to watch for when service name matches env prefix.

    • pending
    • plan-mcd-tracker — parent plan
    • phase-mcd-tracker-1-service-onboarding — depends on (infra ready)
    • arch-deployment-mcd-tracker — deployment diagram
Architecture 3
  • Architecture: Deployment — mcd-tracker arch-deployment-mcd-tracker

    Architecture: Deployment — mcd-tracker

    Purpose

    Where does it live? Services, infrastructure, CI/CD pipelines, and how they connect. This diagram is also the development workflow — it shows how code moves from editor to production.

    Infrastructure Diagram

    graph TB
        subgraph DEV["Development (archbox — Arch Linux)"]
            EDITOR[vim / helix / editor]
            GIT_LOCAL[Local git repos]
            EDITOR --> GIT_LOCAL
        end
    
        subgraph FORGEJO["Source Control (Forgejo)"]
            REPO_API[mcd-tracker-api]
            REPO_APP[mcd-tracker-app
    SvelteKit + Capacitor]
            REPO_DEPLOY[pal-e-deployments]
        end
    
        GIT_LOCAL -->|git push| FORGEJO
    
        subgraph CI["CI/CD (Woodpecker)"]
            LINUX_AGENT[Linux Agent
    Docker backend
    test + build + push]
            MAC_AGENT[Mac Agent
    local backend
    npx cap sync + xcodebuild]
        end
    
        REPO_API -->|webhook| LINUX_AGENT
        REPO_APP -->|webhook| LINUX_AGENT
        REPO_APP -->|webhook
    platform=darwin| MAC_AGENT
    
        subgraph REGISTRY["Distribution"]
            HARBOR[Harbor
    mcd-tracker/api
    mcd-tracker/app]
            TESTFLIGHT[TestFlight
    mcd-tracker.ipa]
        end
    
        LINUX_AGENT -->|kaniko push| HARBOR
        MAC_AGENT -->|fastlane pilot| TESTFLIGHT
    
        subgraph GITOPS["GitOps"]
            IMG_UPDATER[ArgoCD Image Updater
    watches Harbor tags]
            ARGOCD[ArgoCD
    syncs kustomize overlays]
        end
    
        HARBOR -->|new tag detected| IMG_UPDATER
        IMG_UPDATER -->|write newTag| REPO_DEPLOY
        REPO_DEPLOY -->|auto-sync| ARGOCD
    
        subgraph K3S["k3s Cluster (archbox)"]
            subgraph NS_API["namespace: mcd-tracker"]
                API_POD[mcd-tracker-api
    FastAPI :8000]
                PG_POD[Postgres 16
    sidecar :5432]
                PG_PVC[(1Gi PVC)]
                API_POD --- PG_POD
                PG_POD --- PG_PVC
            end
            subgraph NS_APP["namespace: mcd-tracker-app"]
                APP_POD[mcd-tracker-app
    nginx serving SPA :3000]
            end
            subgraph NS_KC["namespace: keycloak"]
                KC_POD[Keycloak
    realm: mcd-tracker
    public client + PKCE]
            end
        end
    
        ARGOCD -->|deploy| NS_API
        ARGOCD -->|deploy| NS_APP
        APP_POD -->|client-side API calls
    Bearer token| API_POD
        API_POD -->|JWT validate| KC_POD
    
        subgraph INGRESS["Tailscale Funnels (TLS)"]
            FUNNEL_API[mcd-tracker.tail5b443a.ts.net]
            FUNNEL_APP[mcd-tracker-app.tail5b443a.ts.net]
        end
    
        FUNNEL_API --> API_POD
        FUNNEL_APP --> APP_POD
    
        subgraph APPLE["Apple"]
            APP_STORE[App Store]
        end
    
        TESTFLIGHT -->|promote| APP_STORE
    
        subgraph MAC["MacBook Air M1"]
            XCODE[Xcode]
            FASTLANE[Fastlane]
            CAPACITOR[Capacitor
    npx cap sync ios]
            WP_AGENT[Woodpecker Agent
    local backend]
            XCODE --- WP_AGENT
            FASTLANE --- WP_AGENT
            CAPACITOR --- WP_AGENT
        end
    
        MAC_AGENT -.- WP_AGENT
    

    Development Workflow

    graph LR
        subgraph BACKEND["Backend Pipeline"]
            B1[Write Python on archbox] --> B2[git push to Forgejo]
            B2 --> B3[Woodpecker: pytest + ruff]
            B3 --> B4[Woodpecker: kaniko build + push to Harbor]
            B4 --> B5[Image Updater writes newTag]
            B5 --> B6[ArgoCD syncs to k3s]
            B6 --> B7[Live at mcd-tracker.tail5b443a.ts.net]
        end
    
        subgraph FRONTEND["Frontend Pipeline"]
            F1[HTML/CSS in playground] --> F2[Lucas approves on phone]
            F2 --> F3[Copy-paste to SvelteKit]
            F3 --> F4[git push to Forgejo]
            F4 --> F5[Woodpecker: build + push]
            F5 --> F6[ArgoCD deploys]
            F6 --> F7[Live at mcd-tracker-app.tail5b443a.ts.net]
        end
    
        subgraph MOBILE["iOS Pipeline"]
            M1[Write Swift on archbox] --> M2[git push to Forgejo]
            M2 --> M3[Mac agent: xcodebuild test]
            M3 --> M4[Mac agent: xcodebuild archive]
            M4 --> M5[fastlane pilot → TestFlight]
            M5 --> M6[Manual promote → App Store]
        end
    

    Service Inventory

    Service Namespace Port Image Funnel URL
    mcd-tracker-api mcd-tracker 8000 harbor.../mcd-tracker/api mcd-tracker.tail5b443a.ts.net
    mcd-tracker-app mcd-tracker-app 3000 harbor.../mcd-tracker/app mcd-tracker-app.tail5b443a.ts.net
    Postgres (sidecar) mcd-tracker 5432 postgres:16-alpine n/a (ClusterIP)
    Keycloak keycloak (shared) 8080 quay.io/keycloak:26.0.7 keycloak.tail5b443a.ts.net

    Mac Agent Details

    Component Details
    Hardware MacBook Air M1 (Apple Silicon, arm64)
    Connectivity Tailscale — agent connects to Woodpecker server via tailnet
    Backend Woodpecker local backend (shell executor, no Docker)
    Label filter platform=darwin — only picks up iOS pipeline jobs
    Auto-start launchd plist — survives reboot
    Tools Xcode, Fastlane, xcodebuild, codesign

    Key Observations

    • Dual-agent CI. Linux agent handles backend + frontend (Docker/kaniko). Mac agent handles iOS (local/shell). Pipeline label platform=darwin routes iOS jobs to the Mac.
    • All dev on archbox. Swift, Python, SvelteKit — all written on the Linux workstation. The Mac is infrastructure, not a workstation. It only executes builds.
    • Three delivery targets. Harbor (containers → ArgoCD → k3s), TestFlight (iOS beta), App Store (iOS production). Same Forgejo + Woodpecker pipeline orchestrates all three.
    • Keycloak is shared infrastructure. The mcd-tracker realm lives in the existing Keycloak deployment (keycloak namespace). No new Keycloak instance — just a new realm.
    • Two namespaces for two services. API and frontend each get their own k8s namespace (per service-onboarding-sop). Postgres is a sidecar in the API namespace.
    • arch-domain-mcd-tracker — entity model
    • arch-dataflow-mcd-tracker — runtime flows
    • project-mcd-tracker — project page
    • service-onboarding-sop — how services join the cluster
    • convention-kustomize-overlay — deployment manifest pattern
  • Architecture: Data Flow — mcd-tracker arch-dataflow-mcd-tracker

    Architecture: Data Flow — mcd-tracker

    Purpose

    What happens when? How information moves through the system at runtime. These flows map directly to API endpoints and frontend screens.

    Flow 1: Scan Receipt + Extract Survey Code

    sequenceDiagram
        actor User
        participant App as mcd-tracker-app
        participant API as mcd-tracker-api
        participant OCR as OCR Service
        participant DB as Postgres
        participant FS as Photo Storage
    
        User->>App: Taps "Scan Receipt" button
        App->>App: Opens camera (navigator.mediaDevices)
        User->>App: Snaps photo of receipt
        App->>API: POST /receipts (multipart: photo + GPS coords)
        API->>FS: Save photo to /data/uploads/receipts/{uuid}.jpg
        API->>OCR: Extract survey code from image
        OCR-->>API: survey_code = "12345-67890"
        API->>DB: Find nearest Location by GPS coords
        DB-->>API: Location: "McD - Colfax & Broadway"
        API->>DB: INSERT receipt (photo_path, survey_code, location_id)
        DB-->>API: Created
        API-->>App: {receipt_id, survey_code, location: "McD - Colfax", confirmed: false}
        App-->>User: "Code: 12345-67890 — correct?" [Confirm] [Retake]
        User->>App: Confirms code
        App->>App: Copies code to clipboard
        App-->>User: "Code copied! Ready to start the survey."
    

    Flow 2: Complete Survey + Save BOGO Code

    sequenceDiagram
        actor User
        participant App as mcd-tracker-app
        participant Browser as mcdvoice.com
        participant API as mcd-tracker-api
        participant DB as Postgres
    
        User->>App: Taps "Open Survey"
        App->>Browser: Opens mcdvoice.com in new tab
        Note over User,Browser: User pastes survey code, completes survey (~3 min)
        Browser-->>User: BOGO validation code displayed
        User->>App: Returns to app, taps "Save BOGO Code"
        App-->>User: Form: paste BOGO code, location pre-filled
        User->>App: Pastes BOGO code, confirms location
        App->>API: POST /codes {bogo_code, receipt_id, location_id}
        API->>DB: COUNT active usages at location (expires_at > NOW())
        DB-->>API: count = 2
        Note over API: 2 < 5 — slot available
        API->>DB: INSERT coupon_usage (bogo_code, earned_at, expires_at, receipt_id)
        API->>DB: UPDATE receipt SET survey_completed = true
        DB-->>API: Created
        API-->>App: 201 — BOGO saved, 3 slots remaining at this location
        App-->>User: "BOGO saved! 3 of 5 slots available at Colfax"
    

    Flow 3: Redeem BOGO at Counter

    sequenceDiagram
        actor User
        participant App as mcd-tracker-app
        participant API as mcd-tracker-api
        participant DB as Postgres
    
        User->>App: Opens app at McDonald's counter
        App->>API: GET /codes?status=active (Bearer JWT)
        API->>DB: SELECT active BOGO codes for user
        DB-->>API: List of unredeemed codes with locations
        API-->>App: Active codes
        App-->>User: Shows active codes, nearest location highlighted
        User->>App: Taps "Use This Code" on a code
        App-->>User: Shows BOGO code large + "Mark as Used" button
        User->>App: Shows code to cashier, taps "Mark as Used"
        App->>API: PATCH /codes/{id}/redeem
        API->>DB: UPDATE coupon_usage SET redeemed=true, redeemed_at=NOW()
        DB-->>API: Updated
        API-->>App: 200 — redeemed, slot timer started
        App-->>User: "Redeemed! Slot reopens in 30 days"
    

    Flow 4: Check Slot Availability (Dashboard)

    sequenceDiagram
        actor User
        participant App as mcd-tracker-app
        participant API as mcd-tracker-api
        participant DB as Postgres
    
        User->>App: Opens Dashboard
        App->>API: GET /dashboard (Bearer JWT)
        API->>DB: For each location: COUNT active usages, MIN(expires_at)
        DB-->>API: [{loc: "Colfax", active: 2, next_reopen: "Mar 28"}, ...]
        API-->>App: Dashboard data with slots + countdowns
        App-->>User: Location cards: "3 of 5 available" + "Next slot: Mar 28"
    

    Flow 5: Auto-Detect Location (GPS)

    sequenceDiagram
        actor User
        participant App as mcd-tracker-app
        participant API as mcd-tracker-api
        participant DB as Postgres
    
        App->>App: navigator.geolocation.getCurrentPosition()
        App->>API: GET /locations/nearby?lat=39.74&lng=-104.99
        API->>DB: SELECT locations ORDER BY distance(lat, lng, :lat, :lng) LIMIT 5
        DB-->>API: Nearest McDonald's locations
        API-->>App: [{name: "McD - Colfax", distance: "0.2 mi"}, ...]
        App-->>User: "You're at McD - Colfax & Broadway" [Confirm] [Choose Other]
    

    Flow 6: Slot Limit Rejection

    sequenceDiagram
        actor User
        participant App as mcd-tracker-app
        participant API as mcd-tracker-api
        participant DB as Postgres
    
        User->>App: Tries to save 6th BOGO code at a location
        App->>API: POST /codes {bogo_code, location_id}
        API->>DB: COUNT active usages (expires_at > NOW())
        DB-->>API: count = 5
        Note over API: 5 = 5 — NO SLOTS AVAILABLE
        API-->>App: 409 Conflict — "No slots available. Next opens Mar 22"
        App-->>User: "Limit reached! Next slot opens in 3 days"
    

    Auth Flow (Keycloak OIDC)

    sequenceDiagram
        actor User
        participant App as mcd-tracker-app
        participant KC as Keycloak
        participant API as mcd-tracker-api
    
        User->>App: Clicks "Sign In" on landing page
        App->>KC: OIDC Authorization Request (redirect)
        KC->>User: Login form
        User->>KC: Credentials
        KC-->>App: Authorization code (redirect back)
        App->>KC: Exchange code for tokens
        KC-->>App: access_token (JWT) + refresh_token
    
        Note over App: SvelteKit: Auth.js server-side tokens
        Note over App: iOS: ASWebAuthenticationSession + PKCE
    
        App->>API: GET /dashboard (Authorization: Bearer {access_token})
        API-->>App: Protected resource
    

    Key Observations

    • Receipt-first workflow. The user journey starts with a receipt photo, not a form. Camera → OCR → survey → BOGO code → save. The app optimizes for speed at the McDonald's counter.
    • Two codes, two entities. Survey code (on receipt, extracted by OCR) and BOGO code (from survey, user enters). Receipt holds one, CouponUsage holds the other. Linked by receipt_id.
    • GPS is the UX unlock. Auto-detecting which McDonald's the user is at removes the biggest friction: typing a location name. Combined with the camera, the entire scan-to-save flow can be nearly zero-input.
    • Photo as proof. Receipt photos serve as proof of purchase. Over time, this builds a trail that could let users stop carrying physical receipts.
    • 409 for slot limit. Same as before — semantically correct for "this would conflict with current state."
    • Survey completion is tracked. A receipt can exist without a BOGO code (user scanned but didn't finish the survey). This prevents double-counting.
    • arch-domain-mcd-tracker — entity model these flows operate on
    • arch-deployment-mcd-tracker — where these services run
    • project-mcd-tracker — project page
  • Architecture: Domain Model — mcd-tracker

    Purpose

    What are the things? Entities, their attributes, and how they relate. This diagram drives the SQLAlchemy models and Alembic migrations.

    Diagram

    erDiagram
        USER ||--o{ LOCATION : "saves"
        USER ||--o{ RECEIPT : "captures"
        USER ||--o{ COUPON_USAGE : "earns"
        LOCATION ||--o{ RECEIPT : "captured at"
        LOCATION ||--o{ COUPON_USAGE : "tracks"
        RECEIPT ||--o| COUPON_USAGE : "produces"
    
        USER {
            string keycloak_sub PK "from JWT — no local user table"
            string email "from JWT claim"
            string username "from JWT claim"
            string[] roles "user | admin"
        }
    
        LOCATION {
            int id PK
            string keycloak_sub FK "owner"
            string name "e.g. McD - Colfax and Broadway"
            string address
            string city
            string state
            float latitude "required — GPS matching"
            float longitude "required — GPS matching"
            string source "gps_auto | manual"
            timestamp created_at
        }
    
        RECEIPT {
            int id PK
            string keycloak_sub FK "who captured it"
            int location_id FK "which McDonalds"
            string photo_path "path to receipt photo"
            string survey_code "OCR extracted from receipt"
            bool survey_completed "did user finish the survey?"
            timestamp captured_at
        }
    
        COUPON_USAGE {
            int id PK
            int location_id FK
            int receipt_id FK "nullable — links back to the receipt"
            string keycloak_sub FK "who earned it"
            string bogo_code "the BOGO validation code from survey"
            timestamp earned_at "when code was saved"
            bool redeemed "has the BOGO been used at counter?"
            timestamp redeemed_at "nullable — when BOGO was used"
            timestamp expires_at "earned_at + 30 days — computed"
        }
    

    Key Design Decisions

    • No local User table. Identity comes from Keycloak JWT (sub claim). keycloak_sub is stored as a string FK-like reference on all entities.
    • Two codes, not one. The survey code is on the receipt (input to mcdvoice.com). The BOGO code is the output of the survey (the valuable one you redeem). Receipt holds the survey code, CouponUsage holds the BOGO code.
    • Receipt is a first-class entity. It's proof of purchase — the photo, the survey code, and whether the survey was completed. A Receipt produces a CouponUsage (one-to-one, nullable — you might scan a receipt but not finish the survey).
    • Location has required lat/lng. GPS auto-detection is a core feature. When the user scans a receipt, the app uses GPS to find the nearest McDonald's and auto-fills the location. source tracks whether the location was auto-detected or manually entered.
    • Photo storage. Receipt photos stored on a PVC (same pattern as basketball-api uploads). Path stored in DB. Future: migrate to MinIO for S3-compatible storage.
    • expires_at is computed. earned_at + 30 days. Stored for query efficiency, derived from earned_at.
    • Redeemed is separate from earned. You earn a BOGO code when you complete the survey. You redeem it when you use it at the counter. The 30-day timer starts at redemption (redeemed_at), and the slot reopens 30 days later.

    Rolling Window Logic

    The core business rule: 5 codes per location per user per rolling 30-day window.

    -- Available slots at a location
    SELECT 5 - COUNT(*) AS slots_remaining
    FROM coupon_usage
    WHERE location_id = :loc_id
      AND keycloak_sub = :user_sub
      AND expires_at > NOW();
    
    -- Next slot reopen date
    SELECT MIN(expires_at) AS next_reopen
    FROM coupon_usage
    WHERE location_id = :loc_id
      AND keycloak_sub = :user_sub
      AND expires_at > NOW();
    
    • project-mcd-tracker — project page
    • arch-dataflow-mcd-tracker — runtime flows
    • arch-deployment-mcd-tracker — infrastructure
Repos 2
  • mcd-tracker-app
    active
  • mcd-tracker-api
    active