Files
recipe/.planning/PROJECT.md

196 lines
16 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.
# Recipe (working title)
## What This Is
A mobile-first meal planning app for a small household — pick recipes for the week, fill a calendar across five meal slots per day, and watch pantry gaps + shopping lists emerge from the plan. Kotlin Multiplatform targeting iOS primarily, with Android, Desktop, and Wasm as secondary targets. Built for me + my partner (shared household plan) with a handful of family/friends as authorized users on the same self-hosted backend.
## Core Value
**"My week is planned."** I pick recipes, the calendar fills up, and I know what we're eating. Everything else — pantry tracking, shopping list, nutrition numbers — exists to reinforce that one moment.
## Requirements
### Validated
(None yet — ship to validate)
### Active
**Authentication & identity**
- [ ] Users sign in via the user's self-hosted Authentik instance (OIDC)
- [ ] Sessions persist across app launches; offline access works with cached credentials
**Household sharing**
- [ ] Two users (me + partner) share one household: one plan, one pantry, one shopping list
- [ ] Changes by either user converge on all devices when online
**Recipes (browse & detail)**
- [ ] User can browse a curated recipe catalog with a grid view
- [ ] User can filter recipes by meal slot, tags, and cooking time
- [ ] User can search recipes by title/tag
- [ ] User can open a recipe detail view with ingredients, steps, and nutrition per serving
**Meal planner (hero feature)**
- [ ] User can view a calendar of days and see planned meals per day
- [ ] User can add recipes to any of 5 slots/day (śniadanie, drugie śniadanie, obiad, przekąska, kolacja)
- [ ] User can remove or replace a meal entry
- [ ] User can adjust servings on a meal entry
- [ ] User can customize a meal entry: swap ingredients (substitutions), exclude ingredients, add extras, override amounts
- [ ] User can select a specific product (pack size) for an ingredient in a meal entry
- [ ] User can mark a meal slot as "skipped" for a day
- [ ] User sees daily nutrition totals (kcal, protein, fat, carbs) computed from the plan
**Pantry**
- [ ] User can view current pantry inventory grouped by category
- [ ] User can add/update quantities manually in the pantry
- [ ] User sees which ingredients fall short over a chosen planning horizon
- [ ] User can filter pantry by category and by shortfall status
**Shopping list**
- [ ] User can select days from the plan to generate a shopping list
- [ ] Shopping list aggregates ingredient needs across selected days minus pantry
- [ ] Shopping list groups items by category for an efficient store trip
- [ ] User can mark items as bought during a shopping session; marked items are removed from active needs and added to pantry
**Offline + sync**
- [ ] App is fully usable offline: read and write plans, pantry, shopping list
- [ ] Local changes sync to the backend when connectivity returns, without data loss
- [ ] Conflicts between two household members' concurrent edits resolve deterministically (last-write-wins for MVP; revisit if it hurts)
**Polish UI foundation**
- [ ] All user-facing strings are externalized into resource files (i18n-ready), even though v1 ships Polish only
- [ ] UI uses a Liquid-Glass-inspired visual language (translucent surfaces, blur, soft depth) implemented in Compose Multiplatform
- [ ] Visual hierarchy is less cramped than the mockup (more breathing room, calmer typography)
- [ ] iOS app feels iOS-idiomatic within Compose's constraints (tab bar placement, navigation patterns, safe areas, dark mode)
### Out of Scope
**For v1 (deferred to later phases / milestones):**
- In-app recipe authoring — *v1 seeds the DB manually; authoring in-app comes next phase*
- Recipe sharing between users/households — *future feature; households are isolated in v1*
- Nutrition goal tracking (targets, streaks, deficits) — *v1 shows numbers informationally only*
- English and other language copy — *code is i18n-ready but v1 ships Polish only*
- True native iOS 26 Liquid Glass via SwiftUI interop — *Compose approximation for v1; revisit only if real-device chrome feels clearly inadequate*
- Desktop and Wasm as shipped products — *Desktop useful for hot-reload dev; Wasm is a possible future target, neither is a v1 deliverable*
- Sign in with Apple as a first-class button — *user's Authentik handles auth; Apple can be federated upstream in Authentik if needed later*
**Permanently out of scope (explicit exclusions):**
- Social features: comments, ratings, recipe feeds, public profiles — *this is a private household app, not a community product*
- Meal-plan marketplaces / paid plans — *personal-use product*
- Grocery delivery integrations (Instacart, etc.) — *Polish market + small scope; not worth the integration cost*
- Barcode scanning / receipt OCR for pantry updates — *manual entry is fine for a 2-person household*
- AI-generated recipes — *curated catalog is the value*
**Deliberately not carried forward from the mockup:**
- The mockup's seed data (~80 ingredients, ~30 recipes) — *user chose to start the catalog fresh*
- The mockup's visual design — *full visual rebuild; mockup is functional reference only*
- The mockup's localStorage data model — *server-backed with local cache replaces it*
## Context
**Codebase state.** The `~/dev/repo/recipe` directory is a freshly-generated Kotlin Multiplatform Compose template from IntelliJ with four modules: `composeApp` (Android + Desktop + iOS shared UI), `iosApp` (iOS bootstrap), `server` (Ktor, not yet written), and `shared` (common code). No app logic exists yet — this is effectively greenfield with the build infra in place.
**Reference implementation.** The user built a working PWA at `~/dev/repo/recipe-mockup/` (vanilla JS + Tailwind CDN + nginx/Docker). It implements the same four views (Recipe List, Meal Planner, Pantry, Shopping List) and has mature logic worth mining as a *functional* spec — particularly planner entry customization (substitutions, amount overrides, product selection), shortfall computation over a horizon, and shopping-list aggregation with "bought" session tracking. The mockup's UI design is **not** being carried forward; the user is redesigning visuals around a Liquid-Glass-inspired language.
**Users.** Authorized users only, behind the user's Authentik. Primary user is the author; secondary is their partner (household sharing from day 1); a handful of family/friends may use their own household accounts. Not an App Store public launch — personal / close-circle use.
**Infra.** User runs a homelab. Authentik is already installed. The Ktor backend will run on the same server (containerized). No managed cloud dependencies planned.
**Language & platform.** Polish-only UI for v1 (strings externalized for future i18n). iOS is the primary daily driver; Android deployed later for friends; Desktop useful for development (hot reload); Wasm is aspirational.
**Liquid Glass decision.** True iOS 26 Liquid Glass (refractive material, specular highlights, morphing chrome) is a SwiftUI-native feature that Compose on iOS cannot reproduce exactly (Compose uses Skia, not Metal-native glass material). The v1 plan is: Compose-only approximation (blur + translucency + gradients) everywhere, measure real-device performance and visual quality, and **only** selectively add SwiftUI interop for the chrome (tab bar, nav bar) if the approximation feels insufficient. This avoids upfront interop complexity for 90%+ of the UI.
## Constraints
- **Tech stack**: Kotlin Multiplatform + Compose Multiplatform for UI, Ktor for server, Authentik OIDC for auth — Locked; aligns with user's skills + self-hosted infra
- **Primary platform**: iOS — Must feel good here first; other platforms are secondary
- **Hosting**: Self-hosted on user's existing homelab (alongside Authentik) — No managed cloud; implies containerized deploy, self-managed DB, reverse proxy
- **Offline**: Full offline read/write is required — User will use the shopping list in-store where signal is unreliable; online-only is unacceptable
- **Audience size**: ~510 authenticated users total — Don't over-engineer multi-tenancy, rate limiting, or horizontal scaling
- **Language**: Polish UI for v1, i18n-ready code — All strings must be externalized from day 1 to avoid costly retrofit later
- **Data seeding**: Catalog starts empty; user will author recipes directly in DB for MVP — Need admin-friendly seeding path (SQL migrations, JSON fixtures, or CLI tool)
- **Visual direction**: Liquid-Glass-inspired (Compose approximation) — Bright mockup palette is being replaced; design needs to be reworked as part of the rebuild
## Key Decisions
### Product & scope
| Decision | Rationale | Outcome |
|----------|-----------|---------|
| KMP with Compose Multiplatform for UI | iOS-primary + Android secondary + Desktop/Wasm optional; single codebase for 90%+ of UI | — Pending |
| Household-sharing from day 1 (me + partner share one plan) | Core use case is cooking together; per-user + later-sharing would force data-model rewrite | — Pending |
| Authentik OIDC as sole auth provider for MVP | User already runs Authentik; self-hosted == aligned; Apple Sign-in likely not required for App Store since Authentik is user's own IdP, not a third-party social login | — Pending |
| Server lives on user's homelab alongside Authentik | Existing infra, zero managed-cloud cost, same ops surface | — Pending |
| Offline-first with last-write-wins sync | Grocery-store usage demands offline; conflict resolution overkill for a 2-person household | — Pending |
| Compose-only Liquid Glass approximation for v1 | Real iOS 26 Liquid Glass requires SwiftUI interop; approximation keeps single codebase; revisit only if chrome feels inadequate on real device | — Pending |
| Polish-only strings, i18n-ready infrastructure | Single-language content for v1 speed; externalized strings prevent future rewrite | — Pending |
| Start catalog fresh (don't port mockup seed data) | Mockup data is a reference, not production content; user wants to re-curate | — Pending |
| Nutrition is informational only in v1 | Keep scope tight; tracking/goals are a natural v2 if usage patterns justify | — Pending |
| Mockup is functional spec only, not visual spec | Visual direction is changing (Liquid Glass); logic is mature and worth mining | — Pending |
### Client tech stack (composeApp)
| Decision | Rationale | Outcome |
|----------|-----------|---------|
| Navigation: Jetpack Navigation Compose (CMP port) — `org.jetbrains.androidx.navigation:navigation-compose:2.9.x` | JetBrains-official recommendation on kotlinlang.org; type-safe routes via `@Serializable`; works across Android/iOS/Desktop/Wasm; skill transferable to Android | — Pending |
| Architecture: ViewModel + StateFlow + method-per-action | Standard modern pattern; matches JetBrains/Google samples; lowest ceremony; upgrade individual screens to sealed-event onEvent only when they grow complex | — Pending |
| DI: Koin — `koin-core`, `koin-compose`, `koin-compose-viewmodel` | De facto KMP standard; smoothest `koinViewModel()` integration with Jetpack Nav back-stack scoping; no codegen; small surface to learn | — Pending |
| Local DB: SQLDelight 2.x | Most mature KMP DB; Wasm-ready (hedge for future Compose-for-Web target); raw SQL is a transferable skill; clear migration story via .sq files | — Pending |
| HTTP client: Ktor Client | Same team as server; first-class KMP; shared serialization config | — Pending |
| Serialization: kotlinx.serialization (JSON) | Standard; works everywhere; pairs with Ktor and SQLDelight | — Pending |
| Date/time: kotlinx.datetime | Standard; SQLDelight adapters available | — Pending |
| Logging: Kermit (Touchlab) | KMP-native logger; simple API; optional Crashlytics/Sentry bridges | — Pending |
| Image loading: Coil 3 (`io.coil-kt.coil3:coil-compose`) | First-class Compose Multiplatform support; modern API | — Pending |
| Key-value settings: `com.russhwolf:multiplatform-settings` | Small prefs (last tab, theme toggles) that don't belong in SQLDelight | — Pending |
| Glass/blur effects: Haze (`dev.chrisbanes.haze:haze`) | Purpose-built for glass UI in CMP; handles content capture + efficient re-blur; multiplatform | — Pending |
| Mobile OIDC: AppAuth (Android) + ASWebAuthenticationSession wrapper (iOS), exposed via KMP interface | Platform-native OAuth flows; no cross-platform auth library mature enough yet for this in 2026 | — Pending |
### Server tech stack
| Decision | Rationale | Outcome |
|----------|-----------|---------|
| Server framework: Ktor Server 3.x | Same team as client HTTP; Kotlin-native; fits homelab deployment; coroutines throughout | — Pending |
| Database: Postgres | Homelab-friendly (Docker); JSONB for meal-entry extras; room to grow; standard skill | — Pending |
| SQL: Exposed DSL | Kotlin-backend standard; type-safe SQL builders; JSONB first-class; strong tutorial trail with Ktor. Avoid Exposed's DAO (active record) API | — Pending |
| Migrations: Flyway | Industry-standard numbered `V__.sql` files; auto-apply on startup; works with any JDBC-backed stack | — Pending |
| Token validation: `io.ktor:ktor-server-auth-jwt` | Built-in JWKS support with caching + rotation; direct integration with Authentik's OIDC endpoint | — Pending |
### Sync
| Decision | Rationale | Outcome |
|----------|-----------|---------|
| Strategy: last-write-wins with server-assigned `updated_at` per row | Household of 2 has negligible concurrent-edit risk; simplest to implement and debug; upgradeable per-table to op-log later if hurt | — Pending |
| Transport: HTTP polling (2030s when foreground) + pull-to-refresh + debounced push after local writes | Sufficient freshness for a household; SSE is a v2 enhancement if polling feels laggy | — Pending |
| API shape: REST, versioned `/api/v1`, two sync endpoints (`POST /sync/push`, `GET /sync/pull?since=...`) plus catalog + households/invites CRUD | Versioning leaves room to evolve; minimal surface area; read-mostly catalog is heavily cached on client | — Pending |
| Server-side data model: `users`, `households`, `memberships`, `invites` + household-scoped tables carrying `household_id`, `updated_at`, `deleted_at` | Supports household sharing + invites, JIT user provisioning from OIDC `sub`, soft deletes for sync | — Pending |
### Build & module structure
| Decision | Rationale | Outcome |
|----------|-----------|---------|
| Gradle version catalog (`gradle/libs.versions.toml`) | Single source of truth for versions; standard KMP practice | — Pending |
| Convention plugins (`build-logic/` module) from day 1 | Centralizes Kotlin/Compose/test config; educational payoff; small upfront cost | — Pending |
| Keep template modules: `composeApp/`, `iosApp/`, `server/`, `shared/` — no feature modules in v1 | Feature modules don't pay off until ~10 features or multiple devs; flat is clearer at this scale | — Pending |
| `shared/commonMain` holds: domain models + API DTOs only (no UI, no HTTP, no DB) | Keeps shared dep graph minimal; both client and server depend on `shared/` | — Pending |
| `composeApp/commonMain` package layout: `app/ navigation/ ui/{theme,components,screens/{recipes,planner,pantry,shopping}} data/{local,remote,repository} domain/` | Groups by UI concern + data layer; resists premature modularization | — Pending |
## Evolution
This document evolves at phase transitions and milestone boundaries.
**After each phase transition** (via `/gsd-transition`):
1. Requirements invalidated? → Move to Out of Scope with reason
2. Requirements validated? → Move to Validated with phase reference
3. New requirements emerged? → Add to Active
4. Decisions to log? → Add to Key Decisions
5. "What This Is" still accurate? → Update if drifted
**After each milestone** (via `/gsd-complete-milestone`):
1. Full review of all sections
2. Core Value check — still the right priority?
3. Audit Out of Scope — reasons still valid?
4. Update Context with current state
---
*Last updated: 2026-04-24 after initial tech-stack discussion*