mcd-tracker
Notes
Plan 1
-
Plan: mcd-tracker
plan-mcd-trackerPlan: 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)
Decision Rationale 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 month McDonald's enforces per-code. Each usage starts an independent 30-day timer. 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. 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 agent Still 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-trackerSeparate 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-mobileauth-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-deploymentsForgejo Issue: TBD
Steps:
- Add
mcd-trackerentry topal-e-services/terraform/k3s.tfvarsvar.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" } tofu plan -lock=false→ verify 6-7 new resources (namespace, Harbor project, robot accounts, pull secret, ArgoCD app, funnel ingress)tofu apply -lock=false- Create kustomize overlay:
pal-e-deployments/overlays/mcd-tracker/prod/kustomization.yaml— base ref + rename patches + images transformerdeployment-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-apiForgejo Issue: TBD
Steps:
- Create
mcd-tracker-apirepo on Forgejo - Scaffold FastAPI app (following basketball-api pattern):
src/mcd_tracker_api/main.py— FastAPI app + lifespansrc/mcd_tracker_api/config.py— Pydantic Settings withMCD_TRACKER_prefixsrc/mcd_tracker_api/routes/health.py—/healthzendpointDockerfile— multi-stage Python build.woodpecker.yaml— test + build-and-push (kaniko)pyproject.toml
- Activate Woodpecker → add Harbor secrets (
harbor_username,harbor_password) - Push → green pipeline → Harbor image → ArgoCD deploys
- 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-deploymentsForgejo Issue: TBD
Steps:
- Add Postgres sidecar to kustomize overlay (
postgres.yaml— Postgres 16-alpine, 1Gi PVC, ClusterIP service) - Add
src/mcd_tracker_api/database.py— engine, SessionLocal, get_db() - 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)
- Init Alembic, create first migration
- Update health endpoint to verify DB connection
- Deploy — verify migration runs on startup
Data Model Notes:
- No separate User table — user identity comes from Keycloak JWT (
subclaim). Storekeycloak_subas 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 daysfrom 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-apiForgejo Issue: TBD
Steps:
- Create
mcd-trackerrealm in Keycloak admin console - Create realm roles:
user,admin - Create OIDC client:
mcd-tracker-app(for SvelteKit) andmcd-tracker-ios(for Swift, public client + PKCE) - Add
src/mcd_tracker_api/auth.py— JWKS fetch, JWT decode, User dataclass, require_role dependency factory (copy basketball-api pattern) - Add config:
MCD_TRACKER_KEYCLOAK_REALM_URL - Update deployment-patch.yaml with Keycloak env var
- 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-apiForgejo Issue: TBD
Steps:
- Routes:
POST /locations— save a McDonald's location (name, address)GET /locations— list user's saved locationsPOST /locations/{id}/codes— log a coupon code at a locationGET /locations/{id}/codes— list codes at a locationPATCH /codes/{id}/redeem— mark code as redeemedGET /locations/{id}/slots— availability: slots remaining (0-5), next reopen dateGET /dashboard— all locations with slot status for current userGET /admin/stats— aggregate stats (admin only)
- 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
- Count active codes per location per user where
- 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
- 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-playgroundForgejo Issue: mcd-tracker-playground #4 (closed)
Steps:
- Decide: folder experiment in
html-playgroundor repo experiment (mcd-tracker-playground) - 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)
- Mobile-first design — this is a phone app at heart
- Lucas verifies on phone via Tailscale funnel
- 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-lifecyclecreated. 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-deploymentsForgejo Issue: TBD
Steps:
- Create
mcd-tracker-apprepo on Forgejo - SvelteKit scaffold with Auth.js + Keycloak OIDC (copy westside-app pattern)
- Direct port from playground: copy CSS → scoped styles, copy HTML → Svelte templates, replace hardcoded data with
{data.foo}bindings - Pages:
/(dashboard),/add(log code),/locations(manage),/history,/admin(stats) - Service onboarding for frontend: add
mcd-tracker-appto k3s.tfvars + kustomize overlay - 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:
- Install Xcode on MacBook Air M1 (App Store)
- Install Woodpecker agent binary (
brew install woodpecker-ci/tap/woodpecker-agentor direct download) - 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 - Create launchd plist for auto-start on boot
- Install Fastlane:
brew install fastlane - 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-iosForgejo Issue: TBD
Steps:
- Create
mcd-tracker-iosrepo on Forgejo - 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
- Keycloak OIDC auth via
ASWebAuthenticationSession(public client + PKCE —mcd-tracker-iosclient created in Phase 4) - API client layer — same endpoints as SvelteKit, JWT Bearer auth
.woodpecker.yamlwithlabels: [platform: darwin]— targets Mac agent- Pipeline:
xcodebuild test→xcodebuild 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-iosForgejo Issue: TBD
Steps:
- Enroll in Apple Developer Program ($99/yr)
- Fastlane setup:
fastlane match— manage signing certificates + provisioning profilesfastlane deliver— App Store metadata, screenshotsfastlane pilot— TestFlight uploads
- CI pipeline addition: after
xcodebuild archive, runfastlane pilot upload - TestFlight beta distribution — Lucas + testers validate
- App Store submission — metadata, screenshots, privacy policy, review
- 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 /eventsendpoint 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-appDepends 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-appDepends on: Phase 7 (frontend must exist)
Scope:
- API:
GET /statsendpoint — 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-apiDepends 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:
ReceiptItemtable (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-trackerexists, ArgoCD app visible - [ ] Phase 2:
curl https://mcd-tracker.tail5b443a.ts.net/healthzreturns 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}_PORTcollides with Pydantic Settings. Useserver_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_lengthhardening 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,
@htmlusage (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.
Related
project-mcd-tracker— project pageservice-onboarding-sop— Phase 1 procedureconvention-kustomize-overlay— Phase 1 overlay patternsop-frontend-experiment— Phase 6 playground workflowplan-pal-e-platform— platform this project runs onplan-pal-e-agency— SOPs and conventions this project follows
- [x] Project created in pal-e-docs (
Board 1
-
mcd-tracker
board-mcd-trackermcd-tracker
Doc 5
-
TODO: mcd-tracker API observability — Prometheus instrumentation + business metrics
todo-mcd-observability-apimcd-tracker API observability
What
Replace the stub
/metricsendpoint (hardcodedup 1) with real Prometheus instrumentation. Copy the pal-e-docs pattern exactly.Scope
- Add
prometheus-fastapi-instrumentatorto dependencies - Wire in
main.py— auto-instruments all endpoints (request rate, latency histograms, error rates by status code) - Remove manual
/metricsstub inroutes/health.py— the instrumentator handles it - Add custom business counters in route handlers:
mcd_receipts_uploaded_totalmcd_codes_saved_totalmcd_codes_redeemed_totalmcd_nearby_queries_total
- Existing ServiceMonitor already scrapes
/metricson port 8000 every 30s — no deployment changes needed
Pattern to follow
pal-e-docs/src/pal_e_docs/main.pylines 60-63:Instrumentator( should_ignore_untemplated=True, excluded_handlers=["/healthz", "/metrics"], ).instrument(app)Depends on
Nothing. Can start immediately.
- Add
-
TODO: Wire real camera + manual code entry (replace mock scan flow)
todo-mcd-real-scan-flowWire real camera + manual code entry
What
The scan flow in
scan/+page.svelteis 100% mocked — fake camera, hardcoded survey code, simulated OCR delay. Replace with real functionality:- Wire
@capacitor/camerafor 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.
- Wire
-
TODO: mcd-tracker Grafana dashboard + alert rules
todo-mcd-observability-dashboardmcd-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.json→mcd-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) - Clone
-
TODO: Smart proximity — query Postgres instead of Overpass
todo-mcd-smart-proximitySmart proximity — query Postgres instead of Overpass
What
Replace the Overpass-dependent
/locations/nearbyendpoint with a Postgres spatial query against the pre-seededmcdonalds_locationstable. Haversine function already exists inservices/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— queriesmcdonalds_locationstable - 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. - New endpoint or refactored
-
TODO: Pre-seed McDonald's locations into Postgres
todo-mcd-preseed-locationsPre-seed McDonald's locations into Postgres
What
One-time batch load of McDonald's locations from Overpass/OSM into a
mcdonalds_locationstable 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. - New table:
Project Page 1
-
mcd-tracker
project-mcd-trackermcd-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-deployUser (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-codeUser (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-spendingUser (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-codesActive: 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:
- DevOps first — service onboarding via pal-e-services + k8s manifests in pal-e-deployments. Production namespace ready before code is written.
- Backend second — FastAPI + Postgres (CNPG sidecar pattern). Alembic migrations. Keycloak JWT auth. Integration tests. Deployed to prod.
- Frontend third — HTML/CSS in
mcd-tracker-playground(pal-e-playground project). Once design locks, promote to SvelteKit inmcd-tracker-app. Deploy to prod. - 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):
- Domain Model (
arch-domain-mcd-tracker) — User, Location, Receipt, CouponUsage. Two codes: survey code (receipt) + BOGO code (coupon). Rolling 30-day window. - 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. - 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.
Repo Platform Role Status mcd-tracker-apiForgejo FastAPI backend LIVE — 8 endpoints, 58 tests mcd-tracker-playgroundForgejo HTML/CSS prototypes (served at playground.tail5b443a.ts.net/mcd-tracker/)Scaffolded — 7 pages, design iteration in progress mcd-tracker-appForgejo SvelteKit + 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-apiForgejo FastAPI backend Not created mcd-tracker-playgroundForgejo HTML/CSS design experiments Not created mcd-tracker-appForgejo SvelteKit frontend (promoted from playground) Not created mcd-tracker-iosForgejo 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/appKeycloak 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/prodInbox
Untriaged TODOs — discovered work awaiting scoping into the plan.
Slug Summary Discovered empty
Review 1
-
Review: Mount mcd-tracker-playground in playground nginx
review-199-2026-03-18Verdict: 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 haslocation /westside/block withalias /srv/westside-playground/. Addinglocation /mcd-tracker/withalias /srv/mcd-tracker-playground/follows the same pattern. - [x]
~/mcd-tracker-playground/— verified: directory exists on host with 14 HTML files includingindex.htmlandscan.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-deploymentsand both file targets live in that repo'soverlays/playground/prod/directory. Single-repo change, no cross-repo coordination needed.Dependencies
- Upstream:
~/mcd-tracker-playgrounddirectory 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 becauseindex.htmlexists in the playground directory and the nginxtry_filesdirective 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-discoveryGoal: 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-playgroundDepends 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_notestable (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.Related
plan-mcd-tracker— parent planphase-mcd-tracker-7-sveltekit— depends on dev overlaysop-capacitor-mobile-lifecycle— follows Vite-on-host local dev pattern
-
Phase 7: SvelteKit + Capacitor Frontend
phase-mcd-tracker-7-sveltekitGoal: 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-appDepends 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.cssused directly, HTML pasted into Svelte templates, hardcoded data replaced with{data.field}.SvelteKit Routes (from playground alignment)
Playground Route Data (client-side fetch) Interactivity index.html / None (public landing) Sign In → keycloak.login(). Create Account → keycloak.register(). In production, Keycloak handles auth UI — these are themed Keycloak pages. signin.html /signin None — redirect to Keycloak keycloak.login() redirect. Page only exists as Keycloak theme reference. On iOS: WebView opens Keycloak directly. register.html /register None — redirect to Keycloak keycloak.register() redirect. Same as signin — Keycloak theme reference. home.html /home GET /dashboard + GET /stats (XP/level) XP banner (level, codes redeemed). Location cards sorted by proximity with 5-day expiry warnings. Near Me GPS. Search + sort. Tap location → /locations/{id}. Floating scan FAB. location-detail.html /locations/{id} GET /locations/{id}/slots + GET /locations/{id}/codes Slot progress bar. Active codes with 5-day countdown + pulsing redeem button. Redeemed codes. Slot timeline (all 5 slots). Cashier overlay with receipt photo. Scan Receipt pre-selects this location. scan.html /scan GET /locations/nearby {#if currentStep} 4-step wizard. Permission card. Camera (Capacitor). Survey code confirm. Copy + open survey (Capacitor browser/clipboard). BOGO code entry + location picker. Save → /scan/success. save-success.html /scan/success None (data passed from scan) Code saved celebration. 5-day expiry warning. Slot status update. +1 XP animation. 'Use Now' → cashier overlay (direct, no navigation). 'Done' → /home. redeem-success.html /redeem/success None (data passed from redeem action) Free Food celebration. Lifetime count (#13). 'What did you get?' chip picker (Big Mac, McChicken, etc. → redeemed_item field). +2 XP animation. Slot reopen date. 'Scan Another' or 'Done'. history.html /history GET /receipts + GET /codes → merge + GET /stats Lifetime stats bar (scanned/earned/redeemed/expired). Date-grouped timeline. Color-coded events (scan=blue, earn=green, redeem=amber, expire=red). Tap event → /history/{id}. event-detail.html /history/{id} GET /codes/{id} or GET /receipts/{id} with related data Full event context: type, timestamp, location (tappable), BOGO code details, survey code, receipt photo, slot impact, XP earned. Cashier overlay (state-aware: active → 'Mark as Redeemed', redeemed → 'Already Redeemed' with item + date). Steps
- Create
mcd-tracker-apprepo on Forgejo. SvelteKit scaffold (adapter-static,ssr: falsefor Capacitor SPA mode). - Copy
app.cssfrom playground →src/app.css(direct, no changes). - Promote 7 content pages: copy HTML → Svelte template, replace hardcoded data with
{data.field}. Data loading via client-sidefetch()inonMount(NOT+page.server.ts). Pages:/(landing),/home,/locations/[id],/scan,/scan/success,/redeem/success,/history,/history/[id]. - Create 2 redirect stubs:
/signin→keycloak.login(),/register→keycloak.register(). These are not full page promotions — they call the Keycloak JS method and redirect. No HTML copy needed. - Auth: keycloak-js client-side OIDC (NOT Auth.js). Public client + PKCE. Initialize in
+layout.svelte(onMount). Auth guard blocks rendering until resolved./is public, all other routes require auth. Seeproject-capacitor-mobileauth-decision for full config (redirect URIs, token management, platform detection). - Backend prereqs: COMPLETED (PR #17, merged 2026-03-16).
GET /statsendpoint (XP, levels, lifetime counts) +redeemed_itemfield on redeem endpoint. 144 tests. No backend work needed. - Capacitor init:
npx cap init,npx cap add ios, configurecapacitor.config.ts. - Capacitor plugins:
@capacitor/camera,@capacitor/geolocation,@capacitor/clipboard,@capacitor/browser,@capacitor/app. - Service onboarding: add
mcd-tracker-appto pal-e-services + pal-e-deployments (namespace, Harbor project, ArgoCD app, kustomize overlay, Tailscale funnel). - Deploy to cluster, verify on phone — full flow: landing → auth → home → location → scan → save → use now → redeem → history → event detail.
Deliverables
- PR #2 merged (2026-03-16): SvelteKit + Capacitor scaffold
- 10 routes promoted from playground HTML (verbatim CSS, Svelte templates with data bindings)
- keycloak-js PKCE auth with auth guard in +layout.svelte
src/lib/keycloak.js(OIDC wrapper) +src/lib/api.js(Bearer token fetch) +src/lib/overlay.svelte.js(shared overlay state)- 41 tests (Vitest + @testing-library/svelte): build, routes, modules, components, overlay
- Dockerfile (nginx serving SPA) + .woodpecker.yaml (test + build-and-push)
- Capacitor config initialized (capacitor.config.ts, iOS added)
- PR #5 merged (2026-03-17): Docker Compose local dev stack. 4-service stack (postgres, keycloak, api, app).
dev/realm-export.jsonwith PKCE client + 3 test users.dev/seed-data.pytest data seeder..env.example.api.jsandkeycloak.jsnow use VITE_* env vars with production fallbacks. QA nit fixed:/healthzendpoint. - Remaining:
docker compose uplocal validation, service onboarding (pal-e-services + pal-e-deployments), deploy to cluster, verify on phone
Related
plan-mcd-tracker— parent planphase-mcd-tracker-6-playground— design source of truthsop-frontend-experiment— promotion procedureproject-capacitor-mobile— Capacitor patterns + plugin catalogconvention-frontend-css— CSS rules
- Create
-
Phase 7c: Local Dev Stack (Docker Compose + Tailscale)
phase-mcd-tracker-7c-local-devGoal: 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:8000api— mcd-tracker-api (FastAPI), port 8000.DATABASE_URLpointing to local postgres.KEYCLOAK_REALM_URLpointing to local keycloak.postgres— Postgres 16-alpine, seeded with test data (locations, codes, receipts)keycloak— Keycloak dev mode, pre-configuredmcd-trackerrealm withmcd-tracker-apppublic client, test users, redirect URIs for localhost
Environment variable:
- Add
VITE_API_URLtosrc/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(orhttp://api:8000for container-to-container) - Production build uses the default (no env var needed)
Tailscale funnel:
- Official URL like
mcd-dev.tail5b443a.ts.netor subpath on playground - Accessible from phone on Tailscale network
- Listed on
playground.tail5b443a.ts.nethub 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-experimentand Phase 7 steps: "rundocker compose up, test full flow locally, THEN push to prod" - Update
project-capacitor-mobilelocal dev workflow section (already created this session)
Deliverables
- pending
Related
plan-mcd-tracker— parent planphase-mcd-tracker-7-sveltekit— parent phaseproject-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-themeGoal: 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-trackerrealm - 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
Related
plan-mcd-tracker— parent planphase-mcd-tracker-7-sveltekit— parent phaseproject-capacitor-mobile— auth flow documentation
- Create a custom Keycloak theme for the
-
Phase 6: Frontend Playground
phase-mcd-tracker-6-playgroundGoal: 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-playgroundDepends on: Phase 5 (COMPLETED — API response schemas define the fake data structure)
Scope
Follow
sop-frontend-experimentrepo experiment procedure exactly:- Create Forgejo repo
forgejo_admin/mcd-tracker-playground✓ - Clone locally to
~/mcd-tracker-playground✓ - Initial scaffold: 6 pages with hardcoded data ✓ (index, home, scan, codes, locations, history)
- CSS following
convention-frontend-css: Atkinson Hyperlegible, design tokens, mobile-first ✓ - Add nginx subpath + hostPath mount ✓
- Verify at
playground.tail5b443a.ts.net/mcd-tracker/✓ - 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.
- Lucas approves final design on phone → design lock
- Register repo in
project-frontend-playgroundRepos table - 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-playgroundPR #3
Related
plan-mcd-tracker— parent plansop-frontend-experiment— repo experiment procedureconvention-frontend-css— CSS rulesproject-frontend-playground— playground project
- Create Forgejo repo
-
Phase 7a: E2E + Integration Test Suite
phase-mcd-tracker-7a-testingGoal: 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-appDepends 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 datatestuser2@mcd-tracker.test— second user (isolation testing)testadmin@mcd-tracker.test— admin roleemptyuser@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
Related
plan-mcd-tracker— parent planphase-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-securityGoal: 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.netandcapacitor://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
Related
plan-mcd-tracker— parent planphase-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-intelligenceGoal: 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-apiDepends 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:
ReceiptItemtable (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
Related
plan-mcd-tracker— parent planphase-mcd-tracker-13-gamification— receipt data feeds gamification insightsphase-mcd-tracker-5a-receipt-model— receipt model + photo upload (foundation)
-
Phase 13: Gamification + XP System
phase-mcd-tracker-13-gamificationGoal: 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-appDepends on: Phase 7 (frontend must exist)
Scope
- API:
GET /statsendpoint — 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
Related
plan-mcd-tracker— parent planphase-mcd-tracker-14-receipt-intelligence— receipt data feeds gamification
- API:
-
Phase 10: App Store Submission
phase-mcd-tracker-10-app-storeGoal: mcd-tracker live on the App Store. Full CI/CD: push → TestFlight → promote.
Owner: Main session + Lucas
Repo:
forgejo_admin/mcd-tracker-appDepends on: Phase 9 (TestFlight working)
Scope
- App Store Connect setup: app record, categories, pricing (free)
- App icons (1024x1024 + all sizes)
- Screenshots (6.7" + 5.5" displays)
- Privacy policy URL
- App description, keywords (ASO)
- Submit for review
- CI pipeline: Fastlane
deliverfor metadata automation
Deliverables
- pending
Related
plan-mcd-tracker— parent planproject-capacitor-mobile— App Store requirements checklist
-
Phase 9: Capacitor iOS Build + Native Plugins
phase-mcd-tracker-9-capacitor-iosGoal: 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-appDepends on: Phase 7 (Capacitor configured), Phase 8 (Mac agent running)
Scope
- Add
.woodpecker.yamliOS build step withlabels: [platform: darwin] - Pipeline:
npm run build → npx cap sync → xcodebuild archive → export .ipa - Configure Xcode signing (Apple Developer Program required, $99/yr)
- Test Capacitor native plugins on device: camera (receipt photos), geolocation (Near Me), clipboard
- Fastlane
pilot upload→ TestFlight - Lucas installs via TestFlight, validates on iPhone
Deliverables
- pending
Related
plan-mcd-tracker— parent planproject-capacitor-mobile— plugin catalog, build pipeline, Info.plist requirements
- Add
-
Phase 8: Mac Woodpecker Agent
phase-mcd-tracker-8-mac-agentGoal: 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
- Install Xcode on MacBook Air M1
- Install Node.js, npm (for Capacitor builds)
- Install Woodpecker agent (brew or direct download)
- Configure: local backend,
WOODPECKER_FILTER_LABELS=platform=darwin - Create launchd plist for auto-start
- Install Fastlane:
brew install fastlane - Verify: agent visible in Woodpecker UI, test pipeline runs on Mac
Deliverables
- pending
Related
plan-mcd-tracker— parent planproject-capacitor-mobile— iOS build pipeline docsarch-deployment-mcd-tracker— Mac agent in deployment diagram
-
Phase 5c: GPS Nearby Locations (Overpass API)
phase-mcd-tracker-5c-gps-nearbyGoal: 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-apiDepends 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.- Add
GET /locations/nearby?lat=39.74&lng=-104.99&radius=5000endpoint:- 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
- Queries Overpass API:
- Add
src/mcd_tracker_api/services/overpass.py— Overpass API client (httpx) - Add distance calculation utility (Haversine formula for lat/lng distance)
- Update Location model: ensure latitude/longitude are required for new locations (nullable for legacy)
- 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
Related
phase-mcd-tracker-5-core-api— parent phaseplan-mcd-tracker— parent planarch-dataflow-mcd-tracker#flow-5-auto-detect-location— GPS flow
- Add
-
Phase 5a: Receipt Model + Photo Upload
phase-mcd-tracker-5a-receipt-modelGoal: 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-apiDepends on: Phase 5 (COMPLETED)
Scope
Implements the updated domain model from
arch-domain-mcd-tracker. Adds Receipt entity and updates CouponUsage.- Add
ReceiptSQLAlchemy model:- id (PK), keycloak_sub (indexed), location_id (FK), photo_path (string), survey_code (string), survey_completed (bool, default false), captured_at (timestamp)
- Update
CouponUsagemodel:- Add
receipt_id(FK, nullable — existing codes don't have receipts) - Rename
code→bogo_code - Rename
used_at→earned_at
- Add
- Alembic migration for both changes (add Receipt table, alter CouponUsage columns)
- Add photo upload PVC mount to kustomize overlay (
pal-e-deployments):- PVC:
receipt-uploads(1Gi) - Mount:
/data/uploads/receipts
- PVC:
- Add routes:
POST /receipts— multipart upload (photo + GPS coords). Saves photo to PVC, creates Receipt record, returns receipt_id + survey_code placeholderGET /receipts— list user's receiptsGET /receipts/{id}/photo— serve receipt photo
- Update
POST /codesto accept optionalreceipt_id - Update Pydantic schemas for new/changed fields
- 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
Related
phase-mcd-tracker-5-core-api— parent phaseplan-mcd-tracker— parent planarch-domain-mcd-tracker— Receipt entity + updated CouponUsagearch-dataflow-mcd-tracker#flow-1-scan-receipt— scan receipt flow
- Add
-
Phase 5: Core API Endpoints + Integration Tests
phase-mcd-tracker-5-core-apiGoal: Full CRUD API with rolling window logic, integration tests, deployed to prod. The app is functional.
Owner: Dev agent
Repo:
forgejo_admin/mcd-tracker-apiDepends 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
Related
plan-mcd-tracker— parent planphase-mcd-tracker-4-keycloak-auth— depends onarch-dataflow-mcd-tracker— runtime flowsarch-domain-mcd-tracker— rolling window queries
-
Phase 4: Keycloak Realm + Auth
phase-mcd-tracker-4-keycloak-authGoal: 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-apiDepends on: Phase 3 (COMPLETED — models + DB live)
Scope
- Create
mcd-trackerrealm 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)
- Realm roles:
- 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 factoryget_current_user()dependency
- JWKS fetch + cache from
- Add config:
MCD_TRACKER_KEYCLOAK_REALM_URL - Update kustomize deployment-patch.yaml with Keycloak env var (pal-e-deployments)
- Create test user + admin user in realm
- 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-trackeraccessible athttps://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 created —
mcd-trackerrealm 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
Related
plan-mcd-tracker— parent planphase-mcd-tracker-3-data-model— depends onarch-dataflow-mcd-tracker— auth flow diagram
- Create
-
Phase 3: Data Model + Postgres
phase-mcd-tracker-3-data-modelGoal: SQLAlchemy models defined, Alembic migrations running, DB-backed health check, deployed to prod.
Owner: Dev agent
Repo:
forgejo_admin/mcd-tracker-apiDepends 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.- Add
src/mcd_tracker_api/database.py— SQLAlchemy engine, SessionLocal, Base, get_db() dependency - 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)
- Add SQLAlchemy + psycopg2-binary + alembic to pyproject.toml dependencies
- Init Alembic:
alembic init alembic/, configurealembic/env.pyto use settings.database_url - Create first migration:
alembic revision --autogenerate -m "initial schema" - Add migration-on-startup to lifespan in main.py:
alembic.command.upgrade(config, "head") - Update
/healthzto verify DB connection (SELECT 1) - 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/healthzshows 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
Related
plan-mcd-tracker— parent planphase-mcd-tracker-2-backend-scaffold— depends onarch-domain-mcd-tracker— entity model this implements
- Add
-
Phase 1a: CI Validation for pal-e-deployments
phase-mcd-tracker-1a-ci-validationGoal: 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-deploymentsDepends on: None (can run in parallel with remaining Phase 1 work)
Scope
Add a
.woodpecker.yamlto 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 doneCatches: 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 - doneCatches: invalid field names, CRD mismatches, schema violations. Requires CI kubeconfig access (use node IP
10.0.0.217:6443, not127.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(not127.0.0.1— pipeline pods can't reach localhost) - CI needs a kubeconfig secret or service account token
- Image:
bitnami/kubectl:latestor 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.yamlwith 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
Related
phase-mcd-tracker-1-service-onboarding— parent phaseplan-mcd-tracker— parent plansop-platform-tf-changes— updated SOP with pre-merge patternstodo-pre-merge-infra-validation— claude-custom hook TODO
-
Phase 1: Service Onboarding
phase-mcd-tracker-1-service-onboardingGoal: 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-deploymentsDepends on: None
Forgejo Issue: forgejo_admin/pal-e-services #14
Follow
service-onboarding-sopandconvention-kustomize-overlayto onboard mcd-tracker as a new service on the platform. This is pure infrastructure — no application code.- Add
mcd-trackerentry topal-e-services/terraform/k3s.tfvarsvar.services map tofu plan -lock=false→ verify 6-7 new resourcestofu apply -lock=false- Create kustomize overlay:
pal-e-deployments/overlays/mcd-tracker/prod/
Follow
service-onboarding-sopandconvention-kustomize-overlayto onboard mcd-tracker as a new service on the platform. This is pure infrastructure — no application code.- Add
mcd-trackerentry topal-e-services/terraform/k3s.tfvarsvar.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" } tofu plan -lock=false→ verify 6-7 new resources (namespace, Harbor project, robot accounts, pull secret, ArgoCD app, funnel ingress)tofu apply -lock=false- Create kustomize overlay:
pal-e-deployments/overlays/mcd-tracker/prod/kustomization.yaml— base ref (../../../bases/standard), rename patches (app → mcd-tracker-api), images transformerdeployment-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-trackervisible in ArgoCD UI (will be degraded/missing until first image push — expected) - Harbor project
mcd-trackerexists 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-secretsk8s Secret.
- pending
Related
plan-mcd-tracker— parent planservice-onboarding-sop— procedureconvention-kustomize-overlay— overlay patternarch-deployment-mcd-tracker— deployment diagram
- Add
-
Phase 2: Backend Scaffold + First Deploy
phase-mcd-tracker-2-backend-scaffoldGoal: FastAPI skeleton running in prod — health endpoint responding, CI green, ArgoCD syncing. ImagePullBackOff resolves.
Owner: Dev agent
Repo:
forgejo_admin/mcd-tracker-apiDepends 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.
- Scaffold FastAPI app:
src/mcd_tracker_api/main.py— FastAPI app with lifespansrc/mcd_tracker_api/config.py— Pydantic Settings withMCD_TRACKER_env prefixsrc/mcd_tracker_api/routes/health.py—/healthzendpointpyproject.toml— project metadata + dependencies (fastapi, uvicorn, pydantic-settings)
- Add
Dockerfile— multi-stage Python build (builder + runtime), expose 8000 - Add
.woodpecker.yaml— two steps:test(ruff + pytest) andbuild-and-push(kaniko to Harbor). Use${CI_COMMIT_SHA}as image tag. - Activate Woodpecker on mcd-tracker-api repo
- Add Harbor secrets to Woodpecker repo settings (
harbor_username,harbor_password— get fromtofu output) - 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/healthzreturns 200- Woodpecker pipeline green on mcd-tracker-api
- Harbor shows
mcd-tracker/apiimage with SHA tag - ArgoCD app
mcd-trackerhealthy (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 Settingsport: int. Renamed toserver_host/server_port. Pattern to watch for when service name matches env prefix.
- pending
Related
plan-mcd-tracker— parent planphase-mcd-tracker-1-service-onboarding— depends on (infra ready)arch-deployment-mcd-tracker— deployment diagram
- Scaffold FastAPI app:
Architecture 3
-
Architecture: Deployment — mcd-tracker
arch-deployment-mcd-trackerArchitecture: 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_AGENTDevelopment 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] endService 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 jobsAuto-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=darwinroutes 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-trackerrealm 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.
Related
arch-domain-mcd-tracker— entity modelarch-dataflow-mcd-tracker— runtime flowsproject-mcd-tracker— project pageservice-onboarding-sop— how services join the clusterconvention-kustomize-overlay— deployment manifest pattern
- Dual-agent CI. Linux agent handles backend + frontend (Docker/kaniko). Mac agent handles iOS (local/shell). Pipeline label
-
Architecture: Data Flow — mcd-tracker
arch-dataflow-mcd-trackerArchitecture: 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 resourceKey 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.
Related
arch-domain-mcd-tracker— entity model these flows operate onarch-deployment-mcd-tracker— where these services runproject-mcd-tracker— project page
-
Architecture: Domain Model — mcd-tracker
arch-domain-mcd-trackerArchitecture: 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 (
subclaim).keycloak_subis 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.
sourcetracks 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();Related
project-mcd-tracker— project pagearch-dataflow-mcd-tracker— runtime flowsarch-deployment-mcd-tracker— infrastructure
- No local User table. Identity comes from Keycloak JWT (
Repos 2
-
mcd-tracker-appactive
-
mcd-tracker-apiactive