From d60c5c97a920d96b12193908e9298bfabc154c10 Mon Sep 17 00:00:00 2001 From: yiekheng Date: Sat, 2 May 2026 18:12:59 +0800 Subject: [PATCH] Add implementation plan for B1 (Next.js scaffold) --- .../plans/2026-05-02-b1-nextjs-scaffold.md | 856 ++++++++++++++++++ 1 file changed, 856 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-02-b1-nextjs-scaffold.md diff --git a/docs/superpowers/plans/2026-05-02-b1-nextjs-scaffold.md b/docs/superpowers/plans/2026-05-02-b1-nextjs-scaffold.md new file mode 100644 index 0000000..4b458b6 --- /dev/null +++ b/docs/superpowers/plans/2026-05-02-b1-nextjs-scaffold.md @@ -0,0 +1,856 @@ +# B1: Next.js Scaffold Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Stand up a `web/` Next.js 15 project, a `docker/web-next/Dockerfile`, and a `web-next` compose service exposed on `${CM_WEB_NEXT_HOST_PORT:-8010}` that serves a `frontend-design`-generated placeholder page and a catch-all Route Handler proxy to `api-server:3000`. + +**Architecture:** Hand-roll the Next.js project files instead of using `npx create-next-app` for reproducibility. Tailwind v4 (CSS-first config — no `tailwind.config.ts`). Catch-all `web/app/api/[...path]/route.ts` forwards GET/POST to `api-server:3000` preserving the trailing slash. Multi-stage `node:22-alpine` Dockerfile with `output: "standalone"`. Side-by-side with the existing `cm-web` Flask service — both run; B4 retires Flask later. + +**Tech Stack:** Next.js 15.x stable, React 19.x, TypeScript 5.x, Tailwind CSS v4 (`@tailwindcss/postcss`), Node 22 LTS, npm. No new dev tools. UI code (`layout.tsx`, `page.tsx`) generated by the `frontend-design` skill per the spec. + +**Spec:** [docs/superpowers/specs/2026-05-02-b1-nextjs-scaffold-design.md](../specs/2026-05-02-b1-nextjs-scaffold-design.md) + +--- + +## File Map + +| File | Operation | Purpose | +|---|---|---| +| `web/package.json` | Create | Next.js 15 + React 19 + Tailwind v4 deps; npm scripts. | +| `web/package-lock.json` | Create | Generated by `npm install`. Committed for reproducible Docker builds. | +| `web/tsconfig.json` | Create | TypeScript config matching Next.js 15 defaults. | +| `web/next.config.ts` | Create | `output: "standalone"`, `trailingSlash: true`. | +| `web/postcss.config.mjs` | Create | Tailwind v4 PostCSS plugin. | +| `web/.gitignore` | Create | `.next/`, `node_modules/`, build/test outputs. | +| `web/.dockerignore` | Create | Excludes `node_modules/`, `.next/`, `.git/`. | +| `web/next-env.d.ts` | Create | Auto-generated reference; committed verbatim. | +| `web/app/layout.tsx` | Create (via frontend-design) | Root layout. | +| `web/app/page.tsx` | Create (via frontend-design) | Scaffold-confirmation placeholder. | +| `web/app/globals.css` | Create | `@import "tailwindcss";` | +| `web/app/api/[...path]/route.ts` | Create | Catch-all GET/POST proxy to `api-server:3000`. | +| `docker/web-next/Dockerfile` | Create | Multi-stage Node 22 alpine, standalone output. | +| `docker-compose.yml` | Modify | Add `web-next` service. | +| `docker-compose.override.yml` | Modify | Add `web-next` build directive. | +| `envs/dev/.env.example` | Modify | `CM_WEB_NEXT_HOST_PORT=8010` | +| `envs/rex/.env.example` | Modify | `CM_WEB_NEXT_HOST_PORT=8011` | +| `envs/siong/.env.example` | Modify | `CM_WEB_NEXT_HOST_PORT=8012` | +| `scripts/dev.sh` | Modify | Include `web-next` in `up`/`logs`/`reset-db`. | +| `scripts/publish.sh` | Modify | Append `web-next` to `SERVICES`. | +| `AGENTS.md` | Modify | Mention `web/` and the `cm-web-next` service. | + +No file removals. Nothing in `app/` is touched. + +--- + +## Task 1: Bootstrap `web/` package + configs + +**Files:** +- Create: `web/package.json` +- Create: `web/tsconfig.json` +- Create: `web/next.config.ts` +- Create: `web/postcss.config.mjs` +- Create: `web/.gitignore` +- Create: `web/.dockerignore` +- Create: `web/next-env.d.ts` + +- [ ] **Step 1: Create `web/` and write `package.json`** + +```bash +mkdir -p /home/yiekheng/projects/cm_bot_v2/web +``` + +Create `web/package.json`: + +```json +{ + "name": "cm-web-next", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "next": "15.1.0", + "react": "19.0.0", + "react-dom": "19.0.0" + }, + "devDependencies": { + "@tailwindcss/postcss": "4.0.0", + "@types/node": "22.10.0", + "@types/react": "19.0.0", + "@types/react-dom": "19.0.0", + "tailwindcss": "4.0.0", + "typescript": "5.7.0" + } +} +``` + +The pinned versions are mid-2024-stable Next.js 15 + React 19 + Tailwind v4 final. Lockfile (Step 6) will resolve transitive deps. + +- [ ] **Step 2: Write `web/tsconfig.json`** + +```json +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [{ "name": "next" }], + "paths": { "@/*": ["./*"] } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} +``` + +- [ ] **Step 3: Write `web/next.config.ts`** + +```typescript +import type { NextConfig } from "next"; + +const config: NextConfig = { + output: "standalone", + trailingSlash: true, +}; + +export default config; +``` + +- [ ] **Step 4: Write `web/postcss.config.mjs`** + +```javascript +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; +``` + +- [ ] **Step 5: Write `web/.gitignore` and `web/.dockerignore`** + +`web/.gitignore`: + +``` +node_modules/ +/.next/ +/out/ +/build/ +.DS_Store +*.tsbuildinfo +next-env.d.ts.bak +.env*.local +``` + +`web/.dockerignore`: + +``` +node_modules +.next +.git +.gitignore +README.md +``` + +- [ ] **Step 6: Write `web/next-env.d.ts`** + +This file is normally auto-generated by Next.js; committing it verbatim avoids a phantom diff every build. + +```typescript +/// +/// + +// NOTE: This file should not be edited. +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. +``` + +- [ ] **Step 7: Install dependencies and generate the lockfile** + +```bash +cd /home/yiekheng/projects/cm_bot_v2/web && \ +npm install --no-audit --no-fund 2>&1 | tail -10 +``` + +Expected: completion line like `added packages` and a new `web/package-lock.json`. Errors at this step usually mean the version pins above don't co-resolve; bump the offender to its latest patch and rerun. + +- [ ] **Step 8: Verify the build chain works on configs alone** + +`next build` requires at least one route. Defer the build smoke to Task 3 once we have `app/page.tsx` and the route handler. For now, sanity-check that `npx tsc --noEmit` doesn't error: + +```bash +cd /home/yiekheng/projects/cm_bot_v2/web && \ +npx tsc --noEmit && echo "tsc OK" +``` + +Expected: `tsc OK` (no output from tsc, since there are no .ts files yet — just configs). + +- [ ] **Step 9: Commit** + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +git add web/package.json web/package-lock.json web/tsconfig.json web/next.config.ts web/postcss.config.mjs web/.gitignore web/.dockerignore web/next-env.d.ts && \ +git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \ + commit -m "feat(web): bootstrap Next.js 15 project configs and lockfile" +``` + +--- + +## Task 2: Generate `layout.tsx` and `page.tsx` via `frontend-design` + +**Files:** +- Create: `web/app/layout.tsx` (via frontend-design) +- Create: `web/app/page.tsx` (via frontend-design) +- Create: `web/app/globals.css` + +- [ ] **Step 1: Write `web/app/globals.css`** + +```bash +mkdir -p /home/yiekheng/projects/cm_bot_v2/web/app +``` + +Create `web/app/globals.css`: + +```css +@import "tailwindcss"; +``` + +- [ ] **Step 2: Invoke the `frontend-design` skill** + +Use the Skill tool with `skill="frontend-design:frontend-design"` and the following brief verbatim (from the B1 spec's "Empty UI page" section): + +``` +Generate two files for a Next.js 15 App Router project that uses Tailwind v4 +(already configured via `@import "tailwindcss";` in `app/globals.css`): + +1. `app/layout.tsx` — minimal root layout. ; with + Tailwind defaults (no custom font). Tab title: "CM Bot V2". Imports + `./globals.css`. Server Component. No metadata API beyond `title`. + +2. `app/page.tsx` — a scaffold-confirmation placeholder for the + `cm-web-next` service. Required content: + - Product name "CM Bot V2" + - Literal text "cm-web-next scaffold" (operators grep for this) + - One-line note that the real dashboard lands in B2 + - An obvious link to /api/acc/ for smoke-testing the proxy + +Constraints: +- Tailwind v4 utility classes only — no external font, image, or icon deps. +- Server Component (no "use client", no JS interactivity). +- Single page, no navigation. +- Should clearly read as a temporary scaffold, not a real dashboard. The + visual treatment should signal "work-in-progress / placeholder" so a + user landing here doesn't mistake it for the production UI. +- Mobile-first responsive defaults; no dark mode, no animations. + +Out of scope: dark mode, multi-route navigation, charts/tables, animations. +``` + +The skill will return TSX content for the two files. Save the returned `layout.tsx` to `web/app/layout.tsx` and the returned `page.tsx` to `web/app/page.tsx`. + +- [ ] **Step 3: Verify the generated files compile and the page renders** + +```bash +cd /home/yiekheng/projects/cm_bot_v2/web && \ +npm run build 2>&1 | tail -20 +``` + +Expected: + +- A `Compiled successfully` line (or equivalent for Next.js 15). +- A route summary line for `/` (the page) and any other auto-generated routes. +- No TypeScript errors. If any appear, the issue is in the generated TSX — re-invoke `frontend-design` with the additional constraint "fix this TS error: " and replace. + +- [ ] **Step 4: Verify the placeholder text is present** + +```bash +cd /home/yiekheng/projects/cm_bot_v2/web && \ +grep -E "cm-web-next scaffold|CM Bot V2" app/page.tsx app/layout.tsx +``` + +Expected: hits in both files. Specifically `page.tsx` should contain the literal string `cm-web-next scaffold` (operators search for this) and a reference to `/api/acc/` (the smoke-test link). + +- [ ] **Step 5: Commit** + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +git add web/app/layout.tsx web/app/page.tsx web/app/globals.css && \ +git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \ + commit -m "feat(web): add scaffold layout and page (frontend-design generated)" +``` + +--- + +## Task 3: Catch-all Route Handler proxy to `api-server:3000` + +**Files:** +- Create: `web/app/api/[...path]/route.ts` + +- [ ] **Step 1: Create the route handler directory** + +```bash +mkdir -p /home/yiekheng/projects/cm_bot_v2/web/app/api/\[...path\] +``` + +(The square brackets are Next.js's catch-all dynamic segment syntax. Shell-escape with backslashes.) + +- [ ] **Step 2: Write the route handler** + +Create `web/app/api/[...path]/route.ts`: + +```typescript +import { NextRequest, NextResponse } from "next/server"; + +const API_BASE_URL = process.env.API_BASE_URL ?? "http://api-server:3000"; + +async function forward(request: NextRequest, path: string[]): Promise { + const targetPath = "/" + path.join("/"); + const trailingSlash = request.nextUrl.pathname.endsWith("/") ? "/" : ""; + const target = `${API_BASE_URL}${targetPath}${trailingSlash}`; + + const init: RequestInit = { + method: request.method, + headers: { + "content-type": + request.headers.get("content-type") ?? "application/json", + }, + }; + if (request.method !== "GET" && request.method !== "HEAD") { + init.body = await request.text(); + } + + try { + const upstream = await fetch(target, init); + const body = await upstream.text(); + return new NextResponse(body, { + status: upstream.status, + headers: { + "content-type": + upstream.headers.get("content-type") ?? "application/json", + }, + }); + } catch (err) { + return NextResponse.json({ error: String(err) }, { status: 500 }); + } +} + +export async function GET( + request: NextRequest, + ctx: { params: Promise<{ path: string[] }> }, +): Promise { + return forward(request, (await ctx.params).path); +} + +export async function POST( + request: NextRequest, + ctx: { params: Promise<{ path: string[] }> }, +): Promise { + return forward(request, (await ctx.params).path); +} +``` + +The error shape `{"error": }` with HTTP 500 matches `cm_web_view.py`'s Flask proxy on exception. + +- [ ] **Step 3: Build to verify the route compiles** + +```bash +cd /home/yiekheng/projects/cm_bot_v2/web && \ +npm run build 2>&1 | tail -25 +``` + +Expected: route summary now includes `/api/[...path]` (or similar). No TS errors. + +- [ ] **Step 4: Commit** + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +git add web/app/api/\[...path\]/route.ts && \ +git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \ + commit -m "feat(web): add catch-all Route Handler proxy to api-server:3000" +``` + +--- + +## Task 4: Multi-stage Dockerfile for `cm-web-next` + +**Files:** +- Create: `docker/web-next/Dockerfile` + +- [ ] **Step 1: Create the Dockerfile directory and file** + +```bash +mkdir -p /home/yiekheng/projects/cm_bot_v2/docker/web-next +``` + +Create `docker/web-next/Dockerfile`: + +```dockerfile +# syntax=docker/dockerfile:1.7 + +# --- deps --- +FROM node:22-alpine AS deps +WORKDIR /app +COPY web/package.json web/package-lock.json* ./ +RUN npm ci + +# --- build --- +FROM node:22-alpine AS build +WORKDIR /app +ENV NEXT_TELEMETRY_DISABLED=1 +COPY --from=deps /app/node_modules ./node_modules +COPY web/ ./ +RUN npm run build + +# --- runtime --- +FROM node:22-alpine AS runtime +WORKDIR /app +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +ENV PORT=3000 +ENV HOSTNAME=0.0.0.0 +COPY --from=build /app/.next/standalone ./ +COPY --from=build /app/.next/static ./.next/static +COPY --from=build /app/public ./public +EXPOSE 3000 +CMD ["node", "server.js"] +``` + +`web/public/` doesn't exist yet but the `COPY public ./public` step is harmless if Next.js 15 didn't create it during build (the standalone output bundles public). If `npm run build` fails because `public/` is missing, create the directory empty: + +```bash +mkdir -p /home/yiekheng/projects/cm_bot_v2/web/public +touch /home/yiekheng/projects/cm_bot_v2/web/public/.gitkeep +``` + +- [ ] **Step 2: (Optional) Verify the Dockerfile builds locally** + +This is optional because it requires docker on the engineer's machine. If available: + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +sudo docker build -f docker/web-next/Dockerfile -t cm-web-next:plan-test . 2>&1 | tail -20 +``` + +Expected: a "Successfully tagged cm-web-next:plan-test" line. If the build fails because of a missing `web/public/` directory, run the `mkdir -p web/public` from Step 1's note and rebuild. Skip this step if docker isn't available locally; Task 8 covers the integration verification. + +- [ ] **Step 3: Commit** + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +git add docker/web-next/Dockerfile $(test -d web/public && echo "web/public/.gitkeep") && \ +git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \ + commit -m "feat(docker): add multi-stage Dockerfile for cm-web-next" +``` + +--- + +## Task 5: Add `web-next` to compose + +**Files:** +- Modify: `docker-compose.yml` +- Modify: `docker-compose.override.yml` + +- [ ] **Step 1: Add `web-next` service to base compose** + +Find the existing `transfer-bot:` block in `docker-compose.yml` and add the `web-next:` block immediately above it (so the natural reading order is: telegram-bot → api-server → web-view → web-next → transfer-bot). The new block: + +```yaml + # Next.js Web View (side-by-side with web-view during B-cycle migration). + web-next: + image: "${CM_IMAGE_PREFIX:-your-registry/namespace}/cm-web-next:${DOCKER_IMAGE_TAG:-latest}" + container_name: ${CM_DEPLOY_NAME:-cm}-web-next + restart: unless-stopped + ports: + - "${CM_WEB_NEXT_HOST_PORT:-8010}:3000" + environment: + NODE_ENV: production + NEXT_TELEMETRY_DISABLED: "1" + API_BASE_URL: http://api-server:3000 + volumes: + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + networks: + - bot-network + depends_on: + - api-server +``` + +- [ ] **Step 2: Add the build directive in the override** + +In `docker-compose.override.yml`, append to the `services:` section (after the existing `transfer-bot:` block, before any top-level keys like `volumes:`): + +```yaml + web-next: + build: + context: . + dockerfile: docker/web-next/Dockerfile + image: "${CM_IMAGE_PREFIX:-local}/cm-web-next:${DOCKER_IMAGE_TAG:-dev}" +``` + +No `command:` override and no `profiles:` — `web-next` is part of the dev stack like `web-view`. + +- [ ] **Step 3: Validate both compose files** + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +.venv/bin/python -c " +import yaml +with open('docker-compose.yml') as f: + base = yaml.safe_load(f) +assert 'web-next' in base['services'], 'web-next missing from base' +wn = base['services']['web-next'] +assert wn['ports'] == ['\${CM_WEB_NEXT_HOST_PORT:-8010}:3000'] +assert wn['environment']['API_BASE_URL'] == 'http://api-server:3000' +assert wn['depends_on'] == ['api-server'] +print('base config: web-next service wired correctly') + +with open('docker-compose.override.yml') as f: + over = yaml.safe_load(f) +assert 'web-next' in over['services'] +wn_over = over['services']['web-next'] +assert wn_over['build']['dockerfile'] == 'docker/web-next/Dockerfile' +print('override: web-next build directive present') +" +``` + +Expected: +``` +base config: web-next service wired correctly +override: web-next build directive present +``` + +- [ ] **Step 4: Commit** + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +git add docker-compose.yml docker-compose.override.yml && \ +git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \ + commit -m "feat(compose): add web-next service (side-by-side with web-view)" +``` + +--- + +## Task 6: Update env example files + +**Files:** +- Modify: `envs/dev/.env.example` +- Modify: `envs/rex/.env.example` +- Modify: `envs/siong/.env.example` + +- [ ] **Step 1: Update `envs/dev/.env.example`** + +Find the `CM_WEB_HOST_PORT=8000` line in the `=== Deployment Identity ===` section and add `CM_WEB_NEXT_HOST_PORT=8010` immediately after it: + +``` +# === Deployment Identity === +CM_DEPLOY_NAME=dev-cm +CM_WEB_HOST_PORT=8000 +CM_WEB_NEXT_HOST_PORT=8010 +``` + +- [ ] **Step 2: Update `envs/rex/.env.example`** + +Add `CM_WEB_NEXT_HOST_PORT=8011` after `CM_WEB_HOST_PORT=8001`: + +``` +# === Deployment Identity === +CM_DEPLOY_NAME=rex-cm +CM_WEB_HOST_PORT=8001 +CM_WEB_NEXT_HOST_PORT=8011 +``` + +- [ ] **Step 3: Update `envs/siong/.env.example`** + +Add `CM_WEB_NEXT_HOST_PORT=8012` after `CM_WEB_HOST_PORT=8005`: + +``` +# === Deployment Identity === +CM_DEPLOY_NAME=siong-cm +CM_WEB_HOST_PORT=8005 +CM_WEB_NEXT_HOST_PORT=8012 +``` + +- [ ] **Step 4: Verify all three files agree on the new key** + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +grep -H "^CM_WEB_NEXT_HOST_PORT=" envs/*/.env.example +``` + +Expected: three lines, one per deployment, each with a distinct port (8010 / 8011 / 8012). + +- [ ] **Step 5: Commit** + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +git add envs/dev/.env.example envs/rex/.env.example envs/siong/.env.example && \ +git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \ + commit -m "feat(envs): add CM_WEB_NEXT_HOST_PORT to all .env.example templates" +``` + +--- + +## Task 7: Wire `web-next` into `dev.sh` and `publish.sh` + +**Files:** +- Modify: `scripts/dev.sh` +- Modify: `scripts/publish.sh` + +- [ ] **Step 1: Update `dev.sh`** + +In `scripts/dev.sh`, find the three places that currently pass the explicit service list and add `web-next`: + +`up` case (currently `mysql api-server web-view`): + +```bash + up) + "${COMPOSE[@]}" up -d --build mysql api-server web-view web-next + "${COMPOSE[@]}" ps + ;; +``` + +`reset-db` case (the inner `up` invocation): + +```bash + reset-db) + "${COMPOSE[@]}" down --volumes --remove-orphans + "${COMPOSE[@]}" up -d --build mysql api-server web-view web-next + ;; +``` + +`logs` case: + +```bash + logs) + "${COMPOSE[@]}" logs -f mysql api-server web-view web-next + ;; +``` + +The `status` case stays as-is — it only checks `mysql`. + +Update `usage()` so the help text mentions the new service: + +```bash +usage() { + cat <<'EOF' +Lifecycle for the dev stack (mysql + api-server + web-view + web-next). + +Usage: + scripts/dev.sh up Start all dev services in the background. + scripts/dev.sh down Stop the stack. mysql volume kept (DB persists). + scripts/dev.sh reset-db Stop the stack AND drop the mysql volume; then start. + scripts/dev.sh logs Tail logs from the running stack. + scripts/dev.sh status Print 'OK' if mysql is running, else exit 1. + +Environment: + NO_SUDO=1 Skip the 'sudo' prefix (use if your user is in the docker group). +EOF +} +``` + +- [ ] **Step 2: Update `publish.sh`** + +In `scripts/publish.sh`, find the `SERVICES=` array (currently four entries) and append `web-next`: + +```bash +SERVICES=( + "api docker/api/Dockerfile" + "telegram docker/telegram/Dockerfile" + "web docker/web/Dockerfile" + "transfer docker/transfer/Dockerfile" + "web-next docker/web-next/Dockerfile" +) +``` + +The image-name template `${REGISTRY_PREFIX}/cm-${SERVICE}:${IMAGE_TAG}` produces `gitea.04080616.xyz/yiekheng/cm-web-next:` — matching the compose `image:` reference. + +- [ ] **Step 3: Bash syntax-check both scripts** + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +bash -n scripts/dev.sh && bash -n scripts/publish.sh && echo "syntax OK" +``` + +Expected: `syntax OK`. + +- [ ] **Step 4: Commit** + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +git add scripts/dev.sh scripts/publish.sh && \ +git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \ + commit -m "feat(scripts): include web-next in dev.sh and publish.sh" +``` + +--- + +## Task 8: AGENTS.md updates + +**Files:** +- Modify: `AGENTS.md` + +- [ ] **Step 1: Add `web/` to the Project Structure section** + +Find the existing `## Project Structure & Module Organization` section and add a new bullet after the `app/` bullets, immediately above the `docker//Dockerfile` line: + +Find: + +``` +- `docker//Dockerfile` builds one image per service (`cm-api`, `cm-web`, `cm-telegram`, `cm-transfer`). +``` + +Replace with: + +``` +- `web/` is the Next.js 15 app for the new web view (`cm-web-next` service). Tailwind v4, App Router, TypeScript. Side-by-side with the legacy Flask `cm_web_view.py` until B4 cuts over. +- `docker//Dockerfile` builds one image per service (`cm-api`, `cm-web`, `cm-web-next`, `cm-telegram`, `cm-transfer`). +``` + +- [ ] **Step 2: Update the Dev Tier section's URL note** + +In the existing `## Dev Tier (Local Development)` section, find the bullet that mentions the lifecycle script: + +``` +- Lifecycle: `bash scripts/dev.sh {up,down,reset-db,logs,status}`. +``` + +Add a follow-up bullet describing the new URL: + +``` +- Lifecycle: `bash scripts/dev.sh {up,down,reset-db,logs,status}`. +- URLs: `http://localhost:8000/` (legacy Flask UI), `http://localhost:8010/` (new Next.js scaffold). Both run side-by-side until the B4 cutover retires the Flask version. +``` + +- [ ] **Step 3: Commit** + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +git add AGENTS.md && \ +git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \ + commit -m "docs(agents): document web/ Next.js project and cm-web-next dev URL" +``` + +--- + +## Task 9: Integration verification (deployer host required) + +This task corresponds to the verification scenarios in the spec. No commits — these are smoke checks. If anything fails, debug before declaring done. + +**Files:** none modified. + +**Prerequisites:** docker compose v2 plugin installed; the engineer's `.env` has `CM_WEB_NEXT_HOST_PORT` set (default 8010 if absent thanks to `${CM_WEB_NEXT_HOST_PORT:-8010}` in the compose interpolation). + +- [ ] **Step 1: Bring up the dev stack** + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +bash scripts/dev.sh up +``` + +Wait ~25–35s (mysql healthcheck + npm/Next.js startup). Then: + +```bash +sudo docker compose -f docker-compose.yml -f docker-compose.override.yml ps +``` + +Expected: five containers running — `dev-cm-mysql`, `dev-cm-api-server`, `dev-cm-web-view`, `dev-cm-web-next`, plus a healthy `mysql`. + +- [ ] **Step 2: Empty page renders** + +```bash +curl -s http://localhost:8010/ | grep -E "cm-web-next scaffold|CM Bot V2" +``` + +Expected: hits in the response. Open `http://localhost:8010/` in a browser — page is visible, looks like a placeholder, has a link to `/api/acc/`. + +- [ ] **Step 3: API proxy parity (GET)** + +```bash +diff <(curl -s http://localhost:8000/api/acc/) <(curl -s http://localhost:8010/api/acc/) && echo "GET parity OK" +``` + +Expected: `GET parity OK`. Both go through to `api-server:3000/acc/` and return the same JSON. + +- [ ] **Step 4: API proxy parity (POST)** + +```bash +diff \ + <(curl -s -X POST -H 'Content-Type: application/json' \ + -d '{"username":"13c1000","password":"x","status":"","link":""}' \ + http://localhost:8000/api/update-acc-data) \ + <(curl -s -X POST -H 'Content-Type: application/json' \ + -d '{"username":"13c1000","password":"x","status":"","link":""}' \ + http://localhost:8010/api/update-acc-data) \ + && echo "POST parity OK" +``` + +Expected: `POST parity OK`. Both POSTs round-trip through to `api-server:3000/update-acc-data`. + +- [ ] **Step 5: Old `cm-web` still serves** + +```bash +curl -sf http://localhost:8000/ | head -c 200; echo +``` + +Expected: HTML containing the existing Flask `CM Bot Database Viewer` (unchanged). + +- [ ] **Step 6: Image is buildable through `publish.sh`** + +```bash +bash scripts/publish.sh --help | head -5 +``` + +Expected: usage block lists `web-next` (or at least no errors). Optional full publish: `bash scripts/publish.sh dev-test` after `docker login gitea.04080616.xyz`. + +- [ ] **Step 7: Prod compose parity check** + +```bash +sudo docker compose -f docker-compose.yml config | grep -E "web-next:|web-view:|api-server:" | head +sudo docker compose -f docker-compose.yml config | grep -E "8010|8001|3000:3000" | head +``` + +Expected: `web-next:` listed alongside the other services; web-next bound on `${CM_WEB_NEXT_HOST_PORT:-8010}:3000`; api-server has no host port (preserved from C5). + +- [ ] **Step 8: Tear down** + +```bash +bash scripts/dev.sh down +``` + +Expected: clean shutdown (no orphan complaints — `--remove-orphans` from the C cycle handles it). + +--- + +## Spec Coverage Check (self-review) + +| Spec requirement | Task | +|---|---| +| `web/` directory at repo root, full Next.js project | Task 1 | +| Next.js 15 + App Router + TypeScript + Tailwind v4 | Task 1 (configs) + Task 2 (app shell) | +| `output: "standalone"`, `trailingSlash: true` in `next.config.ts` | Task 1 step 3 | +| `frontend-design`-generated `layout.tsx` and `page.tsx` | Task 2 step 2 | +| Catch-all proxy at `web/app/api/[...path]/route.ts` | Task 3 | +| Multi-stage Dockerfile (Node 22 alpine, standalone output) | Task 4 | +| `web-next` service in base compose, `${CM_WEB_NEXT_HOST_PORT:-8010}:3000` | Task 5 step 1 | +| Build directive in override | Task 5 step 2 | +| `CM_WEB_NEXT_HOST_PORT` in dev/rex/siong .env.example (8010/8011/8012) | Task 6 | +| `dev.sh` includes `web-next` in up/logs/reset-db | Task 7 step 1 | +| `publish.sh` adds `web-next` image | Task 7 step 2 | +| AGENTS.md updates | Task 8 | +| Side-by-side preserved (cm-web Flask untouched) | Verified across Tasks 5, 9 | +| Integration verification | Task 9 | + +No gaps. No placeholders. Type names (`NextRequest`, `NextResponse`) and config keys (`output`, `trailingSlash`, `API_BASE_URL`, `CM_WEB_NEXT_HOST_PORT`) consistent across tasks. The frontend-design invocation is the only step where the produced TSX content isn't quoted verbatim — by design, since the skill is the source of authority for the design.