Skip to content
Merged
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
101 changes: 101 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# AGENTS.md — settimes.ca

Instructions for AI coding agents (Claude Code, OpenCode, Codex, Cursor, …) working in this repo.
Format: [agents.md](https://agents.md). Humans: start with `README.md`; deep Claude-specific
workflow and the full invariant catalogue live in `CLAUDE.md` — read it before non-trivial work.

## What this is

settimes.ca — a **lean, highly performant** multi-venue live-music event platform for Waterloo
Region, ON. Fan-facing (schedule builder, band profiles, archives) and admin tooling are equal
priority. Flagship event: **Long Weekend Band Crawl Vol. 17 — August 2, 2026**.

Machine-readable canon (venues, events, constants, doctrines): **`ground-truth.json`**.
Read it before asserting facts about events, venues, or dates — do not re-derive or guess.

## Stack

React 19 + Vite + Tailwind 4 (`frontend/`) · Cloudflare Pages Functions (`functions/`) ·
Cloudflare D1 / SQLite (`migrations/`) · R2 for images · Vitest + Playwright · GitHub Actions.

## Setup & commands

Use the Makefile — it exists so every agent runs the same gates with real exit codes:

- `make install` — install root + frontend deps
- `make gate` — **the full pre-commit gate** (format → lint → tests → build, both stacks). Run before every commit; do not commit if it fails.
- `make test` / `make test-backend` / `make test-frontend`
- `make build` — frontend production build
- `make e2e` — full local E2E: builds, seeds an isolated D1, serves wrangler on :8788, runs Playwright, cleans up
- `make schema-check` — after touching `migrations/`: regenerates `setup-complete.sql` and checks drift (its schema section is generated — never hand-edit)
- `make validate-openapi` — if `docs/api-spec.yaml` changed

Never pipe a gate command through `tail`/`grep` inside a `&&` chain — the pipe's exit code
masks failures (this shipped a red lint to CI once). `make` propagates failures natively.

## Top invariants (full catalogue in CLAUDE.md — these are the prod-breakers)

- **After-midnight sets** (start before 06:00) belong to the previous evening. Threshold is
`AFTER_MIDNIGHT_THRESHOLD_HOUR = 6` in `frontend/src/utils/festivalDays.js`. Never remove,
lower, or re-encode it; any time-based sort/filter must apply the offset or use `prepareBands()`.
- **Server-side "today" is Toronto-local**, never UTC: `eventLocalToday()` /
`eventLocalClock()` from `functions/utils/eventDay.js`. `toISOString().slice(0,10)` flips
to tomorrow at 8 PM Eastern — a recurring bug class.
- **SQLite datetimes use a space separator** (`YYYY-MM-DD HH:MM:SS`). A `T` breaks string
comparison against `datetime('now')` silently. Exception: `lucia_sessions.expires_at` is
INTEGER Unix seconds — compare with `unixepoch()`.
- **D1 has no BEGIN/COMMIT** — use `DB.batch([...])` (atomic) or compensating deletes.
- **PBKDF2, never bcrypt** (no native binaries on Workers). MFA TOTP is hand-rolled on
Web Crypto — do not reintroduce otplib or pure-JS crypto.
- **Theme tokens on public pages** — `text-text-*`, `bg-surface*`, `border-border`; never
hardcoded `text-white`/`bg-white` (invisible on light themes). Admin (`frontend/src/admin/`)
is dark-pinned; hardcoded white there is intentional.
- **Two CSPs**: `frontend/public/_headers` governs the document (Turnstile, inline scripts —
the theme-flash script is hash-allowlisted; editing it requires regenerating the hash);
`functions/_middleware.js` governs API responses only.
- **Any list query with a LIMIT must bound the entity it lists** — GROUP BY the entity id or
paginate with a hasMore signal. A LIMIT on join-multiplied rows silently drops whole
entities (this once hid 47 of 218 bands from the admin roster).

## Data doctrines (owner-set, non-negotiable)

- **Lean and highly performant** is a design principle, not a preference. No media-heavy
features, no referral-junk URLs, weigh every feature against page weight first.
- **Clean links**: strip tracking params (`si`, `dlsi`, `nd`, `utm_*`, `from=`) before
storing any URL.
- **No AI-written artist prose.** Bios are entered verbatim from artist/organizer-supplied
text only. Structured facts (genre, origin, links) may be researched, but only to the
highest confidence — the source must unambiguously identify the specific band; a missing
field beats a wrong attribution.
- **Band follows are double opt-in** (`verified = 1` gates announcement email). Never revert
to auto-verify — it reopens an email-bombing vector.

## PR rules

- Branch from `main`; **rebase on `origin/main` before every push**, not just at PR open.
- Use the PR template (`.github/pull_request_template.md`); fill every section; `Closes #N`
on its own line in the body. One type label + one priority label at open time.
- **Nothing merges unless ALL CI checks are green** — required and non-required alike.
Never arm `gh pr merge --auto` while any check is still running.
- Track remaining/deferred work as GitHub issues, referenced from PRs — not chat threads.
- After addressing review comments: reply on the thread AND resolve it (rulesets block
merge on unresolved threads).
- When a change resolves a documented invariant/workaround, update `CLAUDE.md` (and this
file if the invariant is listed here) in the same PR.

## Code style

- Prettier + ESLint are law; `make gate` enforces both. Frontend: single quotes, no semis
(see `frontend/.prettierrc`); backend: double quotes, semis (root `.prettierrc`).
- Comments state constraints the code can't show — never narrate the next line or justify
a change to a reviewer. Reference issues (`#NNN`) when documenting a bug-class fix.
- Reference code locations as `file_path:line` in discussion.

## Testing notes

- Backend unit tests run from repo root (better-sqlite3-backed; may DLOPEN-fail on some
Apple Silicon setups — CI covers it, note it and move on).
- E2E requires a live wrangler server and env credentials (`ADMIN_EMAIL`/`ADMIN_PASSWORD`
are required for ANY Playwright invocation, including `--list`). `make e2e` handles this.
- E2E spec files are currently outside prettier/eslint scope (#622) — match their existing
single-quote style; don't reformat them piecemeal.
84 changes: 84 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# settimes.ca — canonical task runner for humans and agents.
# make propagates real exit codes; never wrap these in `cmd | tail` chains.
# E2E recipe mirrors .github/actions/e2e-env/action.yml.

SHELL := /bin/sh
WRANGLER := ./frontend/node_modules/.bin/wrangler
E2E_STATE := .wrangler/e2e-state
E2E_PID := .wrangler/e2e-state/wrangler.pid
E2E_ADMIN_EMAIL ?= e2e-admin@test.local
E2E_ADMIN_PASSWORD ?= e2e-test-password-Xk9

.PHONY: help install build dev format format-check lint test test-backend test-frontend \
gate validate-openapi schema-check e2e e2e-setup e2e-serve e2e-run e2e-clean

help: ## List targets
@grep -E '^[a-zA-Z0-9_-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}'

install: ## Install root + frontend dependencies
npm ci
cd frontend && npm ci

build: ## Frontend production build (also what E2E serves)
npm run build --prefix frontend

dev: ## Serve the built app + functions on :8788 against local D1 (foreground)
$(WRANGLER) pages dev frontend/dist --port 8788 --persist-to .wrangler/state

format: ## Prettier --write, both stacks
npm run format
cd frontend && npx prettier --write "src/**/*.{js,jsx,json,css}"

format-check: ## Prettier --check, both stacks (what CI runs)
npm run format:check
cd frontend && npm run format:check

lint: ## ESLint, both stacks
npm run lint
cd frontend && npm run lint

test-backend: ## Backend unit tests (better-sqlite3; may DLOPEN-fail on some Apple Silicon — CI covers it)
npm test

test-frontend: ## Frontend unit tests
cd frontend && npm test -- --run

test: test-backend test-frontend ## All unit tests

gate: format format-check lint test build ## FULL pre-commit gate — run before every commit

validate-openapi: ## Required when docs/api-spec.yaml changed
npm run validate:openapi

schema-check: ## Required after touching migrations/ — regenerate + drift-check setup-complete.sql
node scripts/regenerate-setup-complete.mjs
node scripts/check-schema-drift.mjs

e2e-setup: build ## Init + seed an isolated local D1 for E2E (safe to re-run)
rm -rf $(E2E_STATE)
$(WRANGLER) d1 execute settimes-production-db --local --persist-to $(E2E_STATE) --file=database/setup-complete.sql
$(WRANGLER) d1 execute settimes-production-db --local --persist-to $(E2E_STATE) --file=database/seed-test-data.sql
SEED_SQL=$$(node scripts/seed-e2e-admin.mjs --email "$(E2E_ADMIN_EMAIL)" --password "$(E2E_ADMIN_PASSWORD)"); \
$(WRANGLER) d1 execute settimes-production-db --local --persist-to $(E2E_STATE) --command="$$SEED_SQL"

e2e-serve: ## Start wrangler for E2E in the background (writes pidfile)
$(WRANGLER) pages dev frontend/dist --port 8788 --persist-to $(E2E_STATE) > $(E2E_STATE)/wrangler.log 2>&1 & \
echo $$! > $(E2E_PID); \
i=0; until curl -s -o /dev/null http://localhost:8788/ || [ $$i -ge 45 ]; do i=$$((i+1)); sleep 2; done; \
curl -s -o /dev/null http://localhost:8788/ || { echo "wrangler failed to start; log:"; tail -20 $(E2E_STATE)/wrangler.log; exit 1; }

e2e-run: ## Run Playwright (server must be up; credentials required even for --list)
ADMIN_EMAIL="$(E2E_ADMIN_EMAIL)" ADMIN_PASSWORD="$(E2E_ADMIN_PASSWORD)" npx playwright test $(SPEC) --reporter=list

e2e-clean: ## Kill the E2E server by pidfile and remove isolated state
-[ -f $(E2E_PID) ] && kill $$(cat $(E2E_PID)) 2>/dev/null || true
rm -rf $(E2E_STATE)

e2e: e2e-setup e2e-serve ## Full local E2E: setup, serve, run, always clean up
ADMIN_EMAIL="$(E2E_ADMIN_EMAIL)" ADMIN_PASSWORD="$(E2E_ADMIN_PASSWORD)" npx playwright test $(SPEC) --reporter=list; \
status=$$?; \
$(MAKE) e2e-clean; \
exit $$status

probe-links: ## Probe canonical bandcamp URLs for linkless bands (FILE=names.txt, newline-separated)
node scripts/probe-band-links.mjs --file $(FILE)
87 changes: 87 additions & 0 deletions ground-truth.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
"_comment": "Machine-readable project canon for agents and humans. Slow-moving facts only — live data (lineups, band profiles, counts) belongs to the database, not this file. Update via PR when canon changes; bump verified_at when a human or agent re-verifies against production.",
"verified_at": "2026-07-17",
"project": {
"name": "SetTimes",
"domain": "https://settimes.ca",
"repo": "BreakableHoodie/settimesdotca",
"region": "Waterloo Region, Ontario, Canada (not Ottawa)",
"mission": "The best multi-venue / multi-artist event platform for Waterloo Region — fan-facing and admin tooling are equal priority",
"design_principles": [
"lean and highly performant — media weight is a cost the site refuses",
"clean links — no referral/tracking params ever stored",
"no AI-written artist prose — bios are artist/organizer-supplied verbatim",
"researched facts at highest confidence only — a missing field beats a wrong attribution",
"SEO is a priority (structured data, titles, local discovery)"
],
"roadmap": "docs/ROADMAP.md"
},
"target_event": {
"name": "Long Weekend Band Crawl - Vol. 17",
"slug": "lwbc17",
"db_id": 21,
"date": "2026-08-02",
"doors": "18:30",
"show": "18:45",
"ages": "19+",
"band_count": 22,
"venue_street": "King St N, Waterloo"
},
"venues_vol17": [
{ "db_id": 16, "name": "Blue Room (Inside Revive Karaoke)", "address": "28 King St N, Waterloo" },
{ "db_id": 8, "name": "Princess Cafe", "address": "46 King St N, Waterloo, ON, N2J 2W8" },
{ "db_id": 6, "name": "Prohibition Warehouse", "address": "56 King St N, Waterloo, ON, N2J 2X1" },
{ "db_id": 7, "name": "Revive Karaoke", "address": "28 King St N, Waterloo, ON, N2J 2W7" },
{ "db_id": 4, "name": "Room 47", "address": "47 King St N, Waterloo, ON, N2J 2W9" },
{ "db_id": 15, "name": "Roost", "address": "85 King St N, Waterloo, ON" }
],
"venue_notes": {
"blue_room": "Blue Room is inside Revive Karaoke — both are distinct venues",
"jane_bond": "Jane Bond (db_id 17) is Vol-16 only",
"historical_2022": "Vol 1/2 venues (Dive Bar 85 King N, Revive Game Bar 90 King N, Pin Up Arcade Bar 8-247 King N) have no venue rows — they live in events.venue_info JSON; 85 King N is now Roost, Revive later moved to 28"
},
"published_events": [
{ "db_id": 35, "slug": "blr3", "name": "Bad Livin' Roadshow 3", "date": "2026-07-10", "end_date": "2026-07-12" },
{ "db_id": 21, "slug": "lwbc17", "name": "Long Weekend Band Crawl - Vol. 17", "date": "2026-08-02" },
{ "db_id": 36, "slug": "buddiesfest2", "name": "Buddies Fest 2", "date": "2026-08-07", "end_date": "2026-08-09" }
],
"archived_events": [
{ "db_id": 33, "slug": "lwbc01", "date": "2022-05-22" },
{ "db_id": 34, "slug": "lwbc02", "date": "2022-07-31" },
{ "db_id": 22, "slug": "lwbc03", "date": "2022-10-09" },
{ "db_id": 23, "slug": "lwbc04", "date": "2023-05-21" },
{ "db_id": 24, "slug": "lwbc05", "date": "2023-08-06" },
{ "db_id": 25, "slug": "lwbc06", "date": "2023-10-08" },
{ "db_id": 26, "slug": "lwbc07", "date": "2024-02-18" },
{ "db_id": 27, "slug": "lwbc08", "date": "2024-05-19" },
{ "db_id": 28, "slug": "lwbc09", "date": "2024-08-04" },
{ "db_id": 29, "slug": "lwbc10", "date": "2024-10-13" },
{ "db_id": 30, "slug": "lwbc11", "date": "2025-02-16" },
{ "db_id": 31, "slug": "lwbc12", "date": "2025-05-18" },
{ "db_id": 32, "slug": "lwbc13", "date": "2025-08-03" },
{ "db_id": 19, "slug": "lwbc14", "date": "2025-10-12" },
{ "db_id": 1, "slug": "lwbc15", "date": "2026-02-15" },
{ "db_id": 20, "slug": "lwbc16", "date": "2026-05-17" }
],
"constants": {
"timezone": "America/Toronto",
"after_midnight_threshold_hour": 6,
"after_midnight_rule": "sets starting before 06:00 belong to the previous evening; canonical constant in frontend/src/utils/festivalDays.js",
"sqlite_datetime_format": "YYYY-MM-DD HH:MM:SS (space separator, never ISO T)",
"lucia_sessions_expires_at": "INTEGER Unix seconds — the one exception; compare with unixepoch()",
"social_link_keys": ["website", "instagram", "bandcamp", "facebook", "youtube", "spotify", "apple_music", "linktree"]
},
"infrastructure": {
"hosting": "Cloudflare Pages + Pages Functions",
"database": { "product": "Cloudflare D1", "name": "settimes-production-db", "id": "c6c8b4a6-f951-47c1-a787-aab0a61ca09b" },
"images": { "product": "Cloudflare R2", "bucket": "settimes-band-photos", "binding": "BAND_PHOTOS" },
"migrations": "auto-apply on merge to main via cloudflare-pages.yml — never apply manually",
"e2e_port": 8788
},
"process": {
"merge_rule": "nothing merges unless ALL CI checks are green (required and non-required)",
"rebase_rule": "rebase on origin/main before every push",
"issue_tracking": "remaining/deferred work becomes a GitHub issue referenced from PRs",
"pre_commit_gate": "make gate"
}
}
Loading
Loading