diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md new file mode 100644 index 0000000..81c728c --- /dev/null +++ b/.planning/REQUIREMENTS.md @@ -0,0 +1,171 @@ +# Requirements: Recipe + +**Defined:** 2026-04-24 +**Core Value:** "My week is planned." I pick recipes, the calendar fills up, and I know what we're eating. + +## v1 Requirements + +### Authentication & identity + +- [ ] **AUTH-01**: User can sign in via the self-hosted Authentik instance using OIDC (authorization code flow with PKCE) +- [ ] **AUTH-02**: Client stores access + refresh tokens securely (iOS Keychain / Android EncryptedSharedPreferences) +- [ ] **AUTH-03**: Ktor server validates incoming access tokens via Authentik's JWKS endpoint (issuer, audience, expiry, signature, clock skew leeway) +- [ ] **AUTH-04**: User session persists across app launches without re-authentication (token refresh handled transparently) +- [ ] **AUTH-05**: User can sign out, which revokes local tokens and returns to the login screen +- [ ] **AUTH-06**: Users are JIT-provisioned in the server database on first successful login (by OIDC `sub` claim) + +### Household sharing + +- [ ] **HSHD-01**: On first login, user is prompted to create a new household or join an existing one via invite code +- [ ] **HSHD-02**: User can create a new household and become its first member +- [ ] **HSHD-03**: User can generate an invite code for their household (short-lived, single-use) +- [ ] **HSHD-04**: Another user can redeem an invite code to join the household +- [ ] **HSHD-05**: All household members see the same plan, pantry, and shopping list +- [ ] **HSHD-06**: Server-side tenancy is enforced: every household-scoped query filters by `household_id` derived from the authenticated principal, never from the request body +- [ ] **HSHD-07**: Client queries for household data filter locally by active `household_id` (defense in depth) + +### Recipes — browse & detail + +- [ ] **RCPE-01**: User can view a grid of the household's recipe catalog +- [ ] **RCPE-02**: User can filter recipes by meal slot (śniadanie / drugie śniadanie / obiad / przekąska / kolacja) +- [ ] **RCPE-03**: User can filter recipes by tag (e.g., "szybkie", "wegetariańskie", "wysokobiałkowe") +- [ ] **RCPE-04**: User can filter recipes by cooking time range (minutes) +- [ ] **RCPE-05**: User can search recipes by title or tag text +- [ ] **RCPE-06**: User can open a recipe detail view showing ingredients (with amounts + units), steps, nutrition per serving, and cooking time +- [ ] **RCPE-07**: Recipe detail shows ingredient alternatives (substitutions) where defined in the catalog +- [ ] **RCPE-08**: Recipe catalog is seeded via server-side SQL fixtures or admin CLI (no in-app authoring in v1) + +### Meal planner (hero feature) + +- [ ] **PLAN-01**: User can view a calendar showing planned meals per day +- [ ] **PLAN-02**: User can navigate between days/weeks/months in the calendar +- [ ] **PLAN-03**: User can add a recipe to any of the 5 slots on any day (śniadanie, drugie śniadanie, obiad, przekąska, kolacja) +- [ ] **PLAN-04**: User can remove a meal entry from a slot +- [ ] **PLAN-05**: User can replace a meal entry by picking a different recipe +- [ ] **PLAN-06**: User can adjust servings on a meal entry (1–12) +- [ ] **PLAN-07**: User can substitute an ingredient in a meal entry with one of the defined alternatives +- [ ] **PLAN-08**: User can exclude an ingredient from a meal entry (won't appear in shopping/pantry calculations) +- [ ] **PLAN-09**: User can add an extra ingredient to a meal entry (amount + unit from the ingredient catalog) +- [ ] **PLAN-10**: User can override the amount of an ingredient in a meal entry +- [ ] **PLAN-11**: User can select a specific product (pack size) for a given ingredient in a meal entry +- [ ] **PLAN-12**: User can mark a meal slot as skipped for a day +- [ ] **PLAN-13**: User sees daily nutrition totals (kcal, protein, fat, carbs) aggregated from all planned meals that day, respecting customizations +- [ ] **PLAN-14**: Meal entries have stable UUID identity (never composite keys like `date + slot`) to survive concurrent edits + +### Pantry + +- [ ] **PNTR-01**: User can view pantry inventory grouped by ingredient category (pieczywo, nabiał, mięso i ryby, warzywa, owoce, suche, przyprawy, inne) +- [ ] **PNTR-02**: User can manually add or update the quantity of an ingredient in the pantry (using its pantry unit: g, ml, szt.) +- [ ] **PNTR-03**: User sees which ingredients fall short over a user-selected planning horizon (e.g., next 7 days) based on the plan minus current pantry +- [ ] **PNTR-04**: User can filter pantry view by category +- [ ] **PNTR-05**: User can filter pantry view by shortfall status (needed / sufficient / not in plan) + +### Shopping list + +- [ ] **SHOP-01**: User can select a date range from the plan to generate a shopping list +- [ ] **SHOP-02**: Shopping list aggregates all ingredient needs across selected days minus current pantry quantities +- [ ] **SHOP-03**: Shopping list groups items by ingredient category for efficient in-store navigation +- [ ] **SHOP-04**: User can mark an item as bought during a shopping session; the item is removed from active needs and added to pantry in its pantry unit +- [ ] **SHOP-05**: User can undo a recently marked-bought item within the same session +- [ ] **SHOP-06**: Session log persists across app restarts until the user explicitly clears it + +### Offline + sync + +- [ ] **SYNC-01**: Client reads all household data from local SQLDelight without requiring network +- [ ] **SYNC-02**: Client writes all household data locally first (optimistic UI), then queues the change for sync +- [ ] **SYNC-03**: Server assigns `updated_at` (server time, monotonic) on every accepted write; clients never trust their own device clock for sync ordering +- [ ] **SYNC-04**: Client pulls household changes via `GET /sync/pull?since=...` using a lexicographic `(updated_at, id)` cursor to survive same-millisecond writes +- [ ] **SYNC-05**: Client pushes pending writes via `POST /sync/push` (batched); accepted writes come back with server-assigned `updated_at` +- [ ] **SYNC-06**: Deletes are soft deletes (`deleted_at` column); synced as state changes, not as "delete ops" +- [ ] **SYNC-07**: Sync runs on: app foreground, pull-to-refresh, debounced 2s after every local write, and polls every 20–30s while foreground +- [ ] **SYNC-08**: Sync failures (network error, 5xx) retry with exponential backoff and do not block the UI +- [ ] **SYNC-09**: Sync engine is implemented as a single Koin singleton owning the outbox queue and pull cursor — features never issue HTTP writes directly +- [ ] **SYNC-10**: When two household members edit the same row, the server-assigned later write wins; no silent data loss because writes use UUID identity, not natural keys + +### UI foundation (polish + glass aesthetic) + +- [ ] **UI-01**: All user-facing strings are externalized as Compose resources (i18n-ready), even though v1 ships Polish only +- [ ] **UI-02**: App ships with Polish-language copy throughout +- [ ] **UI-03**: Bottom tab navigation with 4 tabs: Przepisy / Planer / Spiżarnia / Zakupy, each preserving its own back stack independently +- [ ] **UI-04**: Tab bar and nav bar use Haze-based glass/blur effects (Liquid Glass approximation) +- [ ] **UI-05**: App supports light and dark color schemes with translucent surfaces working in both +- [ ] **UI-06**: UI is iOS-idiomatic within Compose constraints (safe areas, swipe-back gesture where applicable, keyboard avoidance) +- [ ] **UI-07**: Visual hierarchy is less cramped than the mockup — deliberate spacing, calmer typography, readable at arm's length +- [ ] **UI-08**: Locale-aware date formatting for display (days, months, weekday names in Polish); sync wire-format stays UTC ISO-8601 +- [ ] **UI-09**: App starts cleanly on first launch (no blank flash) and shows appropriate empty states when catalog / plan / pantry / shopping are empty + +### Infrastructure & build + +- [ ] **INFRA-01**: Gradle version catalog at `gradle/libs.versions.toml` is the single source of truth for library versions +- [ ] **INFRA-02**: `build-logic/` convention plugins centralize Kotlin/Compose/test configuration across modules +- [ ] **INFRA-03**: iOS Kotlin/Native binary options set from day 1: `kotlin.native.binary.objcDisposeOnMain=false`, `gc=cms` +- [ ] **INFRA-04**: Server Docker image builds and deploys to user's homelab alongside Authentik +- [ ] **INFRA-05**: Flyway migrations run automatically on server startup in a known order +- [ ] **INFRA-06**: `shared/commonMain` contains only domain models + API DTOs — no UI, no HTTP, no DB code +- [ ] **INFRA-07**: App is distributed to partner via TestFlight (iOS) for initial dogfooding + +## v2 Requirements + +Explicitly acknowledged but deferred. Not in the v1 roadmap. + +### In-app recipe authoring + +- **AUTHR-01**: User can create a new recipe with ingredients, steps, nutrition, allowed slots, tags +- **AUTHR-02**: User can edit an existing recipe +- **AUTHR-03**: User can archive/delete a recipe they created + +### Nutrition goal tracking + +- **NUTR-01**: User can set daily macro targets (kcal, protein, fat, carbs) +- **NUTR-02**: Planner shows deficit/surplus vs. targets per day +- **NUTR-03**: User can view weekly nutrition trends + +### Additional platforms + +- **PLAT-01**: Android app distributed to household members (APK or Play Store) +- **PLAT-02**: Web (Compose for Wasm) as a PWA replacement for the current mockup +- **PLAT-03**: English localization (full copy pass) + +### Sync hardening + +- **SYNC2-01**: Server-sent events (SSE) for near-realtime sync instead of polling +- **SYNC2-02**: Per-table upgrade path from LWW to operation-log sync if concurrent-edit data loss becomes observable + +### iOS Liquid Glass fidelity + +- **LG2-01**: Native SwiftUI interop for tab bar and nav bar (real iOS 26 Liquid Glass material) if Compose approximation proves inadequate on real hardware + +## Out of Scope + +Explicitly excluded. Documented to prevent scope creep. + +| Feature | Reason | +|---------|--------| +| Social features (comments, ratings, public profiles, feeds) | Private household app, not a community product | +| Recipe sharing between households | Households are isolated in v1; recipe marketplace is not the point | +| Meal-plan marketplace / paid plans | Personal-use product | +| Grocery-delivery integrations (Instacart, Carrefour Online, etc.) | Polish-market + small scope; integration cost not justified | +| Barcode scanning / receipt OCR for pantry updates | Manual entry is fine for a 2-person household | +| AI-generated recipes | Curated catalog is the value | +| Apple Sign-in as a first-class button | Authentik OIDC is user's IdP, not a third-party social login | +| Port of mockup's vanilla-JS visual design | Visual rebuild around Liquid-Glass language; mockup is functional spec only | +| Port of mockup's ~80 ingredients + ~30 recipes as seed data | User explicitly chose to re-curate catalog fresh | +| Device-clock-based sync timestamps | Silent data loss under drift; server-assigned timestamps mandatory | +| True iOS 26 Liquid Glass native material in v1 | Requires SwiftUI interop; Compose approximation is v1 scope | + +## Traceability + +Populated during roadmap creation. Each v1 requirement maps to exactly one phase. + +| Requirement | Phase | Status | +|-------------|-------|--------| +| *(filled by roadmapper)* | | | + +**Coverage:** +- v1 requirements: **72 total** (AUTH=6, HSHD=7, RCPE=8, PLAN=14, PNTR=5, SHOP=6, SYNC=10, UI=9, INFRA=7) +- Mapped to phases: — (filled by roadmapper) +- Unmapped: — (filled by roadmapper) + +--- +*Requirements defined: 2026-04-24* +*Last updated: 2026-04-24 after initial definition*