Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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