From a9642a71211e9f4330e4238bf77e16f66533dfcd Mon Sep 17 00:00:00 2001 From: yiekheng Date: Sat, 2 May 2026 20:49:13 +0800 Subject: [PATCH] Add implementation plan for B2+B3 (UI port + PWA) --- .../plans/2026-05-02-b2-b3-ui-port-pwa.md | 747 ++++++++++++++++++ 1 file changed, 747 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-02-b2-b3-ui-port-pwa.md diff --git a/docs/superpowers/plans/2026-05-02-b2-b3-ui-port-pwa.md b/docs/superpowers/plans/2026-05-02-b2-b3-ui-port-pwa.md new file mode 100644 index 0000000..fff728b --- /dev/null +++ b/docs/superpowers/plans/2026-05-02-b2-b3-ui-port-pwa.md @@ -0,0 +1,747 @@ +# 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 > >