cm_bot_v2/docs/superpowers/plans/2026-05-02-b1-nextjs-scaffold.md

857 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// 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 <N> 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. <html lang="en">; <body> 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: <paste>" 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<NextResponse> {
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<NextResponse> {
return forward(request, (await ctx.params).path);
}
export async function POST(
request: NextRequest,
ctx: { params: Promise<{ path: string[] }> },
): Promise<NextResponse> {
return forward(request, (await ctx.params).path);
}
```
The error shape `{"error": <string>}` 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:<tag>` — 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/<service>/Dockerfile` line:
Find:
```
- `docker/<service>/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/<service>/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 ~2535s (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 `<title>CM Bot Database Viewer</title>` (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.