diff --git a/.claude/rules/deployment.md b/.claude/rules/deployment.md index 4b6742f..040204e 100644 --- a/.claude/rules/deployment.md +++ b/.claude/rules/deployment.md @@ -6,7 +6,7 @@ All deployments follow this sequence. No exceptions. 1. Feature branch passes CI (build + lint + test) 2. PR reviewed and squash-merged into main -3. Web + Worker: Vercel auto-deploys from main (no manual action) +3. Web (including the `/api/cron/push` route) Vercel auto-deploys from main — no manual action 4. DB: migrations applied via Supabase CLI before deploying code that depends on them ## Deploy Checklist (for every production release) @@ -16,7 +16,7 @@ All deployments follow this sequence. No exceptions. - PR squash-merged into `main` (never direct push) - **DB migrations first**: apply via `supabase db push` BEFORE deploying app code - **Web**: Verify Vercel deployment succeeded (check deployment URL) -- **Post-deploy**: Verify `/api/health` returns OK; check admin dashboard for worker status +- **Post-deploy**: Verify `/api/health` returns OK; check admin dashboard for cron run status (shown as "Worker 狀態" in the UI for historical reasons) ## Deploy Rules @@ -25,9 +25,9 @@ All deployments follow this sequence. No exceptions. - **Never** deploy directly from a feature branch to production - **Rollback**: Vercel instant rollback via dashboard -## Worker Cron +## Cron Entry -Push worker runs as a Vercel serverless function at `/api/cron/push`, triggered hourly by Supabase `pg_cron` + `pg_net`. +The push pipeline runs as a Vercel serverless function at `/api/cron/push`, triggered hourly by Supabase `pg_cron` + `pg_net`. (Previously a standalone `apps/worker/` Node process; merged into the web app in PR #33.) - Auth: `CRON_SECRET` Bearer token (Supabase Vault + Vercel env var) - Catch-up model: `push_hour_utc <= current_hour` recovers missed triggers @@ -38,12 +38,13 @@ Push worker runs as a Vercel serverless function at `/api/cron/push`, triggered | Service | Purpose | Required | |---------|---------|----------| -| Vercel | Web + worker hosting (Next.js) | Yes | +| Vercel | Web + cron route hosting (Next.js) | Yes | | Supabase | PostgreSQL + Auth + RLS + pg_cron | Yes | | GitHub | Repo + CI (Actions) | Yes | | Telegram Bot API | Push notifications | Yes | | LINE Messaging API | Push notifications | Yes | | Resend | Email notifications | Yes | | Cloudflare | DNS, SPF/DKIM/DMARC (for Resend) | Yes | +| Upstash Redis | Cross-instance webhook rate limiter | Optional — in-memory Map fallback per function instance when env vars unset. `UPSTASH_REDIS_REST_URL/TOKEN` or `KV_REST_API_URL/TOKEN` from the Vercel Marketplace integration. | | Sentry | Error tracking | Optional (no-op without `SENTRY_DSN`) | | PostHog | Product analytics | Optional (no-op without `NEXT_PUBLIC_POSTHOG_KEY`) | diff --git a/.claude/rules/git-conventions.md b/.claude/rules/git-conventions.md index 594e3fa..cfc1af1 100644 --- a/.claude/rules/git-conventions.md +++ b/.claude/rules/git-conventions.md @@ -33,7 +33,7 @@ Follows [Conventional Commits 1.0.0](https://www.conventionalcommits.org/) (Goog | Element | Rule | |---------|------| | **type** | Required. One of: `feat`, `fix`, `refactor`, `docs`, `test`, `chore`, `perf`, `ci`, `build`, `style` | -| **scope** | Optional but recommended. One of: `web`, `worker`, `shared`, `db`, `ci`. Omit only for cross-cutting changes | +| **scope** | Optional but recommended. One of: `web`, `shared`, `db`, `ci`. Omit only for cross-cutting changes. (`worker` was retired when the push worker merged into `apps/web/app/api/cron/push` in PR #33.) | | **subject** | Required. Imperative mood. Lowercase. No period. Max 50 chars (hard limit 72) | **Type definitions**: @@ -67,7 +67,7 @@ Closes #42 ``` ``` -fix(worker): prevent duplicate push when worker crashes mid-batch +fix(web): prevent duplicate push when cron route crashes mid-batch stamp_last_push_date() now runs before dispatch instead of after, ensuring at-most-once delivery even on crash. diff --git a/.claude/rules/notifications.md b/.claude/rules/notifications.md index f6b1bc1..0fb6060 100644 --- a/.claude/rules/notifications.md +++ b/.claude/rules/notifications.md @@ -3,7 +3,6 @@ paths: - "apps/web/app/api/telegram/**" - "apps/web/app/api/line/**" - "packages/shared/src/channels/**" - - "apps/worker/src/channels/**" - "apps/web/lib/actions/telegram.ts" - "apps/web/lib/actions/line.ts" - "apps/web/lib/actions/email.ts" @@ -28,4 +27,4 @@ paths: - Resend domain: `caffecode.net` (Cloudflare DNS, verified SPF/DKIM/DMARC) - From: `CaffeCode ` -- Worker renders React Email HTML; admin sends plain text +- Cron route renders React Email HTML via `packages/shared/src/push/channels/email-template.tsx`; admin sends plain text diff --git a/CLAUDE.md b/CLAUDE.md index c41aad5..cf19b38 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,9 +15,11 @@ pnpm monorepo + Turborepo. Single Next.js app + shared library share a Supabase | Component | Location | Runtime | Role | |-----------|----------|---------|------| -| Web | `apps/web/` | Next.js 16 on Vercel | Public pages (SEO), OAuth, dashboard, settings, admin, cron push route | +| Web | `apps/web/` | Next.js 16 on Vercel | Public pages (SEO), OAuth, dashboard, settings, admin, `/api/cron/push` entry | | Shared | `packages/shared/` | TypeScript library | Types, channel senders, problem selection, formatters, push pipeline | +Historically there was also `apps/worker/` running a separate Node process; it was merged into the web app's `/api/cron/push` route in PR #33 and the package deleted. + **Push cron**: `apps/web/app/api/cron/push/route.ts` is the live cron entry. Supabase `pg_cron` + `pg_net` POST hourly with `Authorization: Bearer `. **Pre-curated content model**: All problem content generated offline via admin UI. Zero runtime LLM calls. @@ -30,7 +32,7 @@ Follows Conventional Commits 1.0.0 (Google/Angular style). Full format in `.clau - Branch: `/` - Commit: `(): ` — imperative, lowercase, no period, max 50 chars -- Scopes: `web`, `worker`, `shared`, `db`, `ci` +- Scopes: `web`, `shared`, `db`, `ci` - PR → squash merge → delete branch. Never push to `main`. - Update `CLAUDE.md` and `README.md` before every PR if the change affects them. @@ -59,11 +61,11 @@ Schema in `docs/supabase-schema.sql`. All tables have RLS enabled. | Target | Platform | Deploy method | |--------|----------|---------------| -| Web + Worker | Vercel | `git push origin main` (auto-deploy) | +| Web (includes cron API route) | Vercel | `git push origin main` (auto-deploy) | - DB migrations via Supabase MCP `apply_migration` BEFORE deploying code that depends on them - Never `vercel --prod` — deploy web via git push only -- Worker cron: Supabase `pg_cron` fires `pg_net` HTTP POST to `/api/cron/push` every hour +- Cron entry: Supabase `pg_cron` fires `pg_net` HTTP POST to `/api/cron/push` every hour - Auth: `CRON_SECRET` Bearer token (Supabase Vault + Vercel env var) - Catch-up model: missed cron triggers recovered on the next successful run - Full checklist and cloud services in `.claude/rules/deployment.md` @@ -84,11 +86,13 @@ Schema in `docs/supabase-schema.sql`. All tables have RLS enabled. ## Development Notes -**Tests**: 751 TypeScript vitest (shared 185, web 566) + 57 Playwright E2E + 54 Python. Vitest: `pnpm exec vitest run` per package. E2E: `pnpm exec playwright test` in `apps/web/` (requires dev server running). Python: `cd scripts && python3 -m pytest tests/ -v`. +**Toolchain**: Node 24 LTS, pnpm 10.33.0 (set via `packageManager` — corepack auto-switches). `engines.node >=22.0.0` so contributors on Node 22 can still develop; CI and Vercel use 24. + +**Tests**: 757 TypeScript vitest (shared 186, web 571) + 57 Playwright E2E + 54 Python. Vitest: `pnpm exec vitest run` per package. E2E: `pnpm exec playwright test` in `apps/web/` (requires dev server running). Python: `cd scripts && python3 -m pytest tests/ -v`. **Coverage**: `pnpm test:coverage` runs all packages with `@vitest/coverage-v8`. CI enforces thresholds (shared 95/90/95/95, web 90/85/90/90 for stmts/branch/funcs/lines). Coverage scope: business logic only (`lib/`, `src/`, API routes); excludes components, pages, and infra singletons. -**Next.js 16**: `proxy.ts` (not `middleware.ts`); export must be named `proxy`. +**Next.js 16 + Cache Components**: `cacheComponents: true` enabled. Every runtime data read (`cookies()`, `headers()`, awaited params/searchParams, Supabase auth) sits inside a `` boundary; shared fetches use `'use cache'` + `cacheTag(...)`; admin mutations invalidate via `revalidateTag(..., 'max')`. `proxy.ts` (not `middleware.ts`); export must be named `proxy`. Full pattern in `.claude/rules/web-patterns.md`. **No git worktrees**: `.env.local` not shared across worktrees. Use `git checkout `. diff --git a/README.md b/README.md index b822041..42ae046 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ One Next.js deployment on Vercel hosts both the site and the hourly cron endpoin | Monorepo | pnpm workspaces + Turborepo | | Observability | Sentry (errors), PostHog (analytics), Pino (structured logging) | | Security | CSP headers, Zod validation, webhook HMAC verification, `timingSafeEqual` auth | -| Testing | Vitest (751 TS) + Playwright E2E (57) + pytest (54 Python) | +| Testing | Vitest (757 TS) + Playwright E2E (57) + pytest (54 Python) | | CI/CD | GitHub Actions, Vercel | ## Project Structure @@ -113,8 +113,8 @@ scripts/ ### Prerequisites -- Node.js 22+ -- pnpm 9+ (repo uses `packageManager: "pnpm@9.15.0"`) +- Node.js 22+ (CI and Vercel use Node 24 LTS; `.nvmrc` is pinned to 24) +- pnpm 10+ (repo uses `packageManager: "pnpm@10.33.0"`; enable via `corepack enable`) - A [Supabase](https://supabase.com) project ### Installation @@ -169,8 +169,8 @@ pnpm dev # start web dev server on localhost:3000 pnpm test # Individually -cd packages/shared && pnpm exec vitest run # 185 tests -cd apps/web && pnpm exec vitest run # 566 tests +cd packages/shared && pnpm exec vitest run # 186 tests +cd apps/web && pnpm exec vitest run # 571 tests # E2E tests (requires dev server running) cd apps/web && pnpm exec playwright test # 57 tests diff --git a/docs/staging-setup.md b/docs/staging-setup.md index 5e0f238..ac16823 100644 --- a/docs/staging-setup.md +++ b/docs/staging-setup.md @@ -2,7 +2,7 @@ ## Overview -Staging mirrors production with isolated data. Both components (web + worker cron) deploy to Vercel; database needs a separate Supabase project. +Staging mirrors production with isolated data. The Next.js app (including the `/api/cron/push` route) deploys to Vercel; database needs a separate Supabase project. ## 1. Database (Supabase) @@ -14,9 +14,9 @@ Staging mirrors production with isolated data. Both components (web + worker cro 6. Schedule cron: same SQL as production but with staging `APP_URL` 7. Note the staging URL and keys -## 2. Web + Worker (Vercel) +## 2. Web (Vercel) -Vercel creates Preview Deployments automatically for non-main branches. The push worker runs as `/api/cron/push` within the same deployment. +Vercel creates Preview Deployments automatically for non-main branches. The hourly push pipeline runs as `/api/cron/push` within the same deployment. For a fixed staging URL: 1. Create a `staging` branch: `git checkout -b staging && git push -u origin staging` @@ -43,7 +43,7 @@ feature branch --> PR --> staging branch (manual merge) --> test --> main (produ | Component | Production | Staging | |-----------|-----------|---------| -| Web + Worker URL | caffecode.net | staging.caffecode.net | +| Web URL (includes cron route) | caffecode.net | staging.caffecode.net | | DB | (production project) | (to be created) | | Cron trigger | pg_cron → production URL | pg_cron → staging URL | | Telegram | @CaffeCodeBot | @CaffeCodeDevBot |