Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .claude/skills/api-contract/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,9 +262,9 @@ Other implementation details worth knowing:
- **`STORAGE_UNCONFIGURED` 503**: if `env.RATINGS_DB` is unbound,
`handleReviews` returns `503 UNAVAILABLE`/`STORAGE_UNCONFIGURED` before
touching D1 (`reviews.ts:158-166`). This is also the state of the *real*
production database today: `wrangler.toml:26` has a placeholder
`database_id = "00000000-0000-0000-0000-000000000000"` — D1 has not been
provisioned. Tests and `wrangler dev` use a simulated local D1 via
production worker today: D1 has not been provisioned, so the
`[[d1_databases]]` block in `wrangler.toml` is commented out and the binding
is genuinely absent. Tests and `wrangler dev` use a simulated local D1 via
`@cloudflare/vitest-pool-workers`, so the whole test suite runs green
without a real database. Provisioning is out of scope for this skill — see
`run-and-operate` for the `wrangler d1 create` runbook, or
Expand Down
4 changes: 2 additions & 2 deletions .claude/skills/architecture-contract/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,8 @@ found" — they're tracked.
the class without checking every import, and never rename the route.
- **The `/v1alpha` catalogue API is contract-only** (issue #432/PR #433):
proto + generated OpenAPI + a read-only worker endpoint exist, but there
is no production server backing MyFestival sync yet — D1 has a
placeholder `database_id` (`cloudflare-worker/wrangler.toml:26`). See
is no production server backing MyFestival sync yet — D1 is unprovisioned
and its binding is commented out in `cloudflare-worker/wrangler.toml`. See
`run-and-operate` for the provisioning gap and `api-contract` for the
proto surface itself.

Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/build-and-env/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ train.
| `TEST_LOG` / `ANALYZE_LOG` | `mktemp /tmp/test-XXXXXX.log` / `/tmp/analyze-XXXXXX.log` per run (`mise-tasks/test.sh:6`, `mise-tasks/analyze.sh:6`) | the `test`/`analyze` file-tasks themselves (tee output, preserve exit code via `${PIPESTATUS[0]}`) | Same on both; override to a stable path when you need to grep the same log across multiple invocations without re-running |
| `ENVIRONMENT` (wrangler `[vars]`) | `"production"` (`cloudflare-worker/wrangler.toml:6-7`) | Worker code (`worker.js`/`shared.ts`) for any environment-conditional behavior | Only one value committed — there's no separate staging `[vars]` block in `wrangler.toml`; staging behavior is driven by **origin-based** CORS/bucket logic instead (below), not this var |
| `RATINGS_BUCKET` (env override) | unset — falls back to `resolveBucket(origin, env)` (`cloudflare-worker/shared.ts:23-27`): `origin === "https://cambeerfestival.app"` → `"prod"`, else `"test"` | `shared.ts` bucket resolution for the D1 `reviews` table's composite key | Not set anywhere in committed config today; it's an escape hatch for forcing a bucket regardless of request origin. Do not set it in production without understanding it silently overrides the origin check |
| `RATINGS_DB` (D1 binding, not an env var) | `[[d1_databases]] binding = "RATINGS_DB"`, `database_id = "00000000-0000-0000-0000-000000000000"` **placeholder** (`wrangler.toml:20-25`) | `reviews.ts` via `env.RATINGS_DB`; missing/misconfigured → worker returns 503 `STORAGE_UNCONFIGURED` | Tests/local dev use wrangler's simulated local D1 (id ignored); a real deploy needs `wrangler d1 create cbf-myfestival` + paste the real id + `wrangler d1 migrations apply` — see `run-and-operate` for the provisioning runbook |
| `RATINGS_DB` (D1 binding, not an env var) | `[[d1_databases]]` block **commented out** in `wrangler.toml` (a placeholder `database_id` broke every deploy — error 10181); the test-only binding lives in `vitest.config.js` (`miniflare.d1Databases`) | `reviews.ts` via `env.RATINGS_DB`; missing/misconfigured → worker returns 503 `STORAGE_UNCONFIGURED` | Tests/local dev use a simulated local D1 (id ignored; `--dry-run` doesn't check resource existence either, so `test/wrangler-config.test.js` guards the placeholder case); enabling it needs `wrangler d1 create cbf-myfestival` + uncomment the block with the real id + `wrangler d1 migrations apply --remote` — see `run-and-operate` for the provisioning runbook |
| `BASE_URL` (Playwright) | `"http://127.0.0.1:8080"` (`playwright.config.ts:34`) | `test-e2e/*.spec.ts` via `page.goto`/`baseURL` | Local/CI default targets a locally-served build; CI's `smoke-test-preview` job sets `BASE_URL=<Cloudflare Pages preview URL>` to run `csp-smoke.spec.ts` against a real deployed CSP policy — the only place that check is meaningful (`web/_headers` CSP isn't exercised any other way) |

## 7. Adding a tool or task correctly
Expand Down
3 changes: 2 additions & 1 deletion .claude/skills/debugging-playbook/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ most battles in this repo have already been fought once.

| Symptom | Likely cause | Discriminating experiment | Fix pattern | History |
|---|---|---|---|---|
| `/v1alpha/...` returns 503 with reason `STORAGE_UNCONFIGURED` | `env.RATINGS_DB` D1 binding missing — `cloudflare-worker/wrangler.toml` ships a placeholder `database_id = "00000000-..."` | `curl -s https://data.cambeerfestival.app/v1alpha/festivals/x/drinks/y/review -H 'X-Device-Id: t'` and check the error body (`reviews.ts:158-166`) | Provision D1: `wrangler d1 create cbf-myfestival` → paste real id into wrangler.toml → `wrangler d1 migrations apply cbf-myfestival --remote` (`--remote` migrates the real DB, not the local sim); token needs D1:Edit. Full runbook: skill `run-and-operate` | v1alpha arc (PR #426); deploy still pending |
| `/v1alpha/...` returns 503 with reason `STORAGE_UNCONFIGURED` | Expected, not a bug: `env.RATINGS_DB` is absent because the `[[d1_databases]]` block in `cloudflare-worker/wrangler.toml` is **deliberately commented out** until a database is provisioned | `curl -s https://data.cambeerfestival.app/v1alpha/festivals/x/drinks/y/review -H 'X-Device-Id: t'` and check the error body (`reviews.ts:158-166`) | Provision D1: `wrangler d1 create cbf-myfestival` → uncomment the block and paste the real id → `wrangler d1 migrations apply cbf-myfestival --remote` (`--remote` migrates the real DB, not the local sim); token needs D1:Edit. Full runbook: skill `run-and-operate` | v1alpha arc (PR #426); binding disabled to unblock deploys |
| `wrangler deploy` fails with `D1 binding 'X' references database '…' which was not found [code: 10181]`, while `npm test` is green | A binding names a resource that doesn't exist in the account. Cloudflare validates bindings at upload; the vitest pool and `wrangler dev` use simulated local resources and never check the id, so tests cannot catch it | **Not** `--dry-run` — it exits 0 on a dangling binding (verified, wrangler 4.98.0). Read the deploy log for the resource id, then confirm the resource exists in the account (dashboard, or `wrangler d1 list`). `cloudflare-worker/test/wrangler-config.test.js` catches the placeholder case offline | Either provision the resource, or remove/comment out the binding so the rest of the worker can deploy (the code already degrades to 503 `STORAGE_UNCONFIGURED`). Never leave a placeholder id in place: it blocks *every* deploy, proxy included | Broke all worker deploys 2026-06-13 → 2026-08-10; the live worker silently stayed on pre-#426 code for ~2 months |
| Browser: CORS error calling the worker; curl works fine | Origin not in the allow-list — the worker returns NO CORS headers for unknown origins (silent reject) | Compare your page's `Origin` against `ALLOWED_ORIGINS` (`cloudflare-worker/worker.js:29-37`) and the wildcard suffixes in `getCorsHeaders` (worker.js:279-329): `*.cambeerfestival.pages.dev`, `*.staging-cambeerfestival.pages.dev`, `*.trycloudflare.com` | Add the origin to the allow-list — but `cloudflare-worker/` is on the Do-Not-Modify list; needs explicit maintainer request + PR | Allow-list design; ops trap (trycloudflare wildcard live in prod) |
| Worker returns 502 | Upstream `data.cambridgebeerfestival.com` fetch failed — the worker proxies everything not matched by its own routes | `curl -si https://data.cambridgebeerfestival.com/<same-path>` — is upstream itself down/erroring? | Nothing to fix app-side; upstream CAMRA feeds are untouchable (unwritten rule). The app's SWR cache is the mitigation — verify cached data still renders with the refresh notice | worker.js:102-137 |

Expand Down
22 changes: 13 additions & 9 deletions .claude/skills/my-festival-campaign/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,10 +330,11 @@ account. `cloudflare-worker/` is on the **Do-Not-Modify list** — it needs an
**explicit maintainer request**, and the **festival-freeze window** must be clear
(`change-control`). Do not provision on your own initiative.

**Provision** (one-time — `wrangler.toml:26` `database_id` is the placeholder
`00000000-...`; binding `RATINGS_DB`, db name `cbf-myfestival`). Follow the exact
command sequence in skill `run-and-operate` §4 — `wrangler d1 create
cbf-myfestival`, paste the returned id into `wrangler.toml`, then
**Provision** (one-time — the `[[d1_databases]]` block in `wrangler.toml` is
commented out, so there is no binding at all today; binding name `RATINGS_DB`,
db name `cbf-myfestival`). Follow the exact command sequence in skill
`run-and-operate` §4 — `wrangler d1 create cbf-myfestival`, uncomment the block
and paste the returned id into `wrangler.toml`, then
`wrangler d1 migrations apply cbf-myfestival --remote`. The **`--remote` flag is
mandatory**: without it `wrangler` migrates only the local simulated D1 and the
real production database is left unmigrated while the command reports success.
Expand All @@ -359,8 +360,11 @@ curl -sS -X PATCH \
curl -sS https://data.cambeerfestival.app/v1alpha/festivals/cbf2025/reviewSummaries/beer-1
# expect: {"name":"...","ratingCount":>=1,"averageRating":...,"recommendRate":...}
```
- **See 503 after provisioning?** The binding didn't resolve — the `database_id`
paste is wrong or the deploy predates the migration. Re-apply, redeploy.
- **See 503 after provisioning?** The binding didn't resolve — the block is
still commented out, the `database_id` paste is wrong, or the deploy predates
the migration. Note `--dry-run` lists the binding
even when the database does not exist, so it proves nothing here — check the
real deploy log. Re-apply the migration and redeploy.
- **See `test` data leaking to `prod`?** Bucket resolution keys on `Origin`; only
`https://cambeerfestival.app` → `prod`, everything else → `test` (or the
`RATINGS_BUCKET` var). That's correct isolation, not a bug.
Expand Down Expand Up @@ -573,7 +577,7 @@ Written 2026-07-02. Verified against the working tree at that date:
- Worker: `Review` API (`/review`, `/reviews`, `/reviewSummaries`) implemented in
`cloudflare-worker/reviews.ts`; 503 `STORAGE_UNCONFIGURED` guard on
`env.RATINGS_DB`; `X-Device-Id` identity; bucket resolution in `shared.ts`;
placeholder `database_id` in `wrangler.toml:26`; migration
D1 binding commented out in `wrangler.toml` (unprovisioned); migration
`0001_create_reviews_table.sql`; curl set + provisioning in
`cloudflare-worker/README.md`. The `DrinkEntry`/`drinkEntries` proto endpoints
are **not** implemented (grep over `cloudflare-worker/*.ts`).
Expand All @@ -598,8 +602,8 @@ rg -n 'favorites' lib/router.dart
# Current goldens on disk
find test -path '*goldens*' -name '*.png'

# D1 still a placeholder?
rg -n 'database_id' cloudflare-worker/wrangler.toml
# D1 still unprovisioned? (expect: [[d1_databases]] commented out)
rg -n 'd1_databases' -A5 cloudflare-worker/wrangler.toml

# Baseline
./bin/mise run check # add MISE_ENV=claude-code-web on a 403-sandboxed box
Expand Down
6 changes: 3 additions & 3 deletions .claude/skills/reference/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ are at different maturity levels and it's easy to conflate them.
| Surface | Status | Where implemented | What it does |
|---|---|---|---|
| **Static beverage feeds** | Live in production, untouchable upstream data | `data.cambridgebeerfestival.com` (CAMRA's own static files) proxied through `cloudflare-worker/worker.js` at `data.cambeerfestival.app/{festivalId}/{category}.json`; also `/festivals.json` (embedded registry, `no-cache, must-revalidate`) and `/{festivalId}/available_beverage_types.json` (scrapes an upstream Apache directory listing, 1h cache) | The entire catalogue the app renders — Producers→Products, per festival per category (§3). This is what `BeerApiService`/`FestivalService` fetch. |
| **v1alpha Review API** | Live code, deployed worker, **D1 database not yet provisioned** (`wrangler.toml` ships a placeholder `database_id`) | `cloudflare-worker/reviews.ts` + `shared.ts`, routed at `/v1alpha/...` inside the same worker; D1 table `reviews` (`cloudflare-worker/migrations/0001_create_reviews_table.sql`) | Anonymous star-rating + "would recommend" reviews, keyed by `X-Device-Id` header (not signed-in identity yet — `user_id` column reserved for a future sign-in upgrade). GET/PATCH/DELETE one review, list caller's reviews, get/list aggregate summaries per drink. Any unmatched `/v1alpha/*` path 404s — it is never proxied upstream. |
| **v1alpha Review API** | Live code, deployed worker, **D1 database not yet provisioned**, so `/v1alpha` answers 503 `STORAGE_UNCONFIGURED` in production (the `[[d1_databases]]` block in `wrangler.toml` is commented out) | `cloudflare-worker/reviews.ts` + `shared.ts`, routed at `/v1alpha/...` inside the same worker; D1 table `reviews` (`cloudflare-worker/migrations/0001_create_reviews_table.sql`) | Anonymous star-rating + "would recommend" reviews, keyed by `X-Device-Id` header (not signed-in identity yet — `user_id` column reserved for a future sign-in upgrade). GET/PATCH/DELETE one review, list caller's reviews, get/list aggregate summaries per drink. Any unmatched `/v1alpha/*` path 404s — it is never proxied upstream. |
| **proto CatalogService / MyFestivalService** | **Paper contract only** — defines the intended future v1alpha REST surface via `google.api.http` annotations, generates OpenAPI, but has **no server implementation** in the worker (`/v1alpha` routing only wires up `handleReviews`, i.e. the Review API above; `CatalogService`'s `ListFestivals`/`GetFestival`/`ListDrinks` RPCs have no handler) | `proto/cambeerfestival/festival/v1alpha/{catalog_service,my_festival_service,drink_entry,drink_summary,festival,drink,producer}.proto` → `buf generate` → `docs/code/api/openapi/openapi.yaml` (Redoc-published by `api-docs.yml`) | Design-time contract for where the API is headed: a typed, resource-oriented catalogue API (AIP-compliant) and a richer `DrinkEntry`/`MyFestivalService` (favourite/rating/note/pour-count sync with soft-delete tombstones and etag concurrency) intended to eventually replace/extend the anonymous Review API. Full workflow and AIP facts: skill `api-contract`. |

The **static feeds** and the **v1alpha Review API** are the two surfaces that
Expand Down Expand Up @@ -445,8 +445,8 @@ cat docs/code/api/festival-registry-schema.json
# API surface: is CatalogService actually routed in the worker? (expect: no handler, only handleReviews)
grep -n "v1alpha\|handleReviews\|CatalogService" cloudflare-worker/worker.js

# D1 provisioning state (expect: placeholder database_id until provisioned)
grep -n "database_id" cloudflare-worker/wrangler.toml
# D1 provisioning state (expect: [[d1_databases]] commented out until provisioned)
grep -n "d1_databases" -A5 cloudflare-worker/wrangler.toml

# Proto promotion path / AIP framing
sed -n '1,40p' proto/buf.yaml
Expand Down
61 changes: 42 additions & 19 deletions .claude/skills/run-and-operate/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,36 +196,56 @@ enrollment; every release after that is fully automated.
> changes near/during the live festival; see skill `change-control`). Do not
> run the commands below on your own judgement.

`cloudflare-worker/wrangler.toml` has a **placeholder** database id:

```toml
[[d1_databases]]
binding = "RATINGS_DB"
database_name = "cbf-myfestival"
database_id = "00000000-0000-0000-0000-000000000000"
migrations_dir = "migrations"
```
`cloudflare-worker/wrangler.toml` has **no D1 binding** — the
`[[d1_databases]]` block is commented out, so the deployed worker runs without
`env.RATINGS_DB` and every `/v1alpha` route answers 503 `STORAGE_UNCONFIGURED`
(`reviews.ts:158`).

> **Why commented out, not a placeholder id.** It used to carry
> `database_id = "00000000-0000-0000-0000-000000000000"`. Cloudflare validates
> bindings when the script is uploaded, so that placeholder made **every**
> `wrangler deploy` fail with error 10181 — taking the CORS proxy down with it,
> even though the proxy has nothing to do with D1. The worker was undeployable
> from 2026-06-13 (PR #426, which added the binding) until 2026-08-10, and the
> live worker silently served pre-#426 code that whole time. Do not restore a
> placeholder id "so the config documents itself" — a binding is either real or
> absent.

Tests and `wrangler dev` use a **simulated local D1** (via
`@cloudflare/vitest-pool-workers`) and ignore this id entirely — the whole
worker test suite (`npm test` in `cloudflare-worker/`) runs green with the
placeholder in place, so a passing `test:worker` run tells you nothing about
whether the real database exists.

To provision the real thing (do this before any manual `wrangler deploy` that
needs to serve real `/v1alpha` review traffic):
`@cloudflare/vitest-pool-workers`) and never check the id — a green
`test:worker` run tells you nothing about whether the real database exists, or
even whether the worker can deploy at all. The binding for tests is declared in
`cloudflare-worker/vitest.config.js` (`miniflare.d1Databases`), deliberately
decoupled from `wrangler.toml` so the test suite is unaffected by the
production binding's presence.

**`wrangler deploy --dry-run` does NOT catch a dangling binding** — verified on
wrangler 4.98.0: with the all-zeroes `database_id` restored it printed
`env.RATINGS_DB (cbf-myfestival)` in the binding table and exited 0. Resource
existence is checked server-side at upload, so CI's `validate-worker` job was
green on the PR that broke deploys, and only the post-merge `deploy-worker` job
on `main` failed. The offline guard is
`cloudflare-worker/test/wrangler-config.test.js` (runs in `test-worker`, needs
no credentials); the only positive proof a binding resolves is a real deploy.

To provision the real thing (needed before `/v1alpha` can serve real review
traffic):

```bash
cd cloudflare-worker

# 1. Create the D1 database in the Cloudflare account
wrangler d1 create cbf-myfestival
# → paste the returned database_id into wrangler.toml's database_id field
# → uncomment the [[d1_databases]] block in wrangler.toml and paste the
# returned id into database_id

# 2. Apply migrations to the REAL (remote) database
wrangler d1 migrations apply cbf-myfestival --remote
# (only one migration exists today: migrations/0001_create_reviews_table.sql —
# single `reviews` table, PK (bucket, festival_id, drink_id, device_id))

# 3. Confirm the binding resolves before pushing
npx wrangler deploy --dry-run # must list env.RATINGS_DB (cbf-myfestival)
```

The `CLOUDFLARE_API_TOKEN` used for this needs **D1:Edit** permission in
Expand Down Expand Up @@ -399,8 +419,11 @@ Re-verification commands (run when a fact here feels stale):
sed -n '1,50p' .github/workflows/release-pr.yml
sed -n '1,80p' .github/workflows/release.yml

# D1 still unprovisioned (placeholder id)?
grep -A2 database_id cloudflare-worker/wrangler.toml
# D1 still unprovisioned (expect: the [[d1_databases]] block is commented out)?
grep -n "d1_databases" -A5 cloudflare-worker/wrangler.toml

# Placeholder bindings still absent? (--dry-run does NOT check this; this does)
(cd cloudflare-worker && npx vitest run test/wrangler-config.test.js)

# Deployment topology still matches reality (not the stale doc)?
grep -n "project-name" .github/workflows/ci.yml .github/workflows/release-web.yml
Expand Down
Loading