Add implementation plan for B2+B3 (UI port + PWA)

This commit is contained in:
yiekheng 2026-05-02 20:49:13 +08:00
parent 2de545e854
commit a9642a7121

View File

@ -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<unknown> {
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<Acc[]> {
const data = await fetchApi("/acc/");
return data as Acc[];
}
export async function getUsers(): Promise<User[]> {
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<ActionResult> {
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<ActionResult> {
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 <AccountsTable initial={accounts} prefixPattern={PREFIX_PATTERN} />;
}
```
`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 <UsersTable initial={users} prefixPattern={PREFIX_PATTERN} />;
}
```
- [ ] **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 <html lang="en"> > <body> > <Nav /> > <main>{children}</main>
+ a single <AutoRefresh /> imported from "@/components/auto-refresh".
- Standard root layout structure for App Router; no per-page metadata
here.
File 2: `web/components/nav.tsx` — top nav (Client Component, "use client",
because it uses usePathname() to highlight the active tab).
Required:
- Two links: "Accounts" → "/", "Users" → "/users".
- Uses next/link for client-side navigation.
- Active tab gets a clear visual highlight (matching the dashboard
aesthetic).
- Mobile-friendly: two equal-width links on narrow screens.
- A small product mark on the left ("CM Bot V2" or similar) — purely
decorative, no navigation action.
File 3: `web/app/error.tsx` — top-level error boundary (Client Component,
"use client"; Next.js requires this).
Required:
- Receives { error, reset } props per Next.js's error boundary contract.
- Visual: clear message ("Couldn't reach the API"), explanation that
api-server may be unreachable, a prominent "Retry" button that calls
reset(). Optional: small monospace block showing error.message and
error.digest for the operator's eye.
- Matches the dashboard aesthetic. Keep tone: this is an operator tool
— apologetic copy is wrong; informative is right.
All three files: TypeScript source ready to drop into the paths listed.
Use `@/components/auto-refresh` and `@/components/nav` as imports where
relevant.
```
- [ ] **Step 2: Save the returned files**
Save to:
- `web/app/layout.tsx` (overwrite the B1 scaffold version)
- `web/components/nav.tsx`
- `web/app/error.tsx`
Note the chosen `themeColor` hex — Task 8 hardcodes the same value into `manifest.ts`.
- [ ] **Step 3: Commit**
```bash
cd /home/yiekheng/projects/cm_bot_v2 && \
git add web/app/layout.tsx web/components/nav.tsx web/app/error.tsx && \
git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \
commit -m "feat(web): add layout, nav, and error boundary (frontend-design)"
```
---
## Task 8: Hand-write the PWA manifest
**Files:**
- Create: `web/app/manifest.ts`
- [ ] **Step 1: Write `web/app/manifest.ts`**
Use the `themeColor` hex chosen by frontend-design in Task 7. Replace `#000000` below with the actual value.
```typescript
import type { MetadataRoute } from "next";
export default function manifest(): MetadataRoute.Manifest {
return {
name: "CM Bot V2",
short_name: "CM Bot",
description: "CM Bot account and user dashboard",
start_url: "/",
display: "standalone",
orientation: "portrait",
background_color: "#ffffff",
theme_color: "#000000",
icons: [
{ src: "/icon", sizes: "any", type: "image/png" },
{ src: "/apple-icon", sizes: "180x180", type: "image/png" },
],
};
}
```
Next.js auto-serves this at `/manifest.webmanifest`. The `/icon` and `/apple-icon` URLs are auto-generated from `app/icon.tsx` and `app/apple-icon.tsx` (created in Task 9).
- [ ] **Step 2: Commit**
```bash
cd /home/yiekheng/projects/cm_bot_v2 && \
git add web/app/manifest.ts && \
git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \
commit -m "feat(web): add PWA manifest config"
```
---
## Task 9: Invoke `frontend-design` for the PWA icons
**Files:**
- Create: `web/app/icon.tsx`
- Create: `web/app/apple-icon.tsx`
- [ ] **Step 1: Invoke `frontend-design`**
Use the Skill tool with `skill="frontend-design:frontend-design"` and:
```
Generate two TSX files that produce PWA icons via Next.js 15's
ImageResponse API (from "next/og"). The icons should match the visual
identity of the dashboard frame designed earlier (theme color hex was
<paste the hex chosen in Task 7>; layout is a polished production
dashboard, not the brutalist scaffold).
File 1: `web/app/icon.tsx` — Android home-screen / desktop favicon.
```typescript
import { ImageResponse } from "next/og";
export const size = { width: 512, height: 512 };
export const contentType = "image/png";
export default function Icon() {
return new ImageResponse(
(
// JSX here: a single <div> styled with inline `style={{...}}`
// that contains a brand mark on the chosen theme color
// background. ImageResponse uses Satori, which only supports
// a tiny subset of CSS via inline `style` — no Tailwind, no
// CSS classes, no <main>/<section>; just <div> + <span>.
),
size,
);
}
```
Brand mark requirements:
- The literal text "CM" is the safest bet (the product is "CM Bot V2";
initials read as a logo at small sizes).
- Single color, high contrast against the background.
- Centered, generous padding.
- No font dep — Satori falls back to a system font if none is supplied.
File 2: `web/app/apple-icon.tsx` — iOS home-screen icon.
Same shape as icon.tsx but with `size = { width: 180, height: 180 }`.
iOS does NOT honor the manifest icon list reliably; this file is what
shows up when a user does "Add to Home Screen" on Safari.
Constraints (both files):
- Use ONLY inline `style` (Satori does not support Tailwind classes or
external stylesheets — this is a hard constraint of `ImageResponse`).
- No imports beyond `next/og`'s ImageResponse.
- Keep the JSX tree shallow (a wrapper div + the brand mark).
```
- [ ] **Step 2: Save the returned files**
Save to:
- `web/app/icon.tsx`
- `web/app/apple-icon.tsx`
- [ ] **Step 3: Commit**
```bash
cd /home/yiekheng/projects/cm_bot_v2 && \
git add web/app/icon.tsx web/app/apple-icon.tsx && \
git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \
commit -m "feat(web): add PWA icons via Next.js ImageResponse (frontend-design)"
```
---
## Task 10: Build + smoke verification
**Files:** none modified.
This task is the integration verification. Requires docker compose v2 on the deploy host.
- [ ] **Step 1: Rebuild the cm-web-next image**
```bash
cd /home/yiekheng/projects/cm_bot_v2 && \
sudo docker compose -f docker-compose.yml -f docker-compose.override.yml down --remove-orphans && \
sudo docker compose -f docker-compose.yml -f docker-compose.override.yml build --no-cache web-next 2>&1 | tail -30
```
Expected: a successful build, ending with a "FINISHED" line. If `npm run build` fails inside the container, the error message points to the offending TSX. Common causes:
- frontend-design's TSX uses a dependency we didn't install. Add the dep to `web/package.json`, rebuild.
- Type mismatch between `lib/types.ts` and what a component expects. Reconcile.
- [ ] **Step 2: Bring the dev stack up**
```bash
bash scripts/dev.sh up
```
Wait ~15s for the healthcheck.
```bash
bash scripts/dev.sh status
```
Expected: `OK`.
- [ ] **Step 3: Smoke — `/` renders the accounts table**
```bash
curl -sf http://localhost:8010/ | grep -E "13c1000|13c1011"
```
Expected: hits — the seed accounts from `docker/mysql/init.d/02-seed.sql` are visible in the rendered HTML.
- [ ] **Step 4: Smoke — `/users/` renders the users table**
```bash
curl -sf http://localhost:8010/users/ | grep -E "player_one_seed|player_two_seed"
```
Expected: hits — the seeded user pairings show up.
- [ ] **Step 5: Smoke — manifest is served**
```bash
curl -sf http://localhost:8010/manifest.webmanifest | python3 -c "import json,sys; m=json.load(sys.stdin); print(m['name'], m['display'])"
```
Expected: `CM Bot V2 standalone`.
- [ ] **Step 6: Smoke — icons render to PNG**
```bash
curl -sIf http://localhost:8010/icon | grep -i "content-type"
curl -sIf http://localhost:8010/apple-icon | grep -i "content-type"
```
Expected: both return `Content-Type: image/png`.
- [ ] **Step 7: Smoke — `/api/anything` is still 404**
```bash
curl -sf -o /dev/null -w "code=%{http_code}\n" http://localhost:8010/api/anything/
```
Expected: `code=404`. Confirms the architecture pivot stuck — no public API surface.
- [ ] **Step 8: Manual browser check**
Open `http://localhost:8010/` in a browser:
- Accounts table renders with the 4 available + 2 done seed rows.
- Click an editable cell on row `13c1010`. Change a value. Save. The cell updates instantly. After ~1 second, the page revalidates and the new value persists.
- `mysql -h 127.0.0.1 -u cm -pdevpassword cm -e "SELECT * FROM acc WHERE username='13c1010'"` shows the new value.
- Click "Users" in the nav. Page navigates to `/users/`, shows the two seeded user pairings.
- Wait 30 seconds with the page open. Browser devtools network tab shows an automatic fetch for the RSC payload (the `auto-refresh.tsx` component firing).
- `sudo docker stop dev-cm-api-server`. Refresh the page in the browser. The error boundary renders. Click Retry. Still errors. `sudo docker start dev-cm-api-server`. Retry. Page recovers.
- [ ] **Step 9: PWA install check (browser)**
In Chrome on desktop or Android, the address bar shows an "Install" affordance. Click it; the app opens chromeless with the icon from `app/icon.tsx`. On Safari iOS, "Add to Home Screen" — the home-screen icon shows the apple-icon.
- [ ] **Step 10: Tear down**
```bash
bash scripts/dev.sh down
```
---
## Spec Coverage Check (self-review)
| Spec requirement | Task |
|---|---|
| URL-based tabs (`/`, `/users/`) | Task 6 |
| Server Components fetch from api-server | Task 2 (helpers), Task 6 (page wrappers) |
| Server Actions for mutations + revalidatePath | Task 3 |
| useOptimistic on inline edits | Task 5 (editable-cell) |
| Sortable username with prefix-priority | Task 5 (accounts-table, users-table) |
| Status badge for empty/wait/done | Task 5 (accounts-table) |
| Refresh button | Task 5 (accounts-table, users-table) |
| Auto-refresh every 30s | Task 4 (component), Task 7 (mounted in layout) |
| Mobile-first responsive (collapse below 640px) | Task 5 |
| Error boundary for api-server unreachable | Task 7 (error.tsx) |
| Two-tab nav with active highlight | Task 7 (nav.tsx) |
| `app/manifest.ts` PWA manifest | Task 8 |
| `app/icon.tsx` and `app/apple-icon.tsx` via ImageResponse | Task 9 |
| Theme color consistent between layout metadata and manifest | Tasks 7 & 8 (frontend-design picks color in 7; Task 8 mirrors it) |
| Seeded data renders (acc + user) | Task 10 steps 3-4 |
| No public `/api/*` route | Task 10 step 7 |
| Manifest served at `/manifest.webmanifest` | Task 10 step 5 |
| Icons served as image/png | Task 10 step 6 |
| Inline edit round-trips through Server Action and persists in mysql | Task 10 step 8 |
| Error boundary works when api-server stopped | Task 10 step 8 |
| Auto-refresh observable in network tab | Task 10 step 8 |
| PWA installable in Chrome / Safari | Task 10 step 9 |
No gaps. No placeholders. Function names (`updateAccount`, `updateUser`, `getAccounts`, `getUsers`, `fetchApi`, `AutoRefresh`, `EditableCell`) consistent across tasks. Type names (`Acc`, `User`, `AccUpdate`, `UserUpdate`, `ActionResult`) consistent across tasks.