Files
recipe/.planning/ROADMAP.md
ulfrxdev 3122fdaf37 docs(02-02): complete server auth boundary plan
- add execution summary with verification and deviations

- update state, roadmap progress, and completed auth requirements
2026-04-28 13:46:46 +02:00

246 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Roadmap: Recipe
**Core Value:** "My week is planned." I pick recipes, the calendar fills up, and I know what we're eating.
**Granularity:** Fine (11 phases)
**Mode:** YOLO
**Source of truth:** Derived from `.planning/REQUIREMENTS.md` (72 v1 requirements) guided by `.planning/research/SUMMARY.md` (suggested skeleton) and `.planning/research/ARCHITECTURE.md` (build-order reasoning).
## Phases
- [x] **Phase 1: Project Infrastructure & Module Wiring** — Running-but-empty KMP client + Ktor server with all build infra baked in
- [ ] **Phase 2: Authentication Foundation** — User signs in through Authentik (OIDC+PKCE) and the server validates tokens
- [ ] **Phase 3: Households, Membership & Server Data Foundation** — Users create/join households; server enforces household scope
- [ ] **Phase 4: Sync Engine Skeleton** — Offline-first read/write with outbox-backed LWW sync on a sentinel table
- [ ] **Phase 5: Recipe Catalog (Read Path)** — User browses, filters, and opens recipe details from a seeded catalog
- [ ] **Phase 6: Meal Planner — Core Write Path** — User picks recipes into the 5-slot calendar; first real outbox-backed aggregate
- [ ] **Phase 7: Meal Planner — Customization & Nutrition** — User tweaks servings/ingredients/products per meal entry and sees daily nutrition
- [ ] **Phase 8: Pantry** — User tracks what's on hand and sees shortfalls against the plan
- [ ] **Phase 9: Shopping List & Session Log** — User generates a grouped shopping list from the plan and shops with "bought" tracking
- [ ] **Phase 10: UI Chrome & Haze Liquid-Glass Polish** — Tab/nav glass effects, iOS-idiomatic chrome, calmer visual hierarchy
- [ ] **Phase 11: Localization & iOS Deployment** — Full Polish copy pass, i18n-ready resources, TestFlight to partner
## Phase Summary Table
| # | Name | Goal (one line) | Requirements | #SC |
|---|------|-----------------|--------------|-----|
| 1 | Project Infrastructure & Module Wiring | KMP client + Ktor server build cleanly with convention plugins, version catalog, iOS binary flags, and a shared DTO module | INFRA-01, INFRA-02, INFRA-03, INFRA-06 | 4 |
| 2 | Authentication Foundation | End-to-end OIDC+PKCE login to Authentik with JIT user provisioning and server-side JWT validation | AUTH-01, AUTH-02, AUTH-03, AUTH-04, AUTH-05, AUTH-06 | 5 |
| 3 | Households, Membership & Server Data Foundation | Create/join households via invites; every request carries a household-scoped principal derived from JWT | HSHD-01, HSHD-02, HSHD-03, HSHD-04, HSHD-05, HSHD-06, HSHD-07, INFRA-05 | 5 |
| 4 | Sync Engine Skeleton | Outbox-backed LWW sync works round-trip on a sentinel table with server-assigned timestamps and cursor pull | SYNC-01, SYNC-02, SYNC-03, SYNC-04, SYNC-05, SYNC-06, SYNC-07, SYNC-08, SYNC-09, SYNC-10 | 5 |
| 5 | Recipe Catalog (Read Path) | User browses a seeded recipe catalog, filters/searches, and opens a detail view — offline-capable | RCPE-01, RCPE-02, RCPE-03, RCPE-04, RCPE-05, RCPE-06, RCPE-07, RCPE-08, UI-05, UI-08 | 5 |
| 6 | Meal Planner — Core Write Path | User fills the 5-slot calendar with recipes; adds/removes/replaces/skips; writes survive offline and sync | PLAN-01, PLAN-02, PLAN-03, PLAN-04, PLAN-05, PLAN-06, PLAN-12, PLAN-14 | 5 |
| 7 | Meal Planner — Customization & Nutrition | User customizes ingredients per meal entry and sees daily macro totals that respect customizations | PLAN-07, PLAN-08, PLAN-09, PLAN-10, PLAN-11, PLAN-13 | 4 |
| 8 | Pantry | User manages pantry inventory by category and sees shortfalls for a chosen horizon | PNTR-01, PNTR-02, PNTR-03, PNTR-04, PNTR-05 | 4 |
| 9 | Shopping List & Session Log | User generates a category-grouped shopping list and marks items bought during a session | SHOP-01, SHOP-02, SHOP-03, SHOP-04, SHOP-05, SHOP-06 | 4 |
| 10 | UI Chrome & Haze Liquid-Glass Polish | 4-tab nav with independent back stacks, Haze glass chrome, iOS idioms, breathing-room visual hierarchy | UI-03, UI-04, UI-06, UI-07, UI-09 | 5 |
| 11 | Localization & iOS Deployment | All strings externalized, Polish copy throughout, partner installs via TestFlight | UI-01, UI-02, INFRA-04, INFRA-07 | 4 |
## Phase Details
### Phase 1: Project Infrastructure & Module Wiring
**Goal:** Stand up a KMP + Ktor repo whose build is "boring correct" from day 1 — version catalog, convention plugins, iOS binary flags, and a pure-Kotlin `shared/` module — so every later phase slots into an already-configured system.
**Depends on:** Nothing (first phase)
**Requirements:** INFRA-01, INFRA-02, INFRA-03, INFRA-06
**Success Criteria** (what must be TRUE):
1. `./gradlew build` succeeds across `composeApp`, `server`, `shared`, and produces an iOS framework and an Android APK from the bare template screens.
2. All library versions are resolved through `gradle/libs.versions.toml`; no version literals exist inside any `build.gradle.kts`.
3. iOS `gradle.properties` carry `kotlin.native.binary.objcDisposeOnMain=false` and `kotlin.native.binary.gc=cms`; a debug launch on simulator boots without warnings about legacy memory-management flags.
4. `build-logic/` convention plugins apply the Kotlin/Compose/test configuration to every module — adding a new module requires only applying a convention plugin, not copying compiler args.
5. `shared/commonMain` contains only domain models + serializable DTOs; no Ktor, Compose, or SQLDelight imports appear anywhere under `shared/`.
**Plans:** 7 plans
Plans:
- [ ] 01-01-PLAN.md — Version catalog extensions (Koin/Kermit/Spotless/Flyway/Postgres) + iOS K/N flags + verify-*.sh invariant scripts
- [ ] 01-02-PLAN.md — build-logic/ included build with 5 precompiled plugins (recipe.quality, recipe.kotlin.multiplatform, recipe.compose.multiplatform, recipe.android.application, recipe.jvm.server) + root settings.gradle.kts includeBuild wiring
- [ ] 01-03-PLAN.md — Module refactor: composeApp/shared/server build.gradle.kts apply recipe.* conventions; drop js target; enable explicitApi() on shared/
- [ ] 01-04-PLAN.md — Koin + Kermit bootstrap across all 4 platforms (commonMain Koin.kt/AppModule.kt/Logging.kt; iOS KoinIos.kt bridge; Android MainApplication.kt + manifest; JVM/Wasm main() rewrites; iOSApp.swift wiring)
- [ ] 01-05-PLAN.md — Server /health + Flyway bootstrap + HOCON config (application.conf, Database.kt with fail-loud contract, db/migration/.gitkeep, ApplicationTest.kt covers /health without Postgres)
- [ ] 01-06-PLAN.md — docker-compose.yml (postgres:16) + README.md Local development section (drops js docs)
- [ ] 01-07-PLAN.md — shared/ package scaffold + full green-build gate (spotlessApply, verify-*.sh, ./gradlew build, ./gradlew check)
**UI hint:** no
**Research flag:** no
### Phase 2: Authentication Foundation
**Goal:** Deliver a working end-to-end login: the app opens Authentik via OIDC (authorization code + PKCE), stores the tokens securely, and the Ktor server validates them on a protected `/api/v1/me` endpoint, JIT-provisioning users on first sign-in.
**Depends on:** Phase 1
**Requirements:** AUTH-01, AUTH-02, AUTH-03, AUTH-04, AUTH-05, AUTH-06
**Success Criteria** (what must be TRUE):
1. From a fresh install on iOS, I can tap "Zaloguj się", complete Authentik's hosted login, and land back in the app as an authenticated user.
2. I close and reopen the app an hour later; I am still signed in without re-entering credentials (refresh token flow runs transparently).
3. I tap "Wyloguj się"; the app returns to the login screen and the stored tokens are gone from Keychain/EncryptedSharedPreferences.
4. Calling `GET /api/v1/me` with a valid token returns my user record; the same call with a missing, expired, or wrong-audience token returns 401.
5. My user row exists in the server DB after my first successful login, keyed by the OIDC `sub` claim (no manual user creation needed).
**Plans:** 7 plans
Plans:
- [x] 02-01-PLAN.md — Shared auth contracts, dependency aliases, Authentik setup docs, and source audit
- [x] 02-02-PLAN.md — Server JWT validation, JWKS hardening, JIT users, and `/api/v1/me`
- [ ] 02-03-PLAN.md — Common OIDC/store contracts, JVM/Wasm actuals, and store contract test
- [ ] 02-04-PLAN.md — Android AppAuth actual, Android secure AuthState store, and manifest callback
- [ ] 02-05-PLAN.md — iOS AppAuth actual, iOS Keychain store, URL scheme, Swift callback, and Podfile
- [ ] 02-06-PLAN.md — AuthSession state machine, bearer HTTP client, refresh/logout behavior, and Koin wiring
- [ ] 02-07-PLAN.md — Compose auth gate UI, Polish resource strings, and iOS Authentik UAT
**UI hint:** yes
**Research flag:** yes
### Phase 3: Households, Membership & Server Data Foundation
**Goal:** Introduce the tenancy model before any feature tables land — `households`, `memberships`, `invites` with Flyway migrations; server's `PrincipalResolver` maps JWT `sub` to an active `householdId`; client finishes onboarding by creating or joining a household.
**Depends on:** Phase 2
**Requirements:** HSHD-01, HSHD-02, HSHD-03, HSHD-04, HSHD-05, HSHD-06, HSHD-07, INFRA-05
**Success Criteria** (what must be TRUE):
1. On my first login, I see an onboarding screen asking me to create a new household or enter an invite code.
2. I create a household, receive a short-lived single-use invite code, send it to my partner, and they redeem it to join the same household.
3. Once both users are in the same household, any household-scoped API call returns identical data regardless of which member made it.
4. A crafted API request that puts a different `household_id` in the body is ignored — the server always derives `household_id` from the authenticated principal, not the payload.
5. The server starts up and Flyway automatically applies `V1__init.sql` (or equivalent) in the correct order; restarting the server twice in a row is idempotent.
**Plans:** TBD
**UI hint:** yes
**Research flag:** no
### Phase 4: Sync Engine Skeleton
**Goal:** Build the offline-first spine — a Koin-singleton `SyncEngine` that owns the outbox and pull cursor, server endpoints `POST /sync/push` + `GET /sync/pull?since=`, and a sentinel table round-trips through it — so every later feature just adds a table, not a sync strategy.
**Depends on:** Phase 3
**Requirements:** SYNC-01, SYNC-02, SYNC-03, SYNC-04, SYNC-05, SYNC-06, SYNC-07, SYNC-08, SYNC-09, SYNC-10
**Success Criteria** (what must be TRUE):
1. I write to the sentinel table while the app is offline (airplane mode); the write appears instantly in the UI, and when I reconnect it reaches the server within seconds without manual intervention.
2. My partner edits the same sentinel row on their device; within the poll interval (2030 s while foregrounded) I see their change, and if we both edited concurrently the server's later-assigned `updated_at` wins with no silent data loss.
3. I delete a sentinel row on device A; after sync the row is gone on device B — and if I re-create "the same" row it comes back with a fresh UUID identity and does not resurrect old fields.
4. Killing the app with pending writes in the outbox and relaunching later preserves those writes; they drain on the next sync cycle.
5. Network failures and 5xx responses trigger exponential backoff retries without blocking the UI; no feature code issues HTTP sync writes directly — all go through the `SyncEngine`.
**Plans:** TBD
**UI hint:** no
**Research flag:** yes
### Phase 5: Recipe Catalog (Read Path)
**Goal:** Deliver the first real user-visible feature — a browseable recipe catalog — via a pull-only cache path that exercises Exposed + SQLDelight + Ktor + Coil end-to-end without write-path complexity, seeded server-side so the rest of the app has real data to develop against.
**Depends on:** Phase 4
**Requirements:** RCPE-01, RCPE-02, RCPE-03, RCPE-04, RCPE-05, RCPE-06, RCPE-07, RCPE-08, UI-05, UI-08
**Success Criteria** (what must be TRUE):
1. I open "Przepisy" and see a grid of recipe cards with thumbnail, title, and cooking time — fully populated from server-seeded catalog data.
2. I can filter the grid by meal slot, tag, and cooking-time range, and search by title/tag text; results update as I type.
3. I tap a recipe and see a detail view with ingredients (amounts + units), steps, nutrition per serving, cooking time, and any defined substitutions.
4. I put the device in airplane mode, relaunch the app, and the catalog still renders from the local SQLDelight cache.
5. The app respects my system light/dark appearance setting, and recipe-list dates/times render in Polish locale format (day and month names in Polish).
**Plans:** TBD
**UI hint:** yes
**Research flag:** no
### Phase 6: Meal Planner — Core Write Path
**Goal:** Ship the hero feature's skeleton — the calendar with 5 slots per day — as the first real household-scoped write aggregate. Every add/remove/replace/skip/serving-change goes through the outbox, proving the sync spine on realistic load.
**Depends on:** Phase 5
**Requirements:** PLAN-01, PLAN-02, PLAN-03, PLAN-04, PLAN-05, PLAN-06, PLAN-12, PLAN-14
**Success Criteria** (what must be TRUE):
1. I open "Planer", navigate between days/weeks/months, and see each day's 5 slots (śniadanie, drugie śniadanie, obiad, przekąska, kolacja) with whatever I've planned.
2. I can tap a slot, pick a recipe from the catalog, and see it appear instantly — even while offline — then reappear on my partner's device after a sync cycle.
3. I can remove a meal entry, replace it with a different recipe, adjust its servings (112), and mark a slot as "skipped" for a specific day.
4. Every meal entry has a stable UUID identity; deleting and re-adding the same recipe on the same (day, slot) creates a distinct new entry rather than reviving the old one.
5. Two household members concurrently editing the same slot converge deterministically on whichever edit the server stamped last, with no silent data loss.
**Plans:** TBD
**UI hint:** yes
**Research flag:** no
### Phase 7: Meal Planner — Customization & Nutrition
**Goal:** Flesh out the hero feature with per-entry customization (substitutions, excludes, extras, amount overrides, product/pack choice) and the nutrition numbers that close the "am I eating right" loop — all while respecting customizations so the math is honest.
**Depends on:** Phase 6
**Requirements:** PLAN-07, PLAN-08, PLAN-09, PLAN-10, PLAN-11, PLAN-13
**Success Criteria** (what must be TRUE):
1. On any meal entry I can substitute an ingredient with one of the catalog-defined alternatives and the change sticks after sync and restart.
2. On any meal entry I can exclude an ingredient, add an extra ingredient from the catalog (amount + unit), and override an ingredient's amount — each of these reflects in shopping/pantry calculations later.
3. On any meal entry I can select a specific product (pack size) for a given ingredient when multiple exist.
4. Each day in the planner shows daily nutrition totals (kcal, protein, fat, carbs) aggregated across all planned meals for that day, recomputed when any customization changes.
**Plans:** TBD
**UI hint:** yes
**Research flag:** no
### Phase 8: Pantry
**Goal:** Give the household a view of what's actually on hand and what's missing, so the plan connects to real life. Reuses the Phase 4 sync foundation on a second household-scoped aggregate.
**Depends on:** Phase 7
**Requirements:** PNTR-01, PNTR-02, PNTR-03, PNTR-04, PNTR-05
**Success Criteria** (what must be TRUE):
1. I open "Spiżarnia" and see my pantry inventory grouped by category (pieczywo, nabiał, mięso i ryby, warzywa, owoce, suche, przyprawy, inne).
2. I can manually add or update the quantity of any pantry ingredient using its pantry unit (g, ml, szt.), and the change syncs to my partner's device.
3. I pick a planning horizon (e.g., "next 7 days") and see which ingredients fall short based on the plan minus current pantry.
4. I can filter the pantry view by category and by shortfall status (needed / sufficient / not in plan).
**Plans:** TBD
**UI hint:** yes
**Research flag:** no
### Phase 9: Shopping List & Session Log
**Goal:** Close the loop from plan to store — generate a category-grouped shopping list from a chosen date range, mark items bought during an in-store session, and move bought items into the pantry automatically.
**Depends on:** Phase 8
**Requirements:** SHOP-01, SHOP-02, SHOP-03, SHOP-04, SHOP-05, SHOP-06
**Success Criteria** (what must be TRUE):
1. I open "Zakupy", pick a date range from the plan, and see a shopping list aggregating ingredient needs minus current pantry, grouped by category for an efficient store trip.
2. During a shopping session I can mark an item bought; it disappears from active needs and shows up in the pantry in its pantry unit.
3. I can undo a recently marked-bought item within the same session; the item reappears in active needs.
4. I close and reopen the app mid-shopping; my session's bought/unbought state is still there until I explicitly clear it.
**Plans:** TBD
**UI hint:** yes
**Research flag:** no
### Phase 10: UI Chrome & Haze Liquid-Glass Polish
**Goal:** Swap the boring default chrome used in Phases 59 for the intended Liquid-Glass-inspired feel — 4-tab bottom nav with independent back stacks, Haze-based blur on tab/nav chrome, iOS-idiomatic safe-area/keyboard/swipe-back behaviors, and a calmer spacing/typography pass across every screen. Measurable against realistic data already present.
**Depends on:** Phase 9
**Requirements:** UI-03, UI-04, UI-06, UI-07, UI-09
**Success Criteria** (what must be TRUE):
1. The app has a 4-tab bottom nav (Przepisy / Planer / Spiżarnia / Zakupy); tapping into a recipe detail, switching tabs, and coming back preserves the detail — each tab keeps its own back stack.
2. The tab bar and top nav bar use Haze-based translucent blur over content beneath them, consistent in light and dark schemes, and scrolling a full recipe grid on iPhone 11 stays above ~55 fps.
3. The app respects iOS safe areas, supports the swipe-back gesture where applicable, and keyboards never cover focused inputs.
4. Typography and spacing feel noticeably calmer than the legacy PWA mockup — more whitespace between cards, larger hit targets, readable at arm's length.
5. On a fresh install I never see a blank flash on launch, and every main screen (catalog / planner / pantry / shopping) renders a deliberate empty state when there's nothing to show yet.
**Plans:** TBD
**UI hint:** yes
**Research flag:** yes
### Phase 11: Localization & iOS Deployment
**Goal:** Externalize every string into Compose resources with complete Polish copy (correct plural forms), build and deploy the Ktor server image to the homelab alongside Authentik, and get the iOS build into my partner's hands via TestFlight.
**Depends on:** Phase 10
**Requirements:** UI-01, UI-02, INFRA-04, INFRA-07
**Success Criteria** (what must be TRUE):
1. Every user-facing string across every screen is resolved through Compose resources — a grep for raw Polish literals inside composables returns only fixture/test data.
2. The whole app reads as correctly-grammatical Polish, including plural forms (1 / 2 / 5 / 22 counts all render with the right form) and date/weekday names.
3. The Ktor server builds into a Docker image and is running in the homelab reachable over HTTPS with a real (Let's-Encrypt-issued) cert, alongside Authentik.
4. My partner installs the iOS app through TestFlight, signs in through Authentik, joins our household via invite, and can plan a meal that I see on my device.
**Plans:** TBD
**UI hint:** yes
**Research flag:** no
## Progress Table
| Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------|
| 1. Project Infrastructure & Module Wiring | 7/7 | Complete | 2026-04-24 |
| 2. Authentication Foundation | 2/7 | Executing | - |
| 3. Households, Membership & Server Data Foundation | 0/0 | Not started | - |
| 4. Sync Engine Skeleton | 0/0 | Not started | - |
| 5. Recipe Catalog (Read Path) | 0/0 | Not started | - |
| 6. Meal Planner — Core Write Path | 0/0 | Not started | - |
| 7. Meal Planner — Customization & Nutrition | 0/0 | Not started | - |
| 8. Pantry | 0/0 | Not started | - |
| 9. Shopping List & Session Log | 0/0 | Not started | - |
| 10. UI Chrome & Haze Liquid-Glass Polish | 0/0 | Not started | - |
| 11. Localization & iOS Deployment | 0/0 | Not started | - |
## Coverage Summary
- **v1 requirements total:** 72 (AUTH=6, HSHD=7, RCPE=8, PLAN=14, PNTR=5, SHOP=6, SYNC=10, UI=9, INFRA=7)
- **Mapped to phases:** 72
- **Unmapped:** 0
- **Coverage:** 100%
---
*Roadmap created: 2026-04-23*
*Granularity: fine (11 phases) | Mode: yolo*