From e34081ce36d1993912dad514127924440437a2e9 Mon Sep 17 00:00:00 2001 From: Tom Flux Date: Tue, 23 Jun 2026 19:55:16 +0100 Subject: Add planning docs: research, requirements, redesign plan Research of current state, requirements for the web-first pivot (pantry polish -> auth -> MCP), and the phased implementation plan. Co-Authored-By: Claude Opus 4.8 --- plan.md | 231 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 plan.md (limited to 'plan.md') 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//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//` + 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. -- cgit v1.2.3