748 lines
25 KiB
Markdown
748 lines
25 KiB
Markdown
# 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.
|