summaryrefslogtreecommitdiff
path: root/plan.md
diff options
context:
space:
mode:
Diffstat (limited to 'plan.md')
-rw-r--r--plan.md231
1 files changed, 231 insertions, 0 deletions
diff --git a/plan.md b/plan.md
new file mode 100644
index 0000000..2ad1a82
--- /dev/null
+++ b/plan.md
@@ -0,0 +1,231 @@
+# Implementation Plan
+
+> Written 2026-06-22. Turns `requirements.md` into concrete, sequenced work.
+> Reflects the current priority order: **pantry polish → auth → MCP**, with the
+> MCP being the highest-value piece (Claude reads *and updates* the pantry, so
+> manual upkeep mostly goes away). Builds on the completed `/simplify` pass
+> (shared `kitchen/helpers.py`, dead config removed).
+>
+> Altitude: file/model/endpoint level. Not code — decisions and steps.
+
+## Sequencing at a glance
+
+| Phase | Goal | Risk | Depends on |
+|------|------|------|-----------|
+| 0 | ✅ Commit the simplify baseline — **done** | none | — |
+| 1 | Pantry: mobile-first, In/Low/Out, fast add | medium (model + UI) | 0 |
+| 2 | Auth (login-once) + prod baseline | low | — (can run parallel to 1) |
+| 3 | MCP server — read + update pantry from claude.ai | medium | 1, 2 |
+
+Deferred (not in this plan): web what-can-i-cook rework, §4.1 substitutions,
+shopping flow rethink, web cook-logging. They stay roughly working but are not
+invested in here.
+
+---
+
+## Phase 0 — Commit the baseline ✅ done
+
+Tom has committed the simplify baseline. Starting point is clean.
+
+---
+
+## Phase 1 — Pantry polish (mobile-first)
+
+The heart of the friction problem. Move from "spreadsheet of decimals" to
+"glance into the fridge", and rebuild the screen phone-first.
+
+### 1.1 Model change — add presence state
+
+`PantryItem` today: `quantity` (required Decimal), `unit`, `location`,
+`expiry_date`, `is_staple`.
+
+- [ ] Add `state` to `PantryItem`: `CharField(choices=IN/LOW/OUT, default="in")`.
+ New `TextChoices` like the existing `Location`.
+- [ ] Change `quantity` from `DecimalField` to an **optional integer**
+ (`PositiveIntegerField(null=True, blank=True)`) — never going to have
+ 1.234 pieces of toast. Keep `unit` too, but neither is required on add.
+ (Migration handles the Decimal→Integer + nullable change plus the
+ backfill below in one step.)
+- [ ] Migration `0003_pantryitem_state_optional_quantity`. Data migration:
+ backfill existing rows → `state = "out"` where `quantity == 0`, else `"in"`.
+- [ ] Decide the In/Low/Out ↔ legacy `quantity==0` / `is_staple` mapping in one
+ place (see 1.5 — it ripples into matcher + shopping queries).
+
+> **Decided:** quantity becomes an optional **integer** (not a decimal, not
+> free text). It's secondary metadata; In/Low/Out is the primary signal.
+
+### 1.2 Presence filtering — stop relying on `quantity__gt=0`
+
+Several queries use `quantity__gt=0` to mean "I have this". With optional
+quantity that breaks (null ≯ 0). Replace presence checks with state.
+
+- [ ] `_pantry_context` (`views_htmx.py`): show **all** items, don't filter out
+ depleted ones — "Out" rows render greyed (requirements §2.1). Annotate
+ `is_expired` / `expiring_soon` as today.
+- [ ] `helpers.get_pantry_total` and the matcher's presence logic
+ (`what_can_i_cook`, `recipes_page`): treat `state != "out"` as "have it";
+ tolerate `quantity = None`. Minimal change only — full presence-based
+ matcher rework stays **deferred**. Goal here is just "don't crash / don't
+ silently show everything as missing."
+
+### 1.3 Pantry UI — mobile-first rebuild
+
+> **Decided:** the current design is poor — do a proper rethink of the pantry
+> screen (layout, interaction, hierarchy), but **keep the palette** (Tom likes
+> it). Mockup goes first (1.3.0) before touching Django templates.
+
+**1.3.0 Design mockup first.** Produce a self-contained HTML mockup of the
+redesigned pantry (phone width, real palette) as an Artifact Tom can open on
+his phone and react to. Only wire it into Django templates once the look is
+agreed.
+
+Replace the table-of-tiny-links (`partials/pantry_table.html`) with a
+phone-first card list.
+
+- [ ] Per-item **card** (single column): ingredient name, location badge, and a
+ **3-way In / Low / Out button group** as the primary control — proper
+ buttons, ≥44px tap targets, not inline links. Expiry shown only if set, as
+ a soft "use soon" pill.
+- [ ] Grouping: fridge / freezer / cupboard sections, collapsible or just
+ stacked. Keep it one-column; no horizontal scroll.
+- [ ] State change endpoint: `POST /app/pantry/<id>/state/` (`to=in|low|out`),
+ re-renders the list partial via HTMX. Replaces the quantity-edit flow as
+ the main interaction.
+- [ ] Keep delete and fridge↔freezer move, but as secondary (smaller, maybe
+ behind a tap). Expiry edit becomes optional, de-emphasised.
+
+### 1.4 Fast add
+
+- [ ] **Autocomplete**: `GET /app/pantry/search/?q=` returns matching
+ `Ingredient`s (name + aliases), rendered as an HTMX dropdown under the add
+ box. Tap a result → adds with sensible defaults (unit/location from the
+ `Ingredient`, `state="in"`, no qty required).
+- [ ] **Quick-add chips**: render a row of one-tap chips from staples +
+ recently-used ingredients (recent = distinct ingredients from recent
+ `PantryItem`/`CookLog`). One tap adds/sets to In.
+- [ ] **Add bar placement**: sticky to the **bottom** of the viewport (thumb
+ reach) on mobile, with the chips just above it. This is the make-or-break
+ detail per requirements.
+- [ ] `pantry_add` already auto-creates unknown ingredients and merges into an
+ existing same-location row — keep that, but set `state="in"` on
+ add/merge, and stop requiring quantity.
+
+### 1.5 CSS / layout (base.html)
+
+- [ ] Shift `base.html` to mobile-first: the current `max-width: 960px` desktop
+ table styling becomes the *enhancement*; default styles target a narrow
+ phone screen. Bigger touch targets, larger tap-friendly buttons, bottom
+ nav or bottom add bar.
+- [ ] Keep the existing dark palette (it's fine) — this is layout/sizing, not a
+ re-theme.
+- [ ] Verify on a real phone-width viewport (e.g. 390px) — no horizontal
+ scroll, controls reachable one-handed.
+
+### 1.6 Tests
+
+`kitchen/tests.py` is empty. Add the first real tests here:
+
+- [ ] Model: state defaults, optional quantity, backfill migration sanity.
+- [ ] Views: add via autocomplete sets `state="in"`; state toggle endpoint;
+ pantry context includes "Out" rows.
+
+---
+
+## Phase 2 — Auth (login once) + prod baseline
+
+Low risk, can proceed alongside Phase 1. Closes the wide-open `/app/`.
+
+### 2.1 Session auth, long-lived
+
+- [ ] One real user account (document the `createsuperuser` / set-password
+ step; don't commit credentials).
+- [ ] Settings:
+ - `SESSION_COOKIE_AGE = 60*60*24*365` (~1 year)
+ - `SESSION_EXPIRE_AT_BROWSER_CLOSE = False`
+ - `SESSION_SAVE_EVERY_REQUEST = True` (sliding expiry — each visit renews
+ the year, so an active user is effectively never logged out)
+ - `LOGIN_URL` / `LOGIN_REDIRECT_URL`
+- [ ] Login page: `django.contrib.auth.views.LoginView` + a minimal template in
+ the existing dark style. Logout link somewhere unobtrusive.
+- [ ] Protect `/app/`: simplest is Django 5.x `LoginRequiredMiddleware`, but it
+ hits *every* view — so exempt `/api/` (token auth keeps its own) and the
+ login view. If exemptions get fiddly, fall back to wrapping the
+ `urls_htmx` includes / decorating the page views with `login_required`.
+ **Pick one approach; don't mix.**
+
+### 2.2 Drop the CSRF hacks
+
+- [ ] With a real session + CSRF token (already wired in `base.html`), remove
+ every `@csrf_exempt` from `views_htmx.py`. Verify HTMX POST/DELETE still
+ work (the `htmx:configRequest` handler already sends `X-CSRFToken`).
+
+### 2.3 Production baseline (cheap, fold in here)
+
+- [ ] `DEBUG` from env (default False in prod).
+- [ ] `SECRET_KEY` from env; drop the committed `django-insecure-…` literal.
+- [ ] Keep `ALLOWED_HOSTS` as-is.
+
+---
+
+## Phase 3 — MCP server (the goal)
+
+Let claude.ai "cooking mode" read the pantry and write changes back, over the
+existing `/api/` (token auth, `IsAuthenticated`). This is what kills the
+"recite my whole pantry every time" chore.
+
+### 3.1 Shape
+
+- [ ] **Separate FastMCP service** (decided). Mirrors a pattern Tom already
+ runs elsewhere: a standalone "ai service" FastMCP server that talks to the
+ Django API with the `caine` token. Keeps concerns decoupled; the API is
+ already the right interface and the `/simplify` pass left it clean.
+- [ ] Tools to expose (map to existing endpoints):
+ - `get_pantry` → `GET /api/pantry/` (+ state)
+ - `add_to_pantry` → `POST /api/bulk-pantry-add/`
+ - `set_pantry_state` → new tiny endpoint or `PATCH /api/pantry/<id>/`
+ setting `state` (in/low/out) — the conversational update path
+ ("used the last of the noodles" → set Out)
+ - `what_can_i_cook` → `GET /api/what-can-i-cook/`
+ - `log_cook` → `POST /api/log-cook/` (and make it accept `rating`, the
+ one API gap from research §5)
+- [ ] Add the `state` field to the pantry serializer so MCP reads/writes it.
+
+### 3.2 Transport, hosting, auth — **decided**
+
+- **Transport:** remote MCP over HTTP (streamable HTTP / SSE), reachable at
+ **`food.jihakuz.xyz/mcp`**. Not stdio.
+- **Hosting:** same box as Django, behind nginx at `/mcp`, as its own systemd
+ service alongside `food.service`.
+- **Auth:** **bearer token** — single user, so one long high-entropy token
+ checked by the MCP service (and/or nginx) is enough. It must be tight: this
+ endpoint is internet-exposed (unlike `/api/` and `/admin/`, which nginx
+ blocks externally), so the token is the only thing protecting it. Strong
+ token, kept out of git (env/secret), TLS only.
+
+### 3.3 Why the cooking blocks still work (no action, just the rationale)
+
+MCP only feeds data/tools into the claude.ai conversation; it doesn't change
+how cooking mode renders. So Claude pulls real pantry contents *and* still
+produces its formatted cooking blocks — grounded instead of guessed.
+
+---
+
+## Decisions (locked)
+
+1. **Quantity** → optional **integer**, secondary to In/Low/Out. ✅
+2. **MCP** → FastMCP "ai service" on the same box at `food.jihakuz.xyz/mcp`,
+ bearer-token auth, talks to the Django API. ✅
+3. **Design** → rethink the pantry screen, keep the palette; mockup first. ✅
+
+Still just an implementation detail (not a blocker): **auth enforcement** —
+`LoginRequiredMiddleware` + exemptions vs per-view `login_required`. Plan: try
+middleware, fall back if exemptions get fiddly.
+
+## Definition of done (per phase)
+
+- **P1:** Tom can add an item and flip In/Low/Out on his phone in a couple of
+ taps, one-handed, no horizontal scroll; pantry stays glanceable.
+- **P2:** Visiting `/app/` requires login; logging in once sticks for ~a year;
+ no `@csrf_exempt` left; `DEBUG=False` + env `SECRET_KEY` in prod.
+- **P3:** In claude.ai cooking mode, Claude can list the pantry and mark items
+ In/Low/Out (and log a cook) without Tom typing the inventory.