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

30 KiB
Raw Blame History

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


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 that maps the public hash to the upstream path and forwards to api-server:3000.
web/lib/api-paths.ts Create Hash → upstream-name mapping (single source of truth for both the Route Handler and future client code).
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

mkdir -p /home/yiekheng/projects/cm_bot_v2/web

Create web/package.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
{
  "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
import type { NextConfig } from "next";

const config: NextConfig = {
  output: "standalone",
  trailingSlash: true,
};

export default config;
  • Step 4: Write web/postcss.config.mjs
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.

/// <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
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:

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
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

mkdir -p /home/yiekheng/projects/cm_bot_v2/web/app

Create web/app/globals.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/414322309db5c06d/ for smoke-testing the proxy
  (this is the SHA-256[:16] of "acc"; see web/lib/api-paths.ts)

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
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

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
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: API path mapping + catch-all Route Handler proxy

Files:

  • Create: web/lib/api-paths.ts

  • Create: web/app/api/[...path]/route.ts

  • Step 1: Create web/lib/api-paths.ts

mkdir -p /home/yiekheng/projects/cm_bot_v2/web/lib

Create web/lib/api-paths.ts with the precomputed hashes:

// SHA-256(endpoint).hex[:16]. Deterministic; no salt. Public-boundary only:
// the upstream api-server still uses the readable names. See B1 spec.
export const API_PATHS = {
  acc:        "414322309db5c06d",  // upstream: /acc/
  user:       "04f8996da763b7a9",  // upstream: /user/
  updateAcc:  "982830e2982d95de",  // upstream: /update-acc-data
  updateUser: "f1a25b37d8db494c",  // upstream: /update-user-data
} as const;

// Reverse map for the Route Handler. Keys are the public path segment,
// values are { upstream: string; trailingSlash: boolean }.
export const PUBLIC_TO_UPSTREAM: Record<string, { upstream: string; trailingSlash: boolean }> = {
  [API_PATHS.acc]:        { upstream: "acc",              trailingSlash: true  },
  [API_PATHS.user]:       { upstream: "user",             trailingSlash: true  },
  [API_PATHS.updateAcc]:  { upstream: "update-acc-data",  trailingSlash: false },
  [API_PATHS.updateUser]: { upstream: "update-user-data", trailingSlash: false },
};

Verify the hashes (so future readers can double-check):

.venv/bin/python3 -c "
import hashlib
for endpoint in ['acc', 'user', 'update-acc-data', 'update-user-data']:
    print(f'{endpoint:24s} -> {hashlib.sha256(endpoint.encode()).hexdigest()[:16]}')
"

Expected output (matches the constants above):

acc                      -> 414322309db5c06d
user                     -> 04f8996da763b7a9
update-acc-data          -> 982830e2982d95de
update-user-data         -> f1a25b37d8db494c
  • Step 2: Create the route handler directory
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 3: Write the route handler

Create web/app/api/[...path]/route.ts:

import { NextRequest, NextResponse } from "next/server";
import { PUBLIC_TO_UPSTREAM } from "@/lib/api-paths";

const API_BASE_URL = process.env.API_BASE_URL ?? "http://api-server:3000";

async function forward(
  request: NextRequest,
  path: string[],
): Promise<NextResponse> {
  const hash = path[0];
  const route = PUBLIC_TO_UPSTREAM[hash];
  if (!route) {
    return NextResponse.json({ error: "Not Found" }, { status: 404 });
  }
  const target = `${API_BASE_URL}/${route.upstream}${route.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);
}

Mapped requests (GET /api/414322309db5c06d/ etc.) resolve through PUBLIC_TO_UPSTREAM and forward to api-server:3000/acc/ etc. Unmapped hashes return 404 — they're literally not routes we expose. Network/upstream errors return {"error": <stringified-error>} with HTTP 500, matching cm_web_view.py's shape.

  • Step 3: Build to verify the route compiles
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: Build and verify the route compiles
cd /home/yiekheng/projects/cm_bot_v2/web && \
npm run build 2>&1 | tail -25

Expected: route summary now includes /api/[...path]. No TS errors. The @/lib/api-paths import resolves via the paths mapping in tsconfig.json.

  • Step 5: Commit
cd /home/yiekheng/projects/cm_bot_v2 && \
git add web/lib/api-paths.ts web/app/api/\[...path\]/route.ts && \
git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \
  commit -m "feat(web): hash-encoded API paths + catch-all Route Handler proxy"

Task 4: Multi-stage Dockerfile for cm-web-next

Files:

  • Create: docker/web-next/Dockerfile

  • Step 1: Create the Dockerfile directory and file

mkdir -p /home/yiekheng/projects/cm_bot_v2/docker/web-next

Create docker/web-next/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:

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:

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
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:

  # 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:):

  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
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
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
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
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):

  up)
    "${COMPOSE[@]}" up -d --build mysql api-server web-view web-next
    "${COMPOSE[@]}" ps
    ;;

reset-db case (the inner up invocation):

  reset-db)
    "${COMPOSE[@]}" down --volumes --remove-orphans
    "${COMPOSE[@]}" up -d --build mysql api-server web-view web-next
    ;;

logs case:

  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:

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:

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
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
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
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
cd /home/yiekheng/projects/cm_bot_v2 && \
bash scripts/dev.sh up

Wait ~2535s (mysql healthcheck + npm/Next.js startup). Then:

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
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)
# Old Flask: /api/acc/. New Next.js: /api/<sha256("acc")[:16]>/.
diff \
  <(curl -s http://localhost:8000/api/acc/) \
  <(curl -s http://localhost:8010/api/414322309db5c06d/) \
  && echo "GET parity OK"

Expected: GET parity OK. Both go through to api-server:3000/acc/ and return the same JSON. The hash mapping is the only thing in front of the upstream — same body, same status.

  • Step 4: API proxy parity (POST)
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/982830e2982d95de) \
  && echo "POST parity OK"

Expected: POST parity OK. Both POSTs round-trip through to api-server:3000/update-acc-data.

  • Step 4b: Unmapped hash returns 404
curl -s -o /dev/null -w "code=%{http_code}\n" http://localhost:8010/api/deadbeefdeadbeef/

Expected: code=404. Confirms the proxy is allowlist-based, not pass-through.

  • Step 5: Old cm-web still serves
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 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
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 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
Hash-encoded API paths at the public boundary Task 3
web/lib/api-paths.ts shared mapping for server + client Task 3 step 1
Catch-all proxy at web/app/api/[...path]/route.ts (allowlist-based) Task 3 step 3
Unmapped hash returns 404 (not pass-through) Task 9 step 4b
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.