From 998898c549d1a5ddf4efa454beaecb8d029d34bb Mon Sep 17 00:00:00 2001 From: dev_agent Date: Sat, 3 Oct 2026 00:15:12 -0400 Subject: [PATCH 1/4] Add AGENTS.md, CLAUDE.md, README and architecture docs AGENTS.md is the authoritative agent guide: house rules, approved dependencies, quality floor, repository layout, branch and merge conventions, where context and work tracking live, GLOSSARY vocabulary and the rule never to use tea. CLAUDE.md imports it. README gives the purpose and a make dev quick start. docs/ARCHITECTURE.md mirrors the CTXD CONTEXT.MD architecture section. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 143 +++++++++++++++++++++++++++++ CLAUDE.md | 1 + README.md | 50 ++++++++++ docs/ARCHITECTURE.md | 214 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 408 insertions(+) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 docs/ARCHITECTURE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d306043 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,143 @@ +# AGENTS.md — MealQuest agent guide + +This is the authoritative guide for every agent (and human) working in this +repository. `CLAUDE.md` only imports this file. Where this guide and a general +habit disagree, this guide wins. + +MealQuest is a single Go service with an embedded React PWA for one +household. The architecture of record is [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md); +product requirements are in [design/MealQuest Requirements.md](design/MealQuest%20Requirements.md). + +## House rules + +1. **Fail fast.** Invalid configuration, input or state is an error reported + immediately and clearly — the service refuses to start on bad config. +2. **No silent fallbacks.** Never substitute a default, retry quietly, or + swallow an error to "keep going". If something is missing, say so. +3. **One way to do a thing.** No parallel code paths, alternative settings or + "just in case" options. Pick one approach and use it everywhere. +4. **No new third-party dependency without owner approval.** Use only the + approved list below; anything else needs the owner's explicit yes first. +5. **Never use `tea`.** The Gitea CLI on this host is authenticated as the + human owner. Agents act only through their own Gitea identities. + +## Approved dependencies (from CTXD) + +Backend: + +- Go 1.25, built with `CGO_ENABLED=0`. +- `github.com/go-chi/chi/v5` — HTTP router. +- `modernc.org/sqlite` — CGO-free SQLite driver. +- `sqlc` — query code generation (dev tool only). +- `github.com/coreos/go-oidc/v3` — OIDC (Authentik). +- `golang.org/x/oauth2` and `golang.org/x/oauth2/google`. +- `github.com/SherClockHolmes/webpush-go` — Web Push. +- `golang.org/x/image` — photo processing. +- `github.com/oklog/ulid/v2` — IDs. +- Standard library `log/slog` and `net/http`. +- **Not allowed:** LLM vendor SDKs, Google API SDKs (call the HTTP APIs + directly), goose, golang-migrate. + +Frontend: + +- React 19, TypeScript, Vite. +- React Router 7, TanStack Query 5. +- `vite-plugin-pwa`. +- `lucide-react` icons at stroke width 2.75. +- CSS Modules over the design tokens in + [tokens.css](design/MealQuest%20UI%20Designs/design_handoff_mealquest/tokens.css). + **No Tailwind, no Framer Motion, no hard-coded hex colours.** +- Self-hosted fonts: Bagel Fat One and Figtree. +- Tests: Vitest, Testing Library, jsdom. + +## Quality floor + +Every change must pass all of these before review (CI in +[.gitea/workflows/ci.yml](.gitea/workflows/ci.yml) enforces them): + +```sh +gofmt -l . # must print nothing +go vet ./... +go test -race ./... +cd web && npm run typecheck && npm test && npm run build +``` + +And the build entry points in the [Makefile](Makefile) and [Dockerfile](Dockerfile): + +```sh +make test +make build +docker build . +``` + +Lint proves code parses, not that it works: where a change has runtime +behaviour, exercise it (hit the endpoint, render the page). + +## Repository layout + +| Path | Purpose | +| --- | --- | +| `cmd/server` | Entry point. Wiring only — no business logic. | +| `internal/config` | `MEALQUEST_*` environment config; secrets from `*_FILE`. | +| `internal/server` | HTTP server, routes, `/health`. | +| `internal/frontend` | `go:embed` of the built PWA with SPA fallback. | +| `internal/*` (planned) | `auth`, `db`, `store`, `secretbox`, `household`, `dinners`, `recipes`, `ai/llm`, `ai/harness`, `ai/skills`, `suggest`, `plan`, `shopping`, `ideas`, `gcal`, `notify`, `push`, `chat`, `jobs`, `scheduler`. | +| `migrations/` | SQL migrations (planned). | +| `web/` | React PWA source. | +| `deploy/` | Deployment files (planned). | +| `docs/` | Project documentation, including [ARCHITECTURE.md](docs/ARCHITECTURE.md). | +| `design/` | Requirements and UI handoff. **Read-only reference** — do not edit. | +| `Dockerfile` | Multi-stage image build, at the repository root. | +| `Makefile` | `dev`, `test`, `build`, `docker`. | +| `.gitea/workflows/ci.yml` | CI quality floor. | + +Design references: + +- [design/MealQuest Requirements.md](design/MealQuest%20Requirements.md) +- [design/MealQuest UI Designs/design_handoff_mealquest/README.md](design/MealQuest%20UI%20Designs/design_handoff_mealquest/README.md) +- [design/MealQuest UI Designs/design_handoff_mealquest/tokens.css](design/MealQuest%20UI%20Designs/design_handoff_mealquest/tokens.css) + +## Branches, pull requests and merges + +- Integration branch is `dev`. `main` is the release branch. +- **Feature branches:** `feature/-`, branched from `dev`. +- **Task branches:** `task/_`, branched from the feature branch. +- Task pull requests **squash-merge into the feature branch**. +- The feature pull request merges into `dev`. +- **Only `overseer` merges `dev` → `main`** (a release). +- Never push directly to `dev` or `main`. +- Authors never review, approve, merge or close their own work. +- Never store credentials in the repository; environment or ignored files only. + +## Where context lives and where work is tracked + +- **Context:** CTXD project `mealquest` — client guide at + . Project documents: `CONTEXT.MD` + (architecture), `DECISIONS.MD` (decision log, e.g. D-021), `RUNBOOKS.MD`, + `PROMPTS.MD`, `GLOSSARY.MD`. +- **Work tracking:** Maestro at , + workspace **Maniacs-Mansion** → project **MealQuest**. Every change has a + task with a verifiable done-condition. + +## Vocabulary (GLOSSARY) + +Use these product terms in code names, UI copy, docs and commit messages. Do +not invent synonyms. + +| Term | Meaning | +| --- | --- | +| **Regulars** | Dinners the household makes routinely. | +| **Side quest** | An AI-suggested new dinner to try. | +| **Quest log** | The to-try pool of accepted Side quests, scheduled later. | +| **Try it / Pass** | Accept (swipe right) / decline (swipe left) a suggestion. | +| **Rotation check** / **"Keep it in rotation?"** | The prompt after a Quest-log dinner is tried, asking whether it becomes a Regular. Promotion is always the household's choice, never automatic. | +| **Supply run** | The current week's shopping list, grouped by dinner. | +| **Supply archive** | The blog-style history of past shopping lists (by week, month, year). | +| **Draft board** | The mobile weekly plan layout the AI drafts for the household to adjust. | +| **Rebuild week** | Ask the AI to redraft the week's plan. | +| **Push to Calendar** | Push the plan one-way to Google Calendar. | +| **Quick dinner** | Cooked in a crock pot **or** 30 minutes or less from prep to table. | +| **Cook from what I have** | Ideas from a list of ingredients on hand. | +| **Kitchen reading mode** | Tablet recipe view: large text, ingredient checkboxes, step-by-step. | +| **"Why this?"** | The reason shown for a suggestion, referencing the household's Regulars. | +| **Our kitchen / Shared household** | The single shared household identity; no per-person profiles. | diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/README.md b/README.md index e69de29..771740c 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,50 @@ +# MealQuest + +MealQuest is a mobile-first Progressive Web App for weekly dinner planning for +one household. The household lists the dinners it makes regularly, an AI +assistant suggests new dinners to try (swipe "Try it" or "Pass"), and the app +builds a weekly plan, pushes it to Google Calendar, and generates a shopping +list grouped by dinner, kept as a browsable weekly history. It ships as a +single Go service with the React PWA embedded in the binary. + +> Status: this repository is the Feature 01 scaffold (Go service with +> `/health`, embedded PWA skeleton, Makefile, Dockerfile, CI). The planned +> application is described in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). + +## Quick start + +Requirements: Go 1.25 and Node.js 22. + +```sh +(cd web && npm ci) +mkdir -p ~/.mealquest-dev # outside the repository: it holds a secret +head -c 32 /dev/urandom | base64 > ~/.mealquest-dev/secret_key +export MEALQUEST_HTTP_ADDR=:8080 +export MEALQUEST_PUBLIC_URL=http://localhost:5173 +export MEALQUEST_DATA_DIR="$HOME/.mealquest-dev" +export MEALQUEST_SHUTDOWN_TIMEOUT=10s +export MEALQUEST_SECRET_KEY_FILE="$HOME/.mealquest-dev/secret_key" +make dev +``` + +`make dev` runs the Go server on `:8080` and the Vite dev server on `:5173` +(which proxies `/api`, `/auth` and `/media` to `:8080`). Open +http://localhost:5173. Configuration is environment-only and fails fast: +every variable above is required, and secrets are read only from files named +by `*_FILE` variables. + +## Common commands + +| Command | What it does | +| -------------- | --------------------------------------------------------------- | +| `make dev` | Go server plus Vite dev server. | +| `make test` | `gofmt` check, `go vet`, `go test -race`, web typecheck and tests. | +| `make build` | Build the PWA, embed it, build the static binary `bin/mealquest`. | +| `make docker` | Build the container image from the root `Dockerfile`. | + +## Where to read next + +- [AGENTS.md](AGENTS.md) — house rules, approved dependencies, conventions. +- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — architecture of record. +- [design/MealQuest Requirements.md](design/MealQuest%20Requirements.md) — product requirements. +- [design/MealQuest UI Designs/design_handoff_mealquest/README.md](design/MealQuest%20UI%20Designs/design_handoff_mealquest/README.md) — UI handoff. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..1c1e312 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,214 @@ +# MealQuest architecture + +This mirrors the architecture section of `CONTEXT.MD` in the CTXD project +`mealquest`, which is the source of record; if the two disagree, CTXD wins and +this file should be updated. It describes the **planned** application. The +repository today is the Feature 01 scaffold: `cmd/server`, `internal/config`, +`internal/server` (`/health`), `internal/frontend` (embedded PWA with SPA +fallback), the `web/` skeleton, `Makefile`, `Dockerfile` and CI. + +Decision numbers (D-NNN) refer to `DECISIONS.MD` in CTXD. + +## Topology + +``` +PWA (phone / tablet / desktop) + │ HTTPS + ▼ +Traefik v3.6 — 10.60.1.33 + file route: mealquest-ccl.yml + │ + ▼ +docker compose project "mealquest" — 10.60.1.14 + ├── mealquest Go service + embedded PWA, port 8090 + ├── searxng sidecar for recipe search + └── Litestream SQLite replication → Synology NAS 10.60.1.50 +``` + +One container serves the API, the auth flow, media and the PWA. SQLite lives +on the host volume and is continuously replicated by Litestream to the NAS. + +## Repository layout + +| Path | Purpose | +| --- | --- | +| `cmd/server` | Entry point; wiring only. | +| `internal/config` | Env-only `MEALQUEST_*` config; secrets from `*_FILE`. | +| `internal/server` | HTTP server, router, middleware. | +| `internal/frontend` | Embedded PWA, SPA fallback. | +| `internal/auth` | Authentik OIDC, sessions, CSRF. | +| `internal/db`, `internal/store` | SQLite connections, sqlc queries. | +| `internal/secretbox` | AES-256-GCM encryption for `_ct` columns. | +| `internal/household`, `internal/dinners`, `internal/recipes` | Domain. | +| `internal/ai/llm`, `internal/ai/harness`, `internal/ai/skills` | AI provider adapters, skill harness, skills. | +| `internal/suggest`, `internal/plan`, `internal/shopping`, `internal/ideas` | Feature services. | +| `internal/gcal`, `internal/notify`, `internal/push`, `internal/chat` | Integrations. | +| `internal/jobs`, `internal/scheduler` | Background jobs and schedules. | +| `migrations/` | SQL migrations (embedded; no goose/golang-migrate). | +| `web/` | React PWA. | +| `deploy/` | Compose, Traefik route, Litestream config. | +| `docs/` | Documentation. | +| `design/` | Requirements and UI handoff (read-only reference). | + +## Backend stack and conventions + +- Go 1.25, `CGO_ENABLED=0`, a single static binary. +- Router: `go-chi/chi/v5`. Logging: `log/slog` (structured). +- SQLite via `modernc.org/sqlite`, with `journal_mode=WAL`, + `foreign_keys=ON` and `busy_timeout`. **One writer connection plus a read + pool.** Queries generated by `sqlc`. +- IDs are ULIDs (`oklog/ulid/v2`). +- Timestamps are stored in UTC; local calendar dates are `YYYY-MM-DD` strings + in the household timezone. +- Configuration is environment-only (`MEALQUEST_*`). Secrets come only from + files named by `*_FILE` variables. Missing or invalid config stops startup. +- Error responses always have the body + `{"error":{"code":"…","message":"…","retryable":false}}`. + +## Authentication + +- Authentik OIDC; the backend is a **confidential client** (authorization + code flow handled server-side with `coreos/go-oidc/v3` and `x/oauth2`). +- Session cookie `__Host-mealquest_session` (Secure, HttpOnly, path `/`). +- Unsafe requests require the CSRF header `X-MealQuest-CSRF`. +- Access requires membership of the Authentik group `mealquest-admin`. + There is a **single role**: one shared household, no per-person profiles. + +## Data model + +Columns ending in `_ct` hold AES-256-GCM ciphertext (via `secretbox`). + +| Table | Holds | +| --- | --- | +| `household_settings` | Household name, timezone, preferences. | +| `recipes` | Dinners: name, method, minutes, difficulty, source, lifecycle status. | +| `recipe_ingredients` | Ingredients with quantities per recipe. | +| `recipe_steps` | Ordered steps per recipe. | +| `photos` | Cached recipe photos. | +| `dinners` | Dinner records and lifecycle state. | +| `suggestion_batches` | One AI suggestion run. | +| `suggestions` | Side quests with reason ("Why this?") and Try it / Pass status. | +| `week_plans` | One plan per Monday-start week; draft or synced. | +| `plan_days` | The dinner for each day of a week plan. | +| `rotation_reviews` | Rotation check answers ("Keep it in rotation?"). | +| `shopping_lists` | One Supply run per week; frozen into the Supply archive after the week. | +| `shopping_groups` | Items grouped by dinner. | +| `shopping_items` | Items, quantity, needed / have-it. | +| `ai_settings` | Provider, model, temperature, system prompt, API key (`_ct`). | +| `recipe_search_settings` | Recipe search configuration (searxng). | +| `integrations_google` | Google Calendar OAuth tokens (`_ct`) and calendar id. | +| `chat_settings` | Google Chat webhook URL (`_ct`). | +| `notification_prefs` | Per-event push / Chat toggles, reminder time. | +| `push_subscriptions` | Web Push subscriptions. | +| `notification_outbox` | Pending notifications for delivery. | +| `jobs` | Durable background job queue. | +| `schedule_runs` | Record of scheduled runs (idempotency). | +| `ai_runs` | Every AI skill call: inputs, outputs, validation, timing. | +| `sessions` | Server-side sessions. | + +## Dinner lifecycle + +``` +suggested ──Try it──▶ quest_log ──cooked──▶ tried ──Rotation check──▶ regular + │ └────────────────────▶ archived + └──Pass──▶ declined (not suggested again) +``` + +Promotion to Regular is **never automatic** (D-021): it happens only when the +household answers "Keep it in rotation?". + +## API surface + +- JSON API under `/api/v1`, using the error body above for every failure. +- `/auth/*` — OIDC login, callback, logout. +- `/media/*` — cached photos. +- `/health` — liveness. +- Everything else serves the embedded PWA with SPA fallback. + +## AI layer + +- **Provider interface** (`ai/llm`) with adapters for OpenAI-compatible, + Anthropic and Ollama endpoints, called over plain HTTP — no vendor SDKs. +- **Skill harness** (`ai/harness`): each skill (suggestions, week draft, + ingredient ideas, quick ideas, archive post titles, …) layers its prompt — + system prompt from `ai_settings`, skill instructions, household context — + then validates the structured output against the skill's schema. An invalid + response gets **one repair retry**; a second failure is an error, never a + silent fallback. Every call is recorded in `ai_runs`. + +## Recipe grounding + +- Recipes are grounded in real pages found via the `searxng` sidecar. +- Fetching is **SSRF-safe** (public addresses only, size and time limits). +- Recipe data is parsed from schema.org `Recipe` JSON-LD. +- Photos are downloaded, processed (`golang.org/x/image`) and cached locally; + the client never hot-links external images. + +## Scheduler + +Times are in the household timezone. + +| Schedule | When | Does | +| --- | --- | --- | +| `suggestions_weekly` | Sunday 08:00 | New batch of Side quests. | +| `plan_draft` | Friday 09:00 | AI drafts next week's plan. | +| `shopping_chat_autosend` | Saturday 08:00 | Sends the Supply run to Google Chat. | +| `day_of_reminder` | Daily 16:30 | Tonight's dinner reminder. | +| `rotation_check` | Daily 09:00 | Asks "Keep it in rotation?" for tried dinners. | +| `week_rollover` | Monday 00:05 | Freezes last week's list into the Supply archive, rolls the plan. | +| `ai_runs` purge | Periodic | Deletes old `ai_runs` rows. | + +Runs are recorded in `schedule_runs` and executed through `jobs`. + +## Notifications + +- Events: plan ready, list ready, day-of reminder, new suggestions; each can + go to Web Push and/or Google Chat per `notification_prefs`. +- Delivery goes through `notification_outbox`. +- Web Push uses VAPID (`webpush-go`). Google Chat uses the stored incoming + webhook. + +## Google Calendar + +- One-way push: the app is the source of truth; edits in Google Calendar do + not come back. +- OAuth once in Settings; one event per planned dinner. +- **Deterministic event ids** derived from the plan day, so re-pushing + updates the same events instead of creating duplicates. + +## Frontend + +- React 19, TypeScript, Vite, React Router 7, TanStack Query 5, + `vite-plugin-pwa`, `lucide-react` (stroke 2.75). +- CSS Modules over `tokens.css`; no Tailwind, no Framer Motion, no + hard-coded hex. Self-hosted Bagel Fat One and Figtree. +- Tests: Vitest, Testing Library, jsdom. +- Offline cache for the current plan, shopping list and recipes. + +| Route | Screen | +| --- | --- | +| `/` | Home | +| `/onboarding` | Add Regulars | +| `/plan` | Weekly plan (Draft board) | +| `/plan/shopping` | Supply run | +| `/plan/shopping/archive` | Supply archive | +| `/discover` | Swipe Side quests | +| `/quick` | Quick dinners | +| `/cook-from-pantry` | Cook from what I have | +| `/dinner/:id` | Recipe detail / Kitchen reading mode | +| `/settings` | Settings home | +| `/settings/notifications` | Notifications and Google Chat | +| `/settings/ai` | Admin · AI assistant | + +## Boundaries and gotchas + +- **Secrets never reach the client** — API keys, OAuth tokens and the Chat + webhook stay server-side, encrypted at rest. +- **The app is the source of truth for the plan**; Google Calendar is a + one-way mirror. +- **Never auto-promote to Regular** (D-021). +- **Quick** means cooked in a crock pot **or** ≤ 30 minutes prep to table. +- **Weeks run Monday–Sunday** in the household timezone. +- **No silent fallbacks** — fail fast and report. +- **iOS Web Push requires the PWA to be installed to the Home Screen**; show + an install hint. -- 2.55.0 From 37f6200250910877f1c9521953eba3497c395b0e Mon Sep 17 00:00:00 2001 From: dev_agent Date: Sat, 3 Oct 2026 00:16:58 -0400 Subject: [PATCH 2/4] Correct architecture doc to match the CTXD architecture section Replace the data model table with the CTXD tables and columns (recipes are fetched recipe pages; dinners hold the meal record and lifecycle), fix the dinner lifecycle (Pass leads to archived; tried can return to quest_log), add the session cookie attributes and expiry, require the session cookie and CSRF header on /api/v1, make the ai_runs purge daily, use the handoff routes plus the CTXD settings routes, and remove details not supported by CTXD, the requirements or the UI handoff. Co-Authored-By: Claude Opus 5.5 --- docs/ARCHITECTURE.md | 138 ++++++++++++++++++++++++------------------- 1 file changed, 77 insertions(+), 61 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 1c1e312..41ee40e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -25,8 +25,8 @@ docker compose project "mealquest" — 10.60.1.14 └── Litestream SQLite replication → Synology NAS 10.60.1.50 ``` -One container serves the API, the auth flow, media and the PWA. SQLite lives -on the host volume and is continuously replicated by Litestream to the NAS. +One Go service serves the API, the auth flow, media and the PWA. Its SQLite +database is replicated by Litestream to the NAS. ## Repository layout @@ -44,9 +44,9 @@ on the host volume and is continuously replicated by Litestream to the NAS. | `internal/suggest`, `internal/plan`, `internal/shopping`, `internal/ideas` | Feature services. | | `internal/gcal`, `internal/notify`, `internal/push`, `internal/chat` | Integrations. | | `internal/jobs`, `internal/scheduler` | Background jobs and schedules. | -| `migrations/` | SQL migrations (embedded; no goose/golang-migrate). | +| `migrations/` | SQL migrations (no goose or golang-migrate). | | `web/` | React PWA. | -| `deploy/` | Compose, Traefik route, Litestream config. | +| `deploy/` | Deployment files. | | `docs/` | Documentation. | | `design/` | Requirements and UI handoff (read-only reference). | @@ -67,10 +67,11 @@ on the host volume and is continuously replicated by Litestream to the NAS. ## Authentication -- Authentik OIDC; the backend is a **confidential client** (authorization - code flow handled server-side with `coreos/go-oidc/v3` and `x/oauth2`). -- Session cookie `__Host-mealquest_session` (Secure, HttpOnly, path `/`). -- Unsafe requests require the CSRF header `X-MealQuest-CSRF`. +- Authentik OIDC; the backend is a **confidential client** + (`coreos/go-oidc/v3`, `golang.org/x/oauth2`). +- Session cookie `__Host-mealquest_session` (HttpOnly, Secure, + SameSite=Lax). Sessions expire after 30 days idle or 180 days absolute. +- Non-GET requests require the CSRF header `X-MealQuest-CSRF`. - Access requires membership of the Authentik group `mealquest-admin`. There is a **single role**: one shared household, no per-person profiles. @@ -80,47 +81,55 @@ Columns ending in `_ct` hold AES-256-GCM ciphertext (via `secretbox`). | Table | Holds | | --- | --- | -| `household_settings` | Household name, timezone, preferences. | -| `recipes` | Dinners: name, method, minutes, difficulty, source, lifecycle status. | -| `recipe_ingredients` | Ingredients with quantities per recipe. | -| `recipe_steps` | Ordered steps per recipe. | -| `photos` | Cached recipe photos. | -| `dinners` | Dinner records and lifecycle state. | -| `suggestion_batches` | One AI suggestion run. | -| `suggestions` | Side quests with reason ("Why this?") and Try it / Pass status. | -| `week_plans` | One plan per Monday-start week; draft or synced. | -| `plan_days` | The dinner for each day of a week plan. | -| `rotation_reviews` | Rotation check answers ("Keep it in rotation?"). | -| `shopping_lists` | One Supply run per week; frozen into the Supply archive after the week. | -| `shopping_groups` | Items grouped by dinner. | -| `shopping_items` | Items, quantity, needed / have-it. | -| `ai_settings` | Provider, model, temperature, system prompt, API key (`_ct`). | -| `recipe_search_settings` | Recipe search configuration (searxng). | -| `integrations_google` | Google Calendar OAuth tokens (`_ct`) and calendar id. | -| `chat_settings` | Google Chat webhook URL (`_ct`). | -| `notification_prefs` | Per-event push / Chat toggles, reminder time. | -| `push_subscriptions` | Web Push subscriptions. | -| `notification_outbox` | Pending notifications for delivery. | -| `jobs` | Durable background job queue. | -| `schedule_runs` | Record of scheduled runs (idempotency). | -| `ai_runs` | Every AI skill call: inputs, outputs, validation, timing. | -| `sessions` | Server-side sessions. | +| `household_settings` | One row: timezone, dinner_time (17:30), reminder_time (16:30), review_time (09:00), plan_draft day/time (Fri 09:00), suggestions day/time (Sun 08:00), chat_autosend day/time (Sat 08:00), onboarding_completed_at, updates_seen_at. | +| `recipes` | A real recipe page: source_url (unique), site_name, title, total/prep/cook minutes, yield, raw_jsonld, fetched_at. | +| `recipe_ingredients` | recipe_id, position, raw_text, name, qty, unit, note. | +| `recipe_steps` | recipe_id, position, text. | +| `photos` | source_url, local_path, width, height, sha256, fetched_at. | +| `dinners` | The meal record and its lifecycle: name, normalized_name, status (`suggested` / `quest_log` / `tried` / `regular` / `archived`), origin, method, minutes_total, difficulty, cuisine, is_favorite, recipe_id, photo_id, grounding_status, updated_by. | +| `suggestion_batches` | trigger, status, ai_run_id. | +| `suggestions` | dinner_id, batch_id, reason, cited_regular_ids (JSON), status, decided_at. | +| `week_plans` | week_start (unique), status (`draft` / `synced` / `changed`), ai_note, ai_run_id, calendar_synced_at. | +| `plan_days` | plan_id, date (unique per plan), dinner_id, is_new, calendar_event_id. | +| `rotation_reviews` | dinner_id + plan_date (unique), status, decision (`keep` / `drop` / `not_made`), decided_at, updated_by. | +| `shopping_lists` | week_start (unique), plan_id, title, status (`current` / `archived`), sent_to_chat_at, frozen_at. | +| `shopping_groups` / `shopping_items` | One group per dinner/day; item name, qty, needed (bool), position. | +| `ai_settings` | One row: provider, base_url, model, temperature, system_prompt, api_key_ct, api_key_last4, key_verified_at. | +| `recipe_search_settings` | One row: provider (`searxng` / `brave`), base_url, api_key_ct. | +| `integrations_google` | One row: refresh_token_ct, calendar_id, calendar_name, connected_at, last_sync_error. | +| `chat_settings` | One row: webhook_url_ct, space_label, last_test_at, last_test_status. | +| `notification_prefs` | event (`plan_ready` / `list_ready` / `day_of_reminder` / `new_suggestions` / `rotation_review`), push, chat. | +| `push_subscriptions` | endpoint (unique), p256dh, auth, user_agent, failure_count, last_success_at. | +| `notification_outbox` | event_key + channel (unique), payload, status, attempts, next_attempt_at, last_error. | +| `jobs` | kind, status, payload, result (progressive), error, timestamps. | +| `schedule_runs` | schedule, period_key (unique). | +| `ai_runs` | skill, skill_version, provider, model, temperature, status, latency_ms, tokens_in/out, error, redacted request/response (30-day retention). | +| `sessions` | id_hash, subject, email, name, groups, csrf_token, created/last_seen/expires. | + +`recipes` holds a fetched recipe page; `dinners` holds the household's meal +record and its lifecycle, optionally linked to a recipe and a photo. ## Dinner lifecycle ``` -suggested ──Try it──▶ quest_log ──cooked──▶ tried ──Rotation check──▶ regular - │ └────────────────────▶ archived - └──Pass──▶ declined (not suggested again) +suggested (deck) ──Try it──▶ quest_log ──planned date passes──▶ tried + │ ▲ │ + │ └──────── explicit decision ─────┤ + │ ├──▶ regular + │ └──▶ archived + └──Pass──▶ archived (name kept for dedupe) ``` -Promotion to Regular is **never automatic** (D-021): it happens only when the -household answers "Keep it in rotation?". +From `tried`, an explicit decision moves the dinner to `regular`, to +`archived`, or back to `quest_log`. Promotion to `regular` is **never +automatic** (D-021). ## API surface - JSON API under `/api/v1`, using the error body above for every failure. -- `/auth/*` — OIDC login, callback, logout. + It requires the session cookie and, for non-GET requests, the + `X-MealQuest-CSRF` header. +- `/auth/*` — the OIDC flow. - `/media/*` — cached photos. - `/health` — liveness. - Everything else serves the embedded PWA with SPA fallback. @@ -129,24 +138,23 @@ household answers "Keep it in rotation?". - **Provider interface** (`ai/llm`) with adapters for OpenAI-compatible, Anthropic and Ollama endpoints, called over plain HTTP — no vendor SDKs. -- **Skill harness** (`ai/harness`): each skill (suggestions, week draft, - ingredient ideas, quick ideas, archive post titles, …) layers its prompt — - system prompt from `ai_settings`, skill instructions, household context — - then validates the structured output against the skill's schema. An invalid - response gets **one repair retry**; a second failure is an error, never a - silent fallback. Every call is recorded in `ai_runs`. +- **Skill harness** (`ai/harness`): prompt layering, validation of the + structured output, and **one repair retry**; if the repair also fails the + run fails — no silent fallback. Every call is recorded in `ai_runs`. ## Recipe grounding -- Recipes are grounded in real pages found via the `searxng` sidecar. -- Fetching is **SSRF-safe** (public addresses only, size and time limits). +- Dinners are grounded in real recipe pages found through recipe search + (`searxng` or `brave`, per `recipe_search_settings`). +- Fetching is **SSRF-safe**. - Recipe data is parsed from schema.org `Recipe` JSON-LD. -- Photos are downloaded, processed (`golang.org/x/image`) and cached locally; - the client never hot-links external images. +- Photos are cached locally (`photos`) and processed with + `golang.org/x/image`. ## Scheduler -Times are in the household timezone. +Times are in the household timezone; the defaults below are set in +`household_settings`. | Schedule | When | Does | | --- | --- | --- | @@ -155,15 +163,16 @@ Times are in the household timezone. | `shopping_chat_autosend` | Saturday 08:00 | Sends the Supply run to Google Chat. | | `day_of_reminder` | Daily 16:30 | Tonight's dinner reminder. | | `rotation_check` | Daily 09:00 | Asks "Keep it in rotation?" for tried dinners. | -| `week_rollover` | Monday 00:05 | Freezes last week's list into the Supply archive, rolls the plan. | -| `ai_runs` purge | Periodic | Deletes old `ai_runs` rows. | +| `week_rollover` | Monday 00:05 | Freezes last week's Supply run into the Supply archive. | +| `ai_runs` purge | Daily | Deletes `ai_runs` rows past 30-day retention. | Runs are recorded in `schedule_runs` and executed through `jobs`. ## Notifications -- Events: plan ready, list ready, day-of reminder, new suggestions; each can - go to Web Push and/or Google Chat per `notification_prefs`. +- Events: `plan_ready`, `list_ready`, `day_of_reminder`, `new_suggestions`, + `rotation_review`; each can go to Web Push and/or Google Chat per + `notification_prefs`. - Delivery goes through `notification_outbox`. - Web Push uses VAPID (`webpush-go`). Google Chat uses the stored incoming webhook. @@ -172,9 +181,10 @@ Runs are recorded in `schedule_runs` and executed through `jobs`. - One-way push: the app is the source of truth; edits in Google Calendar do not come back. -- OAuth once in Settings; one event per planned dinner. -- **Deterministic event ids** derived from the plan day, so re-pushing - updates the same events instead of creating duplicates. +- Connected once with Google OAuth in Settings; one event per planned + dinner. +- **Deterministic event ids**; each plan day records its + `calendar_event_id`. ## Frontend @@ -188,18 +198,24 @@ Runs are recorded in `schedule_runs` and executed through `jobs`. | Route | Screen | | --- | --- | | `/` | Home | -| `/onboarding` | Add Regulars | +| `/onboarding` | Add regular dinners | | `/plan` | Weekly plan (Draft board) | -| `/plan/shopping` | Supply run | +| `/plan/shopping` | Shopping list (Supply run) | | `/plan/shopping/archive` | Supply archive | -| `/discover` | Swipe Side quests | +| `/discover` | Swipe suggestions (Side quests) | | `/quick` | Quick dinners | | `/cook-from-pantry` | Cook from what I have | -| `/dinner/:id` | Recipe detail / Kitchen reading mode | +| `/dinner/:id` | Recipe / dinner detail (Kitchen reading mode on tablet) | | `/settings` | Settings home | +| `/settings/household` | Household settings | +| `/settings/regulars` | Regular dinners | +| `/settings/calendar` | Google Calendar connection | | `/settings/notifications` | Notifications and Google Chat | | `/settings/ai` | Admin · AI assistant | +`/settings/household`, `/settings/regulars` and `/settings/calendar` come from +CTXD; the other routes are defined in the UI handoff. + ## Boundaries and gotchas - **Secrets never reach the client** — API keys, OAuth tokens and the Chat -- 2.55.0 From bed51ed2bdb0c2cad3ea94a73ca9ed7dea85e5c1 Mon Sep 17 00:00:00 2001 From: dev_agent Date: Sat, 3 Oct 2026 00:20:30 -0400 Subject: [PATCH 3/4] Fix architecture and README accuracy findings from review - Topology: show the port mapping host 8090 -> container 8080, and note that searxng is internal to the compose network with no published port. - Backend: logging is log/slog JSON. - Scheduler: only the household-configurable times live in household_settings; week_rollover and the ai_runs purge are fixed. - README: state the Node.js floor required by the committed lockfile. Co-Authored-By: Claude Opus 5.5 --- README.md | 4 +++- docs/ARCHITECTURE.md | 19 ++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 771740c..6c5c3ca 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,9 @@ single Go service with the React PWA embedded in the binary. ## Quick start -Requirements: Go 1.25 and Node.js 22. +Requirements: Go 1.25 and Node.js 22.22.2 or newer (or 24.15 or newer). +Vite and Vitest need at least 22.12; jsdom in the committed lockfile needs +22.22.2 on the 22 line. ```sh (cd web && npm ci) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 41ee40e..4beae8c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -20,8 +20,9 @@ Traefik v3.6 — 10.60.1.33 │ ▼ docker compose project "mealquest" — 10.60.1.14 - ├── mealquest Go service + embedded PWA, port 8090 - ├── searxng sidecar for recipe search + ├── mealquest Go service + embedded PWA, host 8090 → container 8080 + ├── searxng sidecar for recipe search; internal compose network + │ only, no published port └── Litestream SQLite replication → Synology NAS 10.60.1.50 ``` @@ -53,7 +54,7 @@ database is replicated by Litestream to the NAS. ## Backend stack and conventions - Go 1.25, `CGO_ENABLED=0`, a single static binary. -- Router: `go-chi/chi/v5`. Logging: `log/slog` (structured). +- Router: `go-chi/chi/v5`. Logging: `log/slog` JSON. - SQLite via `modernc.org/sqlite`, with `journal_mode=WAL`, `foreign_keys=ON` and `busy_timeout`. **One writer connection plus a read pool.** Queries generated by `sqlc`. @@ -153,8 +154,12 @@ automatic** (D-021). ## Scheduler -Times are in the household timezone; the defaults below are set in -`household_settings`. +Times are in the household timezone. The times of `day_of_reminder` +(`reminder_time`) and `rotation_check` (`review_time`), and the day and time +of `suggestions_weekly`, `plan_draft` and `shopping_chat_autosend`, are +household settings in `household_settings` (`dinner_time` is also stored +there); the table shows their defaults. `week_rollover` and the `ai_runs` +purge are fixed and not configurable. | Schedule | When | Does | | --- | --- | --- | @@ -163,8 +168,8 @@ Times are in the household timezone; the defaults below are set in | `shopping_chat_autosend` | Saturday 08:00 | Sends the Supply run to Google Chat. | | `day_of_reminder` | Daily 16:30 | Tonight's dinner reminder. | | `rotation_check` | Daily 09:00 | Asks "Keep it in rotation?" for tried dinners. | -| `week_rollover` | Monday 00:05 | Freezes last week's Supply run into the Supply archive. | -| `ai_runs` purge | Daily | Deletes `ai_runs` rows past 30-day retention. | +| `week_rollover` | Monday 00:05 (fixed) | Freezes last week's Supply run into the Supply archive. | +| `ai_runs` purge | Daily (fixed) | Deletes `ai_runs` rows past 30-day retention. | Runs are recorded in `schedule_runs` and executed through `jobs`. -- 2.55.0 From 60f977d50c5b15303804b7acf9e7e0ceae05800e Mon Sep 17 00:00:00 2001 From: dev_agent Date: Sat, 3 Oct 2026 00:24:07 -0400 Subject: [PATCH 4/4] Restrict dev secret permissions and state exact Node.js ranges in README The quick start now sets the dev data directory to 700 and the secret key file to 600, so other local users cannot read the key. The prerequisites state the exact supported Node.js ranges from the lockfile's jsdom engine (^22.22.2 || ^24.15.0 || >=26.0.0) instead of "22.22.2 or newer". Co-Authored-By: Claude Opus 5.5 --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6c5c3ca..c764516 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,18 @@ single Go service with the React PWA embedded in the binary. ## Quick start -Requirements: Go 1.25 and Node.js 22.22.2 or newer (or 24.15 or newer). -Vite and Vitest need at least 22.12; jsdom in the committed lockfile needs -22.22.2 on the 22 line. +Requirements: Go 1.25 and Node.js `^22.22.2 || ^24.15.0 || >=26.0.0` — that +is, 22.22.2 or later on the 22 line, 24.15.0 or later on the 24 line, or 26.0.0 +or later. Node.js 23, 24.0–24.14 and 25 are not supported: this is the +`engines` range of jsdom in the committed lockfile, the strictest of all the +web dependencies. ```sh (cd web && npm ci) mkdir -p ~/.mealquest-dev # outside the repository: it holds a secret +chmod 700 ~/.mealquest-dev head -c 32 /dev/urandom | base64 > ~/.mealquest-dev/secret_key +chmod 600 ~/.mealquest-dev/secret_key export MEALQUEST_HTTP_ADDR=:8080 export MEALQUEST_PUBLIC_URL=http://localhost:5173 export MEALQUEST_DATA_DIR="$HOME/.mealquest-dev" -- 2.55.0