From f2bd667b7534376de3970f4056d00f0e70430a47 Mon Sep 17 00:00:00 2001 From: James Brechtel Date: Mon, 25 May 2026 21:45:49 -0400 Subject: [PATCH] docs: add cooking log design spec --- .gitignore | 1 + docs/specs/2026-05-25-cooking-log-design.md | 146 ++++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 docs/specs/2026-05-25-cooking-log-design.md diff --git a/.gitignore b/.gitignore index 07828d5..7da5b39 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ build/ .pi/ __pycache__ .worktrees/ +.superpowers/ diff --git a/docs/specs/2026-05-25-cooking-log-design.md b/docs/specs/2026-05-25-cooking-log-design.md new file mode 100644 index 0000000..7cd6a3b --- /dev/null +++ b/docs/specs/2026-05-25-cooking-log-design.md @@ -0,0 +1,146 @@ +# Cooking Log Design + +Record dates and comments when cooking a recipe, viewable on the recipe page +and across all recipes by date. + +## Data Model + +### SQLite schema + +A single `cook_log` table: + +```sql +CREATE TABLE IF NOT EXISTS cook_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + recipe_filename TEXT NOT NULL, + cooked_date TEXT NOT NULL, -- ISO date (YYYY-MM-DD) + comment TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL -- ISO datetime +); + +CREATE INDEX idx_cook_log_recipe ON cook_log(recipe_filename); +CREATE INDEX idx_cook_log_date ON cook_log(cooked_date DESC); +``` + +- `recipe_filename` is the lowercased filename with `.cook` stripped, + matching the key used by the recipe index. +- `cooked_date` stores the user-picked date (not necessarily today). +- `comment` is optional (defaults to empty string). + +### Haskell types + +New module `Roux.CookLog`: + +```haskell +data CookEntry = CookEntry + { ceId :: Int + , ceRecipeFilename :: Text + , ceCookedDate :: Day + , ceComment :: Text + , ceCreatedAt :: UTCTime + } +``` + +- `Day` (from `Data.Time.Calendar`) for dates. +- `UTCTime` for `created_at`. + +## API Endpoints + +All routes live in `Roux.Server` following existing patterns. + +| Method | Path | Purpose | +|--------|------|---------| +| `POST /cook-log/{filename}` | Record a cooking event | Form body: `cooked-date` (required, YYYY-MM-DD) + `comment` (optional). Returns JSON `{ ok: true }` on success, `{ error: "..." }` on failure. Multiple entries for the same date are allowed — no uniqueness constraint. | +| `GET /cook-log/{filename}` | Fetch entries for one recipe | JSON array of `CookEntry`, sorted by `cooked_date DESC` | +| `GET /cook-log` | Fetch all entries | JSON array of `CookEntry` with recipe info, sorted by `cooked_date DESC`, for cross-date view | + +### DB connection + +SQLite database file at `/cook-log.db`. Opened once at server +startup, passed to handlers via a shared value (same `IORef` pattern as the +recipe state map). + +### Library: `sqlite-simple` + +Minimal dependency. Pure SQL, typed results via `FromRow` instances. + +``` +dependencies: + - sqlite-simple +``` + +## Frontend Rendering + +### "I cooked this" form at the top of the marginalia section + +```html +

Cook history

+
+ + + +
+``` + +- `` gives a native date picker, defaults to today. +- Comment is an optional text input, max 500 chars. +- Form POSTs via `fetch()` with `FormData`, no page reload. +- On success, JS appends the new entry to the entry list below. +- On error, JS shows the error message inline. + +### Entry list in the marginalia column + +``` +── Mar 15 ── +Used baking powder instead of soda, turned out fine + +── Mar 2 ── +Added extra vanilla + +── Feb 20 ── +``` + +Rendered style: +- Date centered between dashes, bold/small. +- Comment below, plain text. +- No comment = just the date separator line. + +### Recipe page rendering + +`recipePage` in `Html.hs` accepts cook entries as an additional parameter +(or fetches them server-side during page render). The log section renders +inside the marginalia column, after Tags and before Notes. + +### JavaScript + +Lightweight inline script (new constant alongside `recipeImageUploadJs`): + +```javascript +document.querySelector('.roux-cook-log-form').addEventListener('submit', async (e) => { + e.preventDefault(); + const form = e.target; + const fd = new FormData(form); + const res = await fetch('/cook-log/' + form.dataset.filename, { method: 'POST', body: fd }); + if (!res.ok) return; + const entry = await res.json(); + // Prepend rendered entry to the list +}); +``` + +## Cross-Date View + +- Page at `/cook-history` showing all log entries grouped by month, + newest first. +- Each entry shows: date, recipe name (linked), comment (if any). +- Accessible from a link in the main navbar ("Cook History"). +- SSR rendered (no additional JS needed). + +## Implementation Order + +1. Add `sqlite-simple` dependency and `Roux.CookLog` module (types + DB ops) +2. Initialize DB at server startup (create table, open connection) +3. Implement POST/GET endpoints for single-recipe log entries +4. Integrate cook log entries into recipe page SSR in marginalia column +5. Add "I cooked this" form with JS submit handler +6. Implement cross-date view at `/cook-history` +7. Add nav link to cook history