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 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user