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
11 changes: 6 additions & 5 deletions .claude/rules/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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

Expand All @@ -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
Expand All @@ -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`) |
4 changes: 2 additions & 2 deletions .claude/rules/git-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**:
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 1 addition & 2 deletions .claude/rules/notifications.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -28,4 +27,4 @@ paths:

- Resend domain: `caffecode.net` (Cloudflare DNS, verified SPF/DKIM/DMARC)
- From: `CaffeCode <noreply@caffecode.net>`
- 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
16 changes: 10 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <CRON_SECRET>`.

**Pre-curated content model**: All problem content generated offline via admin UI. Zero runtime LLM calls.
Expand All @@ -30,7 +32,7 @@ Follows Conventional Commits 1.0.0 (Google/Angular style). Full format in `.clau

- Branch: `<type>/<short-kebab-description>`
- Commit: `<type>(<scope>): <subject>` — 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.

Expand Down Expand Up @@ -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`
Expand All @@ -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 `<Suspense>` 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 <branch>`.

Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions docs/staging-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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`
Expand All @@ -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 |
Loading