| 1 | # codewhale-web |
| 2 | |
| 3 | Documentation and community site for [Codewhale](https://github.com/Hmbown/CodeWhale) — lives at **codewhale.net**. |
| 4 | |
| 5 | Next.js 15 (App Router) + Tailwind, deployed to Cloudflare Workers via [`@opennextjs/cloudflare`](https://opennext.js.org/cloudflare). Curated "Today's Dispatch" content is regenerated every 6 hours by a Cloudflare Cron Trigger that calls `deepseek-v4-flash` to summarise recent repo activity, and stored in Workers KV. |
| 6 | |
| 7 | ## Local dev |
| 8 | |
| 9 | ```bash |
| 10 | cd web |
| 11 | npm install |
| 12 | cp .env.example .env.local # fill in the keys you have |
| 13 | npm run dev # http://localhost:3000 |
| 14 | ``` |
| 15 | |
| 16 | Env (mirrors `.env.example`): |
| 17 | |
| 18 | | Variable | What | Required? | |
| 19 | | --------------------------- | ---------------------------------------------------------------- | -------------------- | |
| 20 | | `DEEPSEEK_API_KEY` | DeepSeek platform key (`sk-...`) | only for the `/api/cron` tasks (summarization + community agent) | |
| 21 | | `GITHUB_TOKEN` | Fine-grained PAT, public-repo read scope | optional (raises rate limit 60 → 5000 req/h) | |
| 22 | | `GITHUB_REPO` | Defaults to `Hmbown/CodeWhale` | optional | |
| 23 | | `CRON_SECRET` | Shared secret for manual `/api/cron` invocation | optional (Cloudflare cron triggers don't need it) | |
| 24 | | `DEEPSEEK_MODEL` | Defaults to `deepseek-v4-flash` | optional | |
| 25 | | `DEEPSEEK_BASE_URL` | Defaults to `https://api.deepseek.com` | optional | |
| 26 | | `MAINTAINER_TOKEN` | Admin panel auth; access `/admin?token=<value>` | only for `/admin` | |
| 27 | | `MAINTAINER_GITHUB_PAT` | PAT with `issues:write`, for posting comments via `/admin` | only for `/admin` posting | |
| 28 | | `NEXT_PUBLIC_GITEE_ENABLED` | Set to `1` once the Gitee mirror exists; blank hides Gitee links | optional | |
| 29 | |
| 30 | The site renders fine without any of them — `Today's Dispatch` falls back to a static editorial; the GitHub feed shows "feed not yet loaded". |
| 31 | |
| 32 | ## Deploy to Cloudflare |
| 33 | |
| 34 | Ordinary pushes and pull requests run the web checks and production build, but |
| 35 | they do **not** deploy. The `deploy` job in `.github/workflows/web.yml` runs |
| 36 | only for a maintainer-triggered `workflow_dispatch` on `main`. Before approval, |
| 37 | record the exact 40-character `origin/main` SHA and trigger that ref: |
| 38 | |
| 39 | ```bash |
| 40 | git fetch origin main |
| 41 | git rev-parse origin/main |
| 42 | gh workflow run web.yml --repo Hmbown/CodeWhale --ref main |
| 43 | ``` |
| 44 | |
| 45 | The manual job records the pre-deploy source drift, builds the OpenNext bundle, |
| 46 | deploys only after the protected Cloudflare inputs pass, and then requires the |
| 47 | public `/api/facts` receipt to report the exact workflow SHA. A credential-free |
| 48 | local comparison is available without starting a deployment: |
| 49 | |
| 50 | ```bash |
| 51 | npm run compare:deployed-facts -- --expected-revision <exact-40-character-sha> |
| 52 | ``` |
| 53 | |
| 54 | You already own `codewhale.net` on Cloudflare and have a Workers Paid plan. The deploy is two steps: |
| 55 | |
| 56 | 1. **Provision KV namespaces once:** |
| 57 | |
| 58 | ```bash |
| 59 | npx wrangler kv namespace create CURATED_KV |
| 60 | npx wrangler kv namespace create NEXT_INC_CACHE_KV |
| 61 | ``` |
| 62 | |
| 63 | Copy the printed `id` values into the matching `wrangler.jsonc` bindings |
| 64 | (replace each `REPLACE_WITH_KV_ID`). |
| 65 | |
| 66 | 2. **Set secrets and deploy:** |
| 67 | |
| 68 | ```bash |
| 69 | npx wrangler secret put DEEPSEEK_API_KEY |
| 70 | npx wrangler secret put GITHUB_TOKEN # optional |
| 71 | npx wrangler secret put CRON_SECRET # optional, for manual /api/cron?task=curate hits |
| 72 | |
| 73 | npm run deploy # builds with OpenNext + uploads |
| 74 | ``` |
| 75 | |
| 76 | 3. **Point the domain:** in the Cloudflare dashboard, add a Worker route for `codewhale.net/*` → the deployed Worker, named `codewhale-web` (see `wrangler.jsonc`). |
| 77 | |
| 78 | The first cron run happens within 6 hours; you can also kick it manually: |
| 79 | |
| 80 | ```bash |
| 81 | curl -H "x-cron-secret: $CRON_SECRET" "https://codewhale.net/api/cron?task=curate" |
| 82 | ``` |
| 83 | |
| 84 | ## What's where |
| 85 | |
| 86 | Pages are bilingual by default: each `app/[locale]/` page renders both |
| 87 | English and Chinese from the same file, keyed by the `[locale]` segment |
| 88 | (see `lib/i18n/config.ts`). Copy changes must update both locales. The |
| 89 | v0.9.2 wave adds routed **partial** locales (ja, vi, ko, ru, uk, es, pt-BR): |
| 90 | their shared chrome (nav/footer/switcher) and home-page copy live in |
| 91 | `lib/i18n/dictionaries/<code>/` (checked by `npm run check:locales`), and |
| 92 | everything else falls back to the English copy. Routing, middleware |
| 93 | detection (`lib/i18n/detect.ts`), sitemap, and hreflang all derive from the |
| 94 | one registry. |
| 95 | |
| 96 | ``` |
| 97 | web/ |
| 98 | ├── app/ |
| 99 | │ ├── globals.css ocean portal, docs layout, type, and shared surfaces |
| 100 | │ ├── [locale]/ 10 routed locales; zh has native page bodies, |
| 101 | │ │ the rest fall back to the English body |
| 102 | │ │ ├── layout.tsx root + locale layout: html shell, fonts, nav, footer |
| 103 | │ │ ├── page.tsx home — hero, ticker, proof, decides, workflow, |
| 104 | │ │ │ start, boundaries, surfaces, install band, community |
| 105 | │ │ ├── install/page.tsx per-OS install with auto-detection |
| 106 | │ │ ├── docs/page.tsx modes / tools / approval / config / mcp / providers |
| 107 | │ │ ├── faq/page.tsx frequently asked questions |
| 108 | │ │ ├── feed/page.tsx live mirror of issues + PRs |
| 109 | │ │ ├── roadmap/page.tsx shipped / underway / considered / ruled out |
| 110 | │ │ ├── contribute/page.tsx how to PR + house rules + dev loop |
| 111 | │ │ └── admin/ maintainer panel (page.tsx + admin-client.tsx) |
| 112 | │ └── api/ |
| 113 | │ ├── cron/route.ts cron tasks: curate, triage, facts-drift, … |
| 114 | │ ├── facts/route.ts public source/deployment receipt |
| 115 | │ ├── github/feed/route.ts cached JSON endpoint |
| 116 | │ └── admin/ login, logout, post (MAINTAINER_TOKEN-gated) |
| 117 | ├── data/ |
| 118 | │ └── latest-published-release.json manually advanced only after publication |
| 119 | ├── components/ |
| 120 | │ ├── nav.tsx sticky header w/ date strip + CJK accents |
| 121 | │ ├── footer.tsx dense 5-column footer |
| 122 | │ ├── whale.tsx shared Codewhale mark |
| 123 | │ ├── ticker.tsx live wire: merges, issues, releases + handles |
| 124 | │ ├── feed-card.tsx one issue/PR card |
| 125 | │ ├── locale-switcher.tsx N-locale dropdown with partial badges |
| 126 | │ └── install-*.tsx install page blocks (binary, code block, tiles) |
| 127 | ├── lib/ |
| 128 | │ ├── types.ts shared types |
| 129 | │ ├── i18n/ locale config, en/zh dictionaries |
| 130 | │ ├── github.ts REST client + relative-time formatter |
| 131 | │ ├── deepseek.ts v4-flash chat client + curate() prompt |
| 132 | │ ├── facts.ts getFacts(): KV value, else build-time FACTS |
| 133 | │ ├── facts.generated.ts GENERATED — do not edit by hand |
| 134 | │ ├── facts-drift.ts runtime re-derivation for the drift cron |
| 135 | │ ├── community-agent.ts triage / pr-review / digest cron tasks |
| 136 | │ └── kv.ts Cloudflare KV access via OpenNext bindings |
| 137 | ├── scripts/ |
| 138 | │ ├── derive-facts.mjs prebuild: repo sources → lib/facts.generated.ts |
| 139 | │ ├── compare-deployed-facts.mjs credential-free exact-SHA receipt check |
| 140 | │ └── check-kv-id.mjs predeploy guard for KV namespace ids |
| 141 | ├── wrangler.jsonc CF Worker config + cron + KV binding |
| 142 | ├── open-next.config.ts OpenNext adapter config |
| 143 | └── tailwind.config.ts design tokens |
| 144 | ``` |
| 145 | |
| 146 | ## Facts pipeline |
| 147 | |
| 148 | Mechanical facts (version, provider list, sandbox backends, crate names, |
| 149 | default model, Node engines) are never hand-written into pages: |
| 150 | |
| 151 | 1. **Build time** — `scripts/derive-facts.mjs` runs as `prebuild` (and before |
| 152 | `npm run dev`), parses the parent repo (`Cargo.toml`, `crates/tui/src/config.rs`, |
| 153 | `crates/tui/src/sandbox/mod.rs`, `npm/codewhale/package.json`) and writes |
| 154 | `lib/facts.generated.ts`. Never edit that file by hand. |
| 155 | 2. **Published release** — `data/latest-published-release.json` records the |
| 156 | latest GitHub Release separately from the source candidate. Install commands |
| 157 | use this published tag; they never turn the workspace version into a release |
| 158 | before publication. The credential-free deployed-facts comparison checks the |
| 159 | record against the public receipt. |
| 160 | 3. **Runtime** — the `/api/cron?task=facts-drift` cron (`lib/facts-drift.ts`) |
| 161 | resolves an exact `main` revision, derives every source fact from that SHA, |
| 162 | and writes changes to `CURATED_KV` under `facts:current`. Pages accept that |
| 163 | snapshot only when its source provenance is the same as or newer than the |
| 164 | deployed build. Legacy, malformed, or older KV data cannot replace newer |
| 165 | build facts; published-release metadata is resolved independently. Public |
| 166 | fact pages revalidate their cached HTML every five minutes. |
| 167 | |
| 168 | `/api/facts` exposes only public provenance and counts: deployed/resolved source |
| 169 | revision, version, provider count, tool count, selection reason, and latest |
| 170 | published release. It contains no environment values, tokens, or KV contents. |
| 171 | |
| 172 | When a new `ApiProvider` variant lands in `crates/tui/src/config.rs`, it must |
| 173 | be added to the `labelMap` in **both** `scripts/derive-facts.mjs` and |
| 174 | `lib/facts-drift.ts` (or to the `EXCLUDED` set if deliberately hidden). Both |
| 175 | fail loudly on unmapped variants, so the build / cron will tell you. |
| 176 | |
| 177 | ## Visual direction |
| 178 | |
| 179 | The public site is a documentation portal with a restrained underwater atmosphere. Content and navigation come first; ocean depth, currents, and the whale mark provide identity without turning every section into a themed card. |
| 180 | |
| 181 | - **Palette**: cool paper and mist for reading surfaces, deep navy for terminal and community sections, muted current blue for links, and small gold/coral signals where status needs contrast. |
| 182 | - **Type**: Space Grotesk for headings, IBM Plex Sans for body copy, and JetBrains Mono for commands and compact interface labels. |
| 183 | - **Structure**: compact documentation rows, quiet hairline dividers, generous but bounded reading widths, and responsive layouts that remove chrome before content. |
| 184 | |
| 185 | If you want to retune the palette, edit `:root` in `app/globals.css` and the `colors` block in `tailwind.config.ts`. |
| 186 |