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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
173e74b4a9
commit
25a09b08fc
@@ -1,17 +0,0 @@
|
|||||||
<tr id="pantry-row-{{ item.id }}">
|
|
||||||
<td>{{ item.ingredient.name }}</td>
|
|
||||||
<td>{{ item.quantity|floatformat:0 }} {{ item.unit }}</td>
|
|
||||||
<td colspan="2">
|
|
||||||
<form style="display: flex; gap: 0.4rem; align-items: center;"
|
|
||||||
hx-post="{% url 'app-pantry-save-expiry' item.id %}"
|
|
||||||
hx-target="#pantry-items"
|
|
||||||
hx-swap="innerHTML">
|
|
||||||
<input type="date" name="expiry_date" value="{{ item.expiry_date|date:'Y-m-d' }}" style="font-size: 0.8rem;">
|
|
||||||
<button type="submit" class="btn btn-primary btn-sm">Save</button>
|
|
||||||
<button type="button" class="btn btn-secondary btn-sm"
|
|
||||||
hx-get="{% url 'app-pantry-cancel-edit' %}"
|
|
||||||
hx-target="#pantry-items"
|
|
||||||
hx-swap="innerHTML">Cancel</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
@@ -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.
|
||||||
+260
@@ -0,0 +1,260 @@
|
|||||||
|
# Food App — Requirements (pivot to web-first)
|
||||||
|
|
||||||
|
> Written 2026-06-22, after reviewing `research.md` and interviewing Tom.
|
||||||
|
> This supersedes the Caine-centric design. Pairs with `research.md` (current
|
||||||
|
> state) — this doc is the *target*.
|
||||||
|
|
||||||
|
## The pivot, in one line
|
||||||
|
|
||||||
|
Caine (the Matrix LLM assistant) was never really used — it cost too much per
|
||||||
|
call. **The web UI becomes the primary interface.** An LLM can come back later
|
||||||
|
as an MCP server riding on the existing claude.ai subscription (no per-call
|
||||||
|
cost), but that's a later phase, not now.
|
||||||
|
|
||||||
|
## The one constraint that wins
|
||||||
|
|
||||||
|
**Ease of use beats everything.** This is a single-user personal app, low
|
||||||
|
stakes. When a requirement trades correctness, completeness, or robustness for
|
||||||
|
less friction, friction wins. Specifically: fewer fields, fewer taps, fewer
|
||||||
|
decisions, nothing that nags, and updating the pantry should feel like nothing.
|
||||||
|
|
||||||
|
**Mobile-first, not mobile-also.** The pantry is used standing in the kitchen
|
||||||
|
or at the shop, on a phone — that's the primary device, and it's fiddly today.
|
||||||
|
Design for a one-handed phone screen first; desktop is the afterthought.
|
||||||
|
Concretely: big tap targets (state changes and add are thumb-sized, not
|
||||||
|
table-cell links), no tiny number inputs or cramped rows, single-column
|
||||||
|
layouts, no horizontal scroll, controls reachable in the lower half of the
|
||||||
|
screen. If something works on the phone it'll be fine on desktop; the reverse
|
||||||
|
is what's been failing.
|
||||||
|
|
||||||
|
Everything below is judged against that.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Priorities (Tom, 2026-06-22 — current call)
|
||||||
|
|
||||||
|
In value order:
|
||||||
|
|
||||||
|
1. **Pantry: make it a bit nicer in the web UI.** Not a ground-up rebuild —
|
||||||
|
incremental polish (the In/Low/Out state + fast add from §2), enough that
|
||||||
|
keeping it accurate isn't a slog. It needs to be *reasonably* accurate
|
||||||
|
precisely because of #3.
|
||||||
|
2. **Add some auth** (§1) — login once, long session.
|
||||||
|
3. **MCP server with claude.ai — the highest-value piece.** The whole point:
|
||||||
|
stop hand-reciting "here's what's in my pantry" every time. Claude reads the
|
||||||
|
pantry directly in cooking mode. Almost certainly also the *lowest-friction
|
||||||
|
update path* — say "I used the last of the noodles" and Claude writes it
|
||||||
|
back — which is the real answer to the pantry-is-tedious problem.
|
||||||
|
|
||||||
|
Everything else in this doc (web what-can-i-cook, shopping rethink, web
|
||||||
|
cook-logging, fixed recipes, §4.1 substitutions) is **deferred** — nice, not
|
||||||
|
now. Detailed below for when we get there.
|
||||||
|
|
||||||
|
**The synergy that justifies the order:** #3 is *why* #1 only needs to be "a
|
||||||
|
bit nicer" rather than perfect. If Claude can both read and update the pantry
|
||||||
|
conversationally, the web UI becomes the glanceable / manual fallback, not the
|
||||||
|
primary data-entry surface. So don't over-invest in pantry UI — invest in
|
||||||
|
making the data MCP can serve trustworthy and easy to nudge.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Auth — log in once, then forget it
|
||||||
|
|
||||||
|
Decision: **a simple login page + a very long-lived session** (≈1 year cookie).
|
||||||
|
Log in once per device; never get prompted again on that device.
|
||||||
|
|
||||||
|
- Standard Django session auth (`login_required`) on all `/app/` views — no
|
||||||
|
HMAC, no signed requests, no per-request secret. (The HMAC idea was heavier
|
||||||
|
than needed for one user.)
|
||||||
|
- Set `SESSION_COOKIE_AGE` long (~1 year) and `SESSION_EXPIRE_AT_BROWSER_CLOSE
|
||||||
|
= False` so the session survives restarts.
|
||||||
|
- One user account is enough. `/api/` keeps its existing token auth for the
|
||||||
|
future MCP client.
|
||||||
|
- This also lets us drop the `@csrf_exempt` hacks — a logged-in session has a
|
||||||
|
real CSRF token, so HTMX POSTs can be protected normally.
|
||||||
|
|
||||||
|
Security is explicitly *not* the top priority, but this gives a real baseline
|
||||||
|
(no more wide-open `/app/`) at near-zero friction.
|
||||||
|
|
||||||
|
## 2. Pantry — the redesign (this is the heart of it)
|
||||||
|
|
||||||
|
Today the pantry is "all just frustrating": adding is slow, quantities drift
|
||||||
|
out of date the moment you cook, expiry dates are fiddly to set and chase, and
|
||||||
|
it's a separate chore that's easy to forget. The fix is to **stop tracking the
|
||||||
|
pantry like a spreadsheet and track it like a fridge you glance into.**
|
||||||
|
|
||||||
|
### 2.1 Track presence, not precise amounts
|
||||||
|
|
||||||
|
Replace exact decimal quantities as the primary signal with a simple state:
|
||||||
|
|
||||||
|
> **In stock · Running low · Out**
|
||||||
|
|
||||||
|
- One tap to change state (e.g. cycle In → Low → Out, or three buttons).
|
||||||
|
- Quantity becomes *optional metadata* (a free note like "½ bag", "3 left"),
|
||||||
|
never required, never the thing the app reasons about.
|
||||||
|
- This kills the "quantities drift after cooking" problem outright — you don't
|
||||||
|
maintain a number, you flip a flag when you notice.
|
||||||
|
|
||||||
|
> **Open decision:** some items genuinely want a rough count (eggs, noodle
|
||||||
|
> nests). Proposal: keep the optional quantity note for those, but the matcher
|
||||||
|
> and shopping logic only ever look at the In/Low/Out state. Confirm before
|
||||||
|
> building.
|
||||||
|
|
||||||
|
### 2.2 Fast add
|
||||||
|
|
||||||
|
- A single search box: type → autocomplete against known ingredients (and
|
||||||
|
aliases) → tap to add. Defaults its unit/location from the `Ingredient`
|
||||||
|
record, state defaults to "In stock". Zero further fields needed.
|
||||||
|
- If the name isn't known, add it inline in the same flow (it auto-creates the
|
||||||
|
ingredient, like the API already does).
|
||||||
|
- **Quick-add chips** for staples and recently-used items — one tap, no typing.
|
||||||
|
- On mobile this is the make-or-break screen: the search box and chips sit near
|
||||||
|
the bottom (thumb reach), adding an item is one tap, and the In/Low/Out
|
||||||
|
control on each row is a proper button, not a fiddly inline link. The current
|
||||||
|
table-of-tiny-actions layout is exactly what's being replaced.
|
||||||
|
|
||||||
|
### 2.3 Expiry stops nagging
|
||||||
|
|
||||||
|
- Expiry dates become fully optional and de-emphasised. No required date entry.
|
||||||
|
- Instead of chasing exact dates, a lightweight **"use soon"** flag you can tap
|
||||||
|
on an item (and an optional date if you actually care for something specific).
|
||||||
|
- Keep the existing freezer rule (frozen = no expiry). Defrosting (freezer →
|
||||||
|
fridge) can set a soft "use soon" instead of computing a hard date.
|
||||||
|
|
||||||
|
### 2.4 Updating the pantry as a by-product, not a chore
|
||||||
|
|
||||||
|
The biggest win against "I forget to update it": make other actions update the
|
||||||
|
pantry for you, so the standalone chore mostly disappears.
|
||||||
|
|
||||||
|
- **Logging a cook** optionally flips the ingredients it used toward Low/Out
|
||||||
|
(replaces the brittle decimal-deduction logic).
|
||||||
|
- **Checking off a shopping item** marks that ingredient back to "In stock" in
|
||||||
|
the pantry — closing the buy → restock loop automatically.
|
||||||
|
|
||||||
|
## 3. Recipes & "what can I cook" — keep it, but make it one implementation
|
||||||
|
|
||||||
|
Decision: **keep meta-recipes and the what-can-i-cook matcher** — it's the part
|
||||||
|
Tom actually wants. But:
|
||||||
|
|
||||||
|
- There are currently **two separate matchers** (`views.what_can_i_cook` JSON
|
||||||
|
and `views_htmx.recipes_page` HTML) that have already drifted. Collapse to
|
||||||
|
**one shared function**; both the page and any API/MCP call use it. (This is
|
||||||
|
also the "two implementations hanging around" Tom flagged.)
|
||||||
|
- The matcher moves to **presence-based**: a required slot is satisfied if any
|
||||||
|
of its options is In (or Low) — no quantity comparison. Simpler, and matches
|
||||||
|
the new pantry model.
|
||||||
|
- Servings is currently hardcoded to 2 in the web page; with a presence-based
|
||||||
|
matcher, servings stops mattering for "can I cook this", so we can just drop
|
||||||
|
the servings input from that view.
|
||||||
|
- Fixed `Recipe`s: keep the model, but **de-scope the URL import for now** — it
|
||||||
|
never links ingredients (see `research.md` §7), so imported recipes are
|
||||||
|
half-broken. Cooking ideas live in claude.ai cooking mode anyway. Don't
|
||||||
|
invest here until the MCP phase, if ever.
|
||||||
|
|
||||||
|
## 4. Shopping list — rethink the flow
|
||||||
|
|
||||||
|
Tom was explicitly unhappy with the current flow. Requirements:
|
||||||
|
|
||||||
|
- Generation stays smart (staples that are Out, "use soon" items, gaps for
|
||||||
|
required recipe slots) but should be **additive and forgiving** — generating
|
||||||
|
again shouldn't wipe manual additions or duplicate.
|
||||||
|
- Checking an item off should (a) cross it out and (b) restock it in the pantry
|
||||||
|
(see §2.4).
|
||||||
|
- Needs a clear "done shopping" action that tidies the list.
|
||||||
|
- Keep it dead simple — a phone checkbox list, grouped by aisle/section.
|
||||||
|
|
||||||
|
### 4.1 "Can't find it at the shop" — substitutions (important to Tom)
|
||||||
|
|
||||||
|
A recurring real pain: Tom goes to the shop to buy something and it's not
|
||||||
|
there, and then he's stuck. He wants help *in the moment* knowing what else
|
||||||
|
works.
|
||||||
|
|
||||||
|
- **Partial fix available now, no LLM needed:** the meta-recipe slot model
|
||||||
|
*is* a substitution table — a protein slot already lists pork mince OR
|
||||||
|
chicken, etc. So a shopping-list item that came from a recipe slot can show
|
||||||
|
"alternatives that fit the same slot" inline. If you can't find pork mince,
|
||||||
|
the list shows the other proteins that recipe accepts.
|
||||||
|
- **Richer substitutions need the LLM/MCP (Phase 2):** general swaps the app
|
||||||
|
has no data for ("no fresh basil → dried is fine", "no crème fraîche → use
|
||||||
|
yoghurt") require world knowledge. That's a claude.ai-via-MCP job: it reads
|
||||||
|
the shopping item + recipe context and suggests a real-world substitute.
|
||||||
|
- So: ship slot-based alternatives in the web UI now; treat "smart, open-ended
|
||||||
|
substitution at the shop" as a headline use case for the MCP phase.
|
||||||
|
|
||||||
|
## 5. Cook log — make it usable from the web
|
||||||
|
|
||||||
|
- Add a **web UI way to log a cook** (currently API/admin only): pick a
|
||||||
|
meta-recipe, tap the slot choices you used, optional rating + note, save.
|
||||||
|
- `log-cook` must accept and store `rating` (the model field exists; the
|
||||||
|
endpoint ignores it today).
|
||||||
|
- Route cook-logging through model validation so the "exactly one recipe link"
|
||||||
|
rule is actually enforced (today's `log_cook` bypasses `clean()`).
|
||||||
|
|
||||||
|
## 6. Cleanup / housekeeping (do first)
|
||||||
|
|
||||||
|
- **Run `/simplify` first**, before feature work, to clear the obvious cruft
|
||||||
|
(duplicate matcher, dead `filterset_fields`/`search_fields` with no
|
||||||
|
django-filter installed, the freezer-first deduction bug if deduction
|
||||||
|
survives the redesign).
|
||||||
|
- Production baseline: `DEBUG = False`, real `SECRET_KEY` from env. Low
|
||||||
|
priority but cheap.
|
||||||
|
|
||||||
|
## 7. Later — MCP (Phase 2, optional)
|
||||||
|
|
||||||
|
Once the web UI is solid, wrap the existing API in an **MCP server** so
|
||||||
|
claude.ai "cooking mode" can read the pantry and (maybe) log cooks. This is the
|
||||||
|
integration Tom actually wanted from Caine, without the per-call cost. Explicit
|
||||||
|
non-goal for now — listed so the API stays MCP-friendly in the meantime.
|
||||||
|
|
||||||
|
Two things this phase unlocks (and why it's worth keeping the API clean):
|
||||||
|
|
||||||
|
- **Cooking blocks still work.** MCP only *feeds data into* the claude.ai
|
||||||
|
conversation — it gives Claude tools to read the pantry. It does not change
|
||||||
|
how claude.ai renders cooking mode. So Claude pulling ingredients from the
|
||||||
|
food app and Claude producing its nice formatted cooking blocks are
|
||||||
|
independent: you get the blocks *and* they're grounded in your actual pantry.
|
||||||
|
- **Smart shop-floor substitutions** (see §4.1) — the open-ended "this isn't on
|
||||||
|
the shelf, what else works?" question is the main thing MCP buys over the
|
||||||
|
built-in slot alternatives.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build order (current)
|
||||||
|
|
||||||
|
1. `/simplify` pass (running) — behavior-preserving cleanup of the existing
|
||||||
|
code so the next steps build on something tidy.
|
||||||
|
2. **Pantry polish** — In/Low/Out state + fast add in the web UI (§2). Scoped
|
||||||
|
to "a bit nicer", not the full redesign.
|
||||||
|
3. **Auth** — login page + ~1-year session, drop `@csrf_exempt` (§1). Fold the
|
||||||
|
cheap production baseline (DEBUG off, real SECRET_KEY) in here since we're
|
||||||
|
touching settings anyway.
|
||||||
|
4. **MCP server** — the goal. Expose pantry **read and update** to claude.ai
|
||||||
|
cooking mode, so Claude can both see what's in stock and write changes back.
|
||||||
|
Reuses the existing `/api/` + token auth.
|
||||||
|
|
||||||
|
Deferred until after the above (still wanted, just not now): web
|
||||||
|
what-can-i-cook, §4.1 slot-based substitutions, shopping flow rethink, web
|
||||||
|
cook-logging.
|
||||||
|
|
||||||
|
## Open decisions to confirm before building
|
||||||
|
|
||||||
|
- **Quantity**: drop exact decimals entirely in favour of In/Low/Out + an
|
||||||
|
optional free-text note? (§2.1) — recommended yes.
|
||||||
|
- **Fixed recipes**: park the URL-import feature rather than fix it? (§3) —
|
||||||
|
recommended yes.
|
||||||
|
- Anything here that's actually *more* friction than today — call it out.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Original notes (Tom, preserved)
|
||||||
|
|
||||||
|
+ interface: never really ended up using caine due to cost issues. was a nice idea though.
|
||||||
|
+ if we went down the route of llms again, maybe a mcp. could claude.ai connect with it
|
||||||
|
+ current food flow is using claude.ai and its "cooking mode", issue is that it doesnt integrate with my pantry
|
||||||
|
+ pantry was tedious to update, not sure the best approach here. might just have to tough it out
|
||||||
|
+ webui interface was generally clunky
|
||||||
|
+ security not the highest prio but would be good to have some basic auth
|
||||||
|
+ lets run the /simplify skill firstly
|
||||||
|
+ tagging recipes never really worked, feels like theres two implementations of it hanging around
|
||||||
|
+ auth just scares me, i want the least friction way of doing this. a hmac secret or something might be nice for fe <> be and a simple log in page would be good. although i really dont want to have to log in every time
|
||||||
|
+ pantry: it's all just frustrating at the moment (adding slow, quantities drift, expiry fiddly, easy to forget)
|
||||||
+263
@@ -0,0 +1,263 @@
|
|||||||
|
# Research: How the Food app works today
|
||||||
|
|
||||||
|
> Snapshot taken 2026-06-22, reading the code on `master` (HEAD `96d9c4b`).
|
||||||
|
> Purpose: establish ground truth before making changes. Where the shipped
|
||||||
|
> docs (`Meal Planning System.md`) disagree with the code, the **code** is
|
||||||
|
> treated as authoritative and the discrepancy is called out.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. What this is
|
||||||
|
|
||||||
|
A single-user Django app for Tom: pantry inventory + "meta-recipes"
|
||||||
|
(cooking templates with swappable ingredient slots) + a smart shopping list +
|
||||||
|
a cook log. Two front doors:
|
||||||
|
|
||||||
|
- **REST API** (`/api/`) — token auth, built for Caine (the Matrix assistant).
|
||||||
|
- **HTMX web app** (`/app/`) — server-rendered HTML fragments, no auth.
|
||||||
|
|
||||||
|
Plus Django admin (`/admin/`) as the full-CRUD fallback.
|
||||||
|
|
||||||
|
There is **one Django app**, `kitchen`, holding everything. ~1000 lines of
|
||||||
|
Python total. No Celery, no JS build, no separate frontend — it's deliberately
|
||||||
|
small.
|
||||||
|
|
||||||
|
## 2. Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
food_project/ Django project config
|
||||||
|
settings.py single settings file, DEBUG=True (see §8)
|
||||||
|
urls.py routes /admin/, /api/, /api-auth/, /app/
|
||||||
|
kitchen/
|
||||||
|
models.py all 11 models
|
||||||
|
admin.py admin registrations + inlines
|
||||||
|
serializers.py DRF serializers (read + nested write)
|
||||||
|
views.py DRF viewsets + the "smart" API endpoints (JSON)
|
||||||
|
urls.py /api/ router + custom endpoints
|
||||||
|
views_htmx.py HTML-fragment views for the web app
|
||||||
|
urls_htmx.py /app/ routes
|
||||||
|
templates/kitchen/ base.html + 4 pages + 5 partials
|
||||||
|
static/kitchen/ htmx.min.js (vendored)
|
||||||
|
management/commands/seed.py initial data loader
|
||||||
|
migrations/ 0001_initial, 0002_add_rating_and_preferences
|
||||||
|
deploy/food.service systemd unit (gunicorn, runs as user `openclaw`)
|
||||||
|
db.sqlite3.backup-… committed snapshot = seed state only (see §9)
|
||||||
|
cookbooks/recipes_extracted.json 75 scraped Roasting Tin recipes (data only)
|
||||||
|
```
|
||||||
|
|
||||||
|
Note the README/design-doc say the project lives at `/var/lib/food/` in
|
||||||
|
production. This working copy is `/home/tom/srcs/food`. The DB
|
||||||
|
(`db.sqlite3`), `venv/`, and `staticfiles/` are gitignored.
|
||||||
|
|
||||||
|
## 3. Tech stack (from requirements.txt)
|
||||||
|
|
||||||
|
- Django 5.2.12 (LTS), SQLite, Django REST Framework 3.17.1
|
||||||
|
- HTMX 2.x (vendored JS), templates rendered server-side
|
||||||
|
- whitenoise (static files), gunicorn (prod)
|
||||||
|
- recipe_scrapers 15.11 + requests/lxml/extruct for URL recipe import
|
||||||
|
|
||||||
|
## 4. Data model (`models.py`)
|
||||||
|
|
||||||
|
The system models two kinds of meal: **meta-recipes** (templates) and
|
||||||
|
**fixed recipes** (traditional ingredient lists). Everything else supports
|
||||||
|
those plus inventory.
|
||||||
|
|
||||||
|
| Model | Role | Key fields / rules |
|
||||||
|
|---|---|---|
|
||||||
|
| `Tag` | ingredient category | just a unique name (protein, carb, veg, …) |
|
||||||
|
| `Ingredient` | master ingredient | `name` (unique, stored lowercased), `default_unit`, `tags` (M2M), `shelf_life_days`, `aliases` (JSON list), `preferences` (free text — how Tom likes it cooked) |
|
||||||
|
| `PantryItem` | current stock | FK ingredient, `quantity` (Decimal), `unit`, `location` ∈ {fridge,freezer,cupboard}, `stored_date` (auto), `expiry_date`, `is_staple`, `notes` |
|
||||||
|
| `MetaRecipe` | template | name, method (markdown text), times, `default_servings`, `gear_needed`, `tags` (JSON) |
|
||||||
|
| `Slot` | swappable category in a template | FK meta_recipe, name, `required`, `max_choices` |
|
||||||
|
| `SlotOption` | an ingredient that can fill a slot | FK slot+ingredient, `quantity_per_serving`, `unit`, `notes` |
|
||||||
|
| `MetaRecipeBase` | always-needed ingredients | FK meta_recipe+ingredient, `quantity_per_serving`, `unit` (onion/garlic/oil etc.) |
|
||||||
|
| `Recipe` | fixed recipe | name, method, times, `servings`, `source_url`, `source_book`, `tags` |
|
||||||
|
| `RecipeIngredient` | line in a fixed recipe | FK recipe+ingredient, `quantity`, `unit`, `optional` |
|
||||||
|
| `CookLog` | what was cooked | date (auto), FK to **either** meta_recipe **or** recipe, `slot_choices` (JSON), `servings`, `rating` (1-5), `notes` |
|
||||||
|
| `ShoppingListItem` | shopping line | optional FK ingredient + fallback `name`, qty/unit, `reason`, `checked` |
|
||||||
|
|
||||||
|
### Model-level business rules (enforced in `clean()`/`save()`)
|
||||||
|
|
||||||
|
- **`PantryItem.save()` calls `full_clean()`** on every save, so the rules
|
||||||
|
below fire on API writes, admin writes, and HTMX writes alike.
|
||||||
|
- Freezer item **must not** have an expiry date → raises ValidationError.
|
||||||
|
- Fridge item with no expiry auto-fills from `ingredient.shelf_life_days`
|
||||||
|
(if set); otherwise left null (no error).
|
||||||
|
- **`CookLog.clean()`**: must link to exactly one of meta_recipe / recipe
|
||||||
|
(not zero, not both). *But note `CookLog` does not override `save()`, so
|
||||||
|
`clean()` only runs when something calls `full_clean()` — DRF does, the
|
||||||
|
`log_cook` endpoint does **not** (it uses `objects.create`). So the
|
||||||
|
"exactly one" rule is not actually enforced on the main cook-logging path.*
|
||||||
|
|
||||||
|
## 5. The API (`views.py`, `/api/`)
|
||||||
|
|
||||||
|
Auth: `TokenAuthentication` + `SessionAuthentication`, **all endpoints
|
||||||
|
`IsAuthenticated`**. Caine uses a token; browser/admin uses session.
|
||||||
|
|
||||||
|
**Plain CRUD viewsets** (DefaultRouter): tags, ingredients, pantry,
|
||||||
|
meta-recipes, slots, slot-options, meta-recipe-bases, recipes,
|
||||||
|
recipe-ingredients, cook-log, shopping-list. `filterset_fields` are declared
|
||||||
|
on a couple of viewsets but **django-filter is not installed** (not in
|
||||||
|
requirements) and not in `INSTALLED_APPS`, so those filters are effectively
|
||||||
|
inert — query params like `?location=fridge` are silently ignored. Same for
|
||||||
|
`search_fields` without a `SearchFilter` backend configured.
|
||||||
|
|
||||||
|
**Custom endpoints** (the interesting logic):
|
||||||
|
|
||||||
|
- `GET /api/what-can-i-cook/?servings=N` — builds an in-memory pantry lookup
|
||||||
|
(ingredient_id → list of stock rows with expiry flags), then for every
|
||||||
|
meta-recipe checks each base ingredient and each slot, and for every fixed
|
||||||
|
recipe checks each line. Classifies as `ready` ✅ / `partial` ⚠️ /
|
||||||
|
`missing` ❌ and sorts ready-first. Staple base ingredients are assumed
|
||||||
|
always available. Expiry warnings attached per ingredient.
|
||||||
|
- `POST /api/log-cook/` — creates a `CookLog`; if `deduct: true`, subtracts
|
||||||
|
used quantities from the pantry via `_deduct_ingredient`. Accepts
|
||||||
|
meta_recipe_id **or** recipe_id, `slot_choices`, `servings`, `notes`.
|
||||||
|
**Does not accept `rating`** even though the model has the field.
|
||||||
|
- `POST /api/bulk-pantry-add/` — photo-intake path. Resolves each item by
|
||||||
|
name/alias, **auto-creates the ingredient if unknown**, computes fridge
|
||||||
|
expiry from shelf life, and merges into an existing same-location stock row
|
||||||
|
if one exists (adds quantity, refreshes expiry). Marks known staples via a
|
||||||
|
hardcoded name set.
|
||||||
|
- `GET /api/generate-shopping-list/?recipes=…&servings=…&add=true` —
|
||||||
|
suggests: (1) staples at qty 0, (2) items expiring within 2 days,
|
||||||
|
(3) per requested recipe, the first option of any required slot that can't
|
||||||
|
currently be filled + missing base ingredients. Dedupes by name, sorts by
|
||||||
|
section+priority, optionally writes `ShoppingListItem` rows.
|
||||||
|
- `POST /api/create-meta-recipe/` (POST=create, PUT=update) — builds a whole
|
||||||
|
meta-recipe (slots + options + bases) in one nested call. On PUT it
|
||||||
|
**deletes and rebuilds** all slots and bases. Auto-creates ingredients by
|
||||||
|
name, can tag them.
|
||||||
|
- `POST /api/import-recipe/` — fetches a URL, parses with `recipe_scrapers`,
|
||||||
|
optionally creates a `Recipe`. **See the gotcha in §7 — it never creates
|
||||||
|
`RecipeIngredient` rows.**
|
||||||
|
|
||||||
|
`_deduct_ingredient` ordering note: the docstring/comment says "prefer fridge,
|
||||||
|
then cupboard, then freezer", but the queryset only does
|
||||||
|
`.order_by("expiry_date")`. In SQLite NULLs sort first, and freezer items have
|
||||||
|
null expiry — so **freezer stock is actually consumed first**, the opposite of
|
||||||
|
the stated intent.
|
||||||
|
|
||||||
|
## 6. The HTMX web app (`views_htmx.py`, `/app/`)
|
||||||
|
|
||||||
|
Four pages, server-rendered, each swapping HTML partials over HTMX:
|
||||||
|
|
||||||
|
- **Pantry** (`/app/`) — add item (auto-creates ingredient, merges into
|
||||||
|
existing stock), delete, move fridge↔freezer (freezer clears expiry; fridge
|
||||||
|
sets +shelf_life or +7d), inline-edit expiry. Items grouped fridge /
|
||||||
|
freezer / cupboard with expired/expiring colour badges.
|
||||||
|
- **Recipes** (`/app/recipes/`) — "what can I cook", **meta-recipes only**
|
||||||
|
(fixed recipes are *not* shown here, unlike the API endpoint). Servings is
|
||||||
|
**hardcoded to 2**. This re-implements the matching logic from
|
||||||
|
`what_can_i_cook` in a second place (see §7).
|
||||||
|
- **Shopping** (`/app/shopping/`) — generate (staples + expiring + recipe
|
||||||
|
gaps, with a summary count), toggle checked, clear checked.
|
||||||
|
- **Cook Log** (`/app/log/`) — read-only history of the last 50 entries with
|
||||||
|
star ratings. **There is no way to create a cook log from the web app** —
|
||||||
|
only via API or admin.
|
||||||
|
|
||||||
|
All HTMX action views are `@csrf_exempt` (commit `6786502` — "localhost-only
|
||||||
|
app"). CSRF token is still wired into `base.html` and sent, but the server
|
||||||
|
ignores it.
|
||||||
|
|
||||||
|
Front-end gaps worth knowing before changing things: !! this has become more important since no more caine
|
||||||
|
- No UI to log a cook, set a rating, or pick slot choices for a real meal.
|
||||||
|
- No UI for fixed recipes at all (browse, import, or cook).
|
||||||
|
- No UI to edit ingredients, aliases, preferences, tags, or staples.
|
||||||
|
- No UI to manage/create meta-recipes (admin or API only).
|
||||||
|
- Servings is fixed at 2 across the recipes page.
|
||||||
|
|
||||||
|
## 7. Duplication & correctness traps (read before editing)
|
||||||
|
|
||||||
|
1. **Two copies of the "what can I cook" matcher.** `views.what_can_i_cook`
|
||||||
|
(JSON, includes fixed recipes, configurable servings) and
|
||||||
|
`views_htmx.recipes_page` (HTML, meta-only, servings=2) implement the same
|
||||||
|
logic separately. They have already drifted. Any change to matching rules
|
||||||
|
must be made in both, or the logic should be extracted into one shared
|
||||||
|
helper first.
|
||||||
|
|
||||||
|
2. **Recipe URL import creates a `Recipe` with zero `RecipeIngredient`s.**
|
||||||
|
`import_recipe_url` stores the scraped ingredient list only as raw text in
|
||||||
|
the response ("needs_review"); it never links ingredients to the model.
|
||||||
|
Consequence: an imported fixed recipe has an empty ingredient list, so
|
||||||
|
`what_can_i_cook` considers it **always "ready"** (nothing to be missing).
|
||||||
|
Linking ingredients is a manual admin/API step that nothing automates yet.
|
||||||
|
|
||||||
|
3. **Three different "expiring soon" windows.** `what_can_i_cook` uses ≤2
|
||||||
|
days, `PantryItem.expiring` API action uses ≤3 days, shopping generation
|
||||||
|
uses ≤2 days, and the HTMX pantry/recipe views use ≤2 days. No single
|
||||||
|
constant — easy to make them inconsistent further.
|
||||||
|
|
||||||
|
4. **`log_cook` bypasses `CookLog.clean()`** (uses `.create`, no
|
||||||
|
`full_clean`), so the "exactly one recipe link" invariant isn't enforced
|
||||||
|
on that path, and `rating` can't be set through it.
|
||||||
|
|
||||||
|
5. **Deduction order contradicts its comment** (see §5).
|
||||||
|
|
||||||
|
6. **`is_staple` / restock relies on quantity reaching exactly 0.** Staple
|
||||||
|
restock logic filters `quantity=0`. Items are decremented by deduction and
|
||||||
|
merged on add, but nothing deletes a depleted row, so a staple at 0 stays
|
||||||
|
as a 0-qty row — which is what restock/shopping keys off. Fine, but means
|
||||||
|
stock rows accumulate rather than disappear.
|
||||||
|
|
||||||
|
7. **Declared `filterset_fields`/`search_fields` do nothing** (no
|
||||||
|
django-filter / filter backend installed) — don't rely on them.
|
||||||
|
|
||||||
|
## 8. Security / production state
|
||||||
|
|
||||||
|
Matches the "TODO" list in the design doc — none of these are done in code:
|
||||||
|
|
||||||
|
|
||||||
|
- `DEBUG = True` and a literal `django-insecure-…` `SECRET_KEY` checked into
|
||||||
|
`settings.py`. No env-var override.
|
||||||
|
- `ALLOWED_HOSTS` includes `food.tomflux.xyz` and `food.jihakuz.xyz`.
|
||||||
|
- **HTMX app has no authentication** and is CSRF-exempt. The design doc says
|
||||||
|
it's exposed at `food.tomflux.xyz`; protection relies entirely on nginx
|
||||||
|
blocking `/api/` and `/admin/`, and on nobody pointing at `/app/`. Anyone
|
||||||
|
who can reach `/app/` can read and mutate the whole pantry.
|
||||||
|
- API is properly auth'd (token/session, IsAuthenticated).
|
||||||
|
- Secrets/hardening (DEBUG off, real SECRET_KEY, session-auth on `/app/`) are
|
||||||
|
all still open.
|
||||||
|
|
||||||
|
## 9. Seed data vs. the live database (important)
|
||||||
|
|
||||||
|
`seed.py` creates: 8 tags, 29 ingredients, 22 pantry items, and **only 2
|
||||||
|
meta-recipes** (Stir Fry, Traybake) — no fixed recipes, no cook logs.
|
||||||
|
|
||||||
|
The committed `db.sqlite3.backup-20260402-174550` matches the seed exactly:
|
||||||
|
2 meta-recipes, 0 fixed recipes, 0 cook logs, 0 shopping items, 16 slot
|
||||||
|
options.
|
||||||
|
|
||||||
|
The design doc, however, describes **6 meta-recipes** (the 4 extra from "The
|
||||||
|
Quick Roasting Tin": Baked Pasta, One-Tin Curry, Roasted Fish, Traybake
|
||||||
|
Meatballs), 30+ extra ingredients, and 75 imported recipes. **None of that is
|
||||||
|
in this repo** — it was created on the production DB via the
|
||||||
|
`create-meta-recipe` / import APIs and never re-seeded. `cookbooks/
|
||||||
|
recipes_extracted.json` holds the raw scraped data but nothing loads it
|
||||||
|
automatically.
|
||||||
|
|
||||||
|
Implication: a fresh `migrate && seed` here gives you the minimal 2-recipe
|
||||||
|
system, *not* what's described in the docs or running in prod. If you need
|
||||||
|
parity, you'd pull the prod DB or replay the API calls.
|
||||||
|
|
||||||
|
## 10. Things to decide before changing (open questions)
|
||||||
|
|
||||||
|
These come straight out of the gaps above — likely candidates for "the
|
||||||
|
changes" depending on intent:
|
||||||
|
|
||||||
|
- **Auth on `/app/`** — the standing TODO. Add `login_required` (session
|
||||||
|
auth) or keep relying on nginx?
|
||||||
|
- **Cook logging from the web** — currently API/admin only; no rating capture
|
||||||
|
on the main path.
|
||||||
|
- **Fixed recipes** are half-built: importable but not ingredient-linked, not
|
||||||
|
shown in the web UI, and trivially "always cookable".
|
||||||
|
- **Unify the two matchers** before touching matching behaviour.
|
||||||
|
- **Production hardening** (DEBUG, SECRET_KEY) untouched.
|
||||||
|
- **Shopping list UX** — the design doc explicitly notes "Tom not happy with
|
||||||
|
current flow, needs more thought."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Method: read every Python module, all templates, settings, urls, migrations,
|
||||||
|
the seed command, the deploy unit, and inspected the committed SQLite backup.
|
||||||
|
No code was changed.*
|
||||||
Reference in New Issue
Block a user