# B2+B3: UI Port + PWA 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:** Port the legacy Flask `cm_web_view.py` UI to Next.js (RSC reads + Server Actions for inline edits, two URL-based tabs) and ship the PWA manifest + icons so operators can install on their phones. **Architecture:** Hand-write the glue (types, fetch helpers, Server Actions, manifest, page wrappers, auto-refresh) and invoke `frontend-design` three times for the UI code (data tables, frame, icons). No Docker/compose changes — `COPY web/ ./` already bundles the new files. **Tech Stack:** Next.js 15 (App Router) + React 19 (Server Components, Server Actions, `useOptimistic`) + Tailwind v4. `next/og`'s `ImageResponse` renders the PWA icons at build time — no external SVG-to-PNG tooling. **Spec:** [docs/superpowers/specs/2026-05-02-b2-b3-ui-port-pwa-design.md](../specs/2026-05-02-b2-b3-ui-port-pwa-design.md) --- ## File Map | File | Operation | Owner | Depends on | |---|---|---|---| | `web/lib/types.ts` | Create | hand | — | | `web/lib/api.ts` | Create | hand | `lib/types` | | `web/app/actions.ts` | Create | hand | `lib/api`, `lib/types` | | `web/components/auto-refresh.tsx` | Create | hand | — | | `web/components/editable-cell.tsx` | Create | frontend-design | — | | `web/components/accounts-table.tsx` | Create | frontend-design | `lib/types`, `actions`, `editable-cell` | | `web/components/users-table.tsx` | Create | frontend-design | `lib/types`, `actions`, `editable-cell` | | `web/components/nav.tsx` | Create | frontend-design | — | | `web/app/layout.tsx` | Rewrite | frontend-design | `nav`, `auto-refresh` | | `web/app/error.tsx` | Create | frontend-design | — | | `web/app/page.tsx` | Rewrite | hand | `lib/api`, `accounts-table` | | `web/app/users/page.tsx` | Create | hand | `lib/api`, `users-table` | | `web/app/manifest.ts` | Create | hand | — | | `web/app/icon.tsx` | Create | frontend-design | — | | `web/app/apple-icon.tsx` | Create | frontend-design | — | Three `frontend-design` invocations, scoped: 1. **Data primitives** — `editable-cell.tsx`, `accounts-table.tsx`, `users-table.tsx` (Task 5). 2. **Frame** — `layout.tsx`, `nav.tsx`, `error.tsx` (Task 7). 3. **Icons** — `icon.tsx`, `apple-icon.tsx` (Task 9). Each invocation gets a focused brief so the output stays coherent. --- ## Task 1: Hand-write types **Files:** - Create: `web/lib/types.ts` - [ ] **Step 1: Create the directory** ```bash mkdir -p /home/yiekheng/projects/cm_bot_v2/web/lib ``` - [ ] **Step 2: Write `web/lib/types.ts`** ```typescript // Mirrors the SQL schema in docker/mysql/init.d/01-schema.sql and the // JSON projection from app/cm_api.py's get_account / get_user routes. export type Acc = { username: string; password: string; status: string; link: string; }; export type User = { f_username: string; f_password: string; t_username: string; t_password: string; last_update_time: string | null; }; export type AccUpdate = Acc; export type UserUpdate = Pick< User, "f_username" | "f_password" | "t_username" | "t_password" >; ``` - [ ] **Step 3: Commit** ```bash cd /home/yiekheng/projects/cm_bot_v2 && \ git add web/lib/types.ts && \ git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \ commit -m "feat(web): add TypeScript types for Acc and User" ``` --- ## Task 2: Hand-write the api-server fetch helper **Files:** - Create: `web/lib/api.ts` - [ ] **Step 1: Write `web/lib/api.ts`** ```typescript import type { Acc, User } from "./types"; const API_BASE_URL = process.env.API_BASE_URL ?? "http://api-server:3000"; type FetchInit = { method?: "GET" | "POST"; body?: unknown; cache?: RequestCache; }; export async function fetchApi(path: string, options: FetchInit = {}): Promise { const url = `${API_BASE_URL}${path}`; const init: RequestInit = { method: options.method ?? "GET", cache: options.cache ?? "no-store", headers: options.body ? { "content-type": "application/json" } : undefined, body: options.body ? JSON.stringify(options.body) : undefined, }; const res = await fetch(url, init); if (!res.ok) { throw new Error(`API ${options.method ?? "GET"} ${path} failed: ${res.status}`); } return res.json(); } export async function getAccounts(): Promise { const data = await fetchApi("/acc/"); return data as Acc[]; } export async function getUsers(): Promise { const data = await fetchApi("/user/"); return data as User[]; } ``` `cache: "no-store"` is critical — RSC's default caching would stale the dashboard between page loads. - [ ] **Step 2: Commit** ```bash cd /home/yiekheng/projects/cm_bot_v2 && \ git add web/lib/api.ts && \ git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \ commit -m "feat(web): add server-side api-server fetch helper" ``` --- ## Task 3: Hand-write Server Actions **Files:** - Create: `web/app/actions.ts` - [ ] **Step 1: Write `web/app/actions.ts`** ```typescript "use server"; import { revalidatePath } from "next/cache"; import { fetchApi } from "@/lib/api"; import type { AccUpdate, UserUpdate } from "@/lib/types"; export type ActionResult = { ok: true } | { ok: false; error: string }; export async function updateAccount(data: AccUpdate): Promise { try { await fetchApi("/update-acc-data", { method: "POST", body: data }); revalidatePath("/"); return { ok: true }; } catch (err) { return { ok: false, error: err instanceof Error ? err.message : String(err) }; } } export async function updateUser(data: UserUpdate): Promise { try { await fetchApi("/update-user-data", { method: "POST", body: data }); revalidatePath("/users"); return { ok: true }; } catch (err) { return { ok: false, error: err instanceof Error ? err.message : String(err) }; } } ``` The `"use server"` directive at the top makes these functions invocable from client components via Next.js's Server Actions wire format. `revalidatePath` re-runs the matching page's RSC fetch and patches the result in. - [ ] **Step 2: Commit** ```bash cd /home/yiekheng/projects/cm_bot_v2 && \ git add web/app/actions.ts && \ git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \ commit -m "feat(web): add updateAccount and updateUser Server Actions" ``` --- ## Task 4: Hand-write the auto-refresh client component **Files:** - Create: `web/components/auto-refresh.tsx` - [ ] **Step 1: Create the directory and file** ```bash mkdir -p /home/yiekheng/projects/cm_bot_v2/web/components ``` Create `web/components/auto-refresh.tsx`: ```typescript "use client"; import { useRouter } from "next/navigation"; import { useEffect } from "react"; /** * Mounts a setInterval that calls router.refresh() every `intervalMs`. * router.refresh() re-runs the matching Server Component fetch and * patches the rendered output in — no full page reload, no flicker. * * Renders nothing. */ export default function AutoRefresh({ intervalMs = 30_000, }: { intervalMs?: number; }) { const router = useRouter(); useEffect(() => { const id = setInterval(() => router.refresh(), intervalMs); return () => clearInterval(id); }, [router, intervalMs]); return null; } ``` - [ ] **Step 2: Commit** ```bash cd /home/yiekheng/projects/cm_bot_v2 && \ git add web/components/auto-refresh.tsx && \ git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \ commit -m "feat(web): add 30s auto-refresh client component" ``` --- ## Task 5: Invoke `frontend-design` for the data primitives **Files:** - Create: `web/components/editable-cell.tsx` - Create: `web/components/accounts-table.tsx` - Create: `web/components/users-table.tsx` - [ ] **Step 1: Invoke the `frontend-design` skill** Use the Skill tool with `skill="frontend-design:frontend-design"` and the following brief: ``` Generate three React Client Components for a Next.js 15 + Tailwind v4 internal CRUD dashboard. The dashboard lives at `web/components/` and ports a legacy Flask UI (gradient purple, FontAwesome, inline cell editing) to a polished modern look that reads as a real production dashboard. The previous scaffold used a brutalist hazard-tape aesthetic; THIS UI is the production replacement and should look distinct from the scaffold while staying in the same design family if possible (or pivot fully to a new direction if that serves the data better). Constraints — every file: - "use client" at the top. - Tailwind v4 utility classes only — no external font, image, or icon deps. - TypeScript with strict types from `@/lib/types`. - Mobile-first responsive: tables collapse to card stacks below 640px. - Server Actions imported from `@/app/actions` for mutations. - React 19 useOptimistic for inline-edit feedback (instant local update, reverts on Server Action failure). - No external state libraries — useState / useOptimistic / useTransition only. File 1: `web/components/editable-cell.tsx` Generic primitive: a span/td that turns into a text input on click, saves on Enter / Save button, cancels on Escape / Cancel button. Props: type EditableCellProps = { value: string; onSave: (next: string) => Promise<{ ok: boolean; error?: string }>; label?: string; // accessibility label, e.g. "edit password" isCurrentlyEditing?: boolean; onEditStart?: () => void; onEditEnd?: () => void; }; Behavior: - Click → input, focus + select all. - Enter or Save button → calls onSave, optimistically updates display immediately, reverts on { ok: false } and shows the error inline for a few seconds. - Escape or Cancel button → restores original value. - Hover affordance (subtle visual cue that the cell is editable). - isCurrentlyEditing/onEditStart/onEditEnd let a parent table track which cell is in edit mode (so auto-refresh can pause). File 2: `web/components/accounts-table.tsx` Renders the Accounts dashboard tab content. Receives initial data from the server component: type Props = { initial: Acc[]; prefixPattern: string }; Renders: - A stats header (count of total accounts). - A refresh button (calls router.refresh() from next/navigation). - A sortable table: - Username column: sortable. Sort logic: rows whose username starts with `prefixPattern` always sort to the top; within each group, sort descending by username string. - Password, Status, Link: editable via EditableCell, calling the `updateAccount` Server Action with the full Acc object on save. - Status column shows a colored badge for empty/wait/done states (three visually distinct variants). - Below 640px: each row collapses to a card stack with labeled rows. - Empty state when initial.length === 0: a clear "no accounts" message. File 3: `web/components/users-table.tsx` Renders the Users dashboard tab content. Receives: type Props = { initial: User[]; prefixPattern: string }; Same shape as accounts-table but for the User type: - Sortable username column with the prefix-priority logic on user.f_username. - Then sorted by last_update_time descending (newest first) within each group. - f_password, t_username, t_password are editable cells that call updateUser Server Action. - last_update_time is read-only. - f_username is read-only (it's the primary key). All three files: return TypeScript source ready to drop into the listed paths. Imports use `@/lib/types`, `@/app/actions`, and `next/navigation` as appropriate. ``` - [ ] **Step 2: Save the returned files** The skill returns three TSX files. Save them to: - `web/components/editable-cell.tsx` - `web/components/accounts-table.tsx` - `web/components/users-table.tsx` - [ ] **Step 3: Commit** ```bash cd /home/yiekheng/projects/cm_bot_v2 && \ git add web/components/editable-cell.tsx web/components/accounts-table.tsx web/components/users-table.tsx && \ git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \ commit -m "feat(web): add data tables and editable-cell primitive (frontend-design)" ``` --- ## Task 6: Hand-write page wrappers (RSC entry points) **Files:** - Rewrite: `web/app/page.tsx` - Create: `web/app/users/page.tsx` - [ ] **Step 1: Replace `web/app/page.tsx`** The current file is the B1 brutalist scaffold. Replace it entirely with the accounts entry-point wrapper: ```typescript import { getAccounts } from "@/lib/api"; import AccountsTable from "@/components/accounts-table"; const PREFIX_PATTERN = process.env.NEXT_PUBLIC_CM_PREFIX_PATTERN ?? "13c"; export default async function AccountsPage() { const accounts = await getAccounts(); return ; } ``` `PREFIX_PATTERN` reads from `NEXT_PUBLIC_CM_PREFIX_PATTERN` (which gets baked into the client bundle since `NEXT_PUBLIC_` prefixed env vars are exposed to the browser). The default `13c` matches `envs/dev/.env.example`'s `CM_PREFIX_PATTERN`. - [ ] **Step 2: Create `web/app/users/page.tsx`** ```bash mkdir -p /home/yiekheng/projects/cm_bot_v2/web/app/users ``` Then: ```typescript import { getUsers } from "@/lib/api"; import UsersTable from "@/components/users-table"; const PREFIX_PATTERN = process.env.NEXT_PUBLIC_CM_PREFIX_PATTERN ?? "13c"; export default async function UsersPage() { const users = await getUsers(); return ; } ``` - [ ] **Step 3: Commit** ```bash cd /home/yiekheng/projects/cm_bot_v2 && \ git add web/app/page.tsx web/app/users/page.tsx && \ git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \ commit -m "feat(web): add Server Component entry pages for / and /users" ``` --- ## Task 7: Invoke `frontend-design` for the frame (layout, nav, error) **Files:** - Rewrite: `web/app/layout.tsx` - Create: `web/components/nav.tsx` - Create: `web/app/error.tsx` - [ ] **Step 1: Invoke `frontend-design`** Use the Skill tool with `skill="frontend-design:frontend-design"` and: ``` Generate three TSX files for a Next.js 15 + Tailwind v4 internal dashboard. The dashboard has two URL-based tabs (`/` for Accounts, `/users` for Users). The visual identity should match the data-table components already designed (accounts-table.tsx, users-table.tsx) — they use a polished production-dashboard aesthetic, distinct from the brutalist hazard-tape scaffold that preceded them. File 1: `web/app/layout.tsx` — root layout (Server Component). Required: - Imports `./globals.css`. - `export const metadata` includes `title: "CM Bot V2"` and a `themeColor` matching the chosen aesthetic (the value also goes into `web/app/manifest.ts` separately — pick a single hex and document it so I can mirror it). - Renders > >