summaryrefslogtreecommitdiff
path: root/plan.md
blob: 2ad1a8254c98ea23a06927735864aea4a5053a50 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
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.