The soft reset to 77073ba inadvertently staged deletions of all phase 2
and 3 artifacts. This commit restores them from their source commits so
subsequent task commits build on the complete prior-phase foundation:
- components/mobile/{BottomNav,HeaderBar,KpiCardMobile,MoreDrawer,NeedsAttentionStrip,WorkerStatusRow}
- app/mobile/layout.tsx, dashboard/page.tsx, analyzer/page.tsx
- app/api/mobile/dashboard/route.ts
- All .planning/** files from phases 01-04
- CLAUDE.md, app/layout.tsx, app/styles/brand.css, public/manifest.json
19 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | objective | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 03-dashboard-restyle | 02 | execute | 2 |
|
|
false |
|
Replace the body of /mobile/dashboard so it renders the new spec §6.1 layout: 2×2 KPI grid → "Needs Attention" horizontal strip → worker/backup status row, fed by /api/mobile/dashboard. Drop all recharts/charts and the old by_status/by_queue/by_priority/sla/recent sections. |
|
Purpose: deliver the user-visible Phase 3 outcome — manager opens
/mobile/dashboard, sees four KPIs in a 2×2 grid, a horizontal Needs
Attention strip, and a compact worker/backup status block. No charts.
Output: rewritten app/mobile/dashboard/page.tsx. No new components.
No edits to API routes (already shipped in plan 01).
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/03-dashboard-restyle/03-01-SUMMARY.md @docs/superpowers/specs/2026-05-03-mobile-shell-design.md @CLAUDE.md@app/mobile/dashboard/page.tsx
@app/mobile/layout.tsx
@components/mobile/KpiCardMobile.tsx @components/mobile/NeedsAttentionStrip.tsx @components/mobile/WorkerStatusRow.tsx
@app/api/mobile/dashboard/route.ts
From app/api/mobile/dashboard/route.ts:
export interface KpiResponse {
id: 'open_total' | 'opened_today' | 'resolved_today' | 'sla_breaches';
label: string;
value: number;
caption?: string;
tone?: 'default' | 'attention';
}
export interface AttentionResponse {
id: 'overdue_tickets' | 'failed_backups' | 'stalled_workflows';
label: string;
count: number;
href: string;
}
export interface WorkerResponse {
id: 'analyzer' | 'rmm' | 'backup_success_rate';
label: string;
value: string;
status: 'ok' | 'warn' | 'down';
href: string;
}
export interface MobileDashboardResponse {
kpis: KpiResponse[];
needsAttention: AttentionResponse[];
workers: WorkerResponse[];
}
From components/mobile/KpiCardMobile.tsx:
export function KpiCardMobile(props: { label: string; value: number | string; caption?: string; tone?: 'default'|'attention' }): JSX.Element;
From components/mobile/NeedsAttentionStrip.tsx:
export interface NeedsAttentionItem { id: string; label: string; count: number; href: string }
export function NeedsAttentionStrip(props: { items: NeedsAttentionItem[] }): JSX.Element | null;
From components/mobile/WorkerStatusRow.tsx:
export type WorkerStatus = 'ok' | 'warn' | 'down';
export interface WorkerStatusEntry { id: string; label: string; value: string; status: WorkerStatus; href: string }
export function WorkerStatusRow(props: { entries: WorkerStatusEntry[] }): JSX.Element;
Task 1: Replace mobile dashboard page body with the new 3-section layout
app/mobile/dashboard/page.tsx
- app/mobile/dashboard/page.tsx (current — being completely replaced)
- app/mobile/layout.tsx (confirms header/bottom nav are layout-provided; page renders into )
- app/api/mobile/dashboard/route.ts (response shape source-of-truth)
- components/mobile/KpiCardMobile.tsx (import contract)
- components/mobile/NeedsAttentionStrip.tsx (import contract)
- components/mobile/WorkerStatusRow.tsx (import contract)
- components/mobile/HeaderBar.tsx (page-title pattern — pages render their own H1; header has no title)
- CLAUDE.md (no SWR, no server actions, useState + fetch pattern)
- Page is a `'use client'` component, default export
- On mount, fetches GET /api/mobile/dashboard exactly once and stores the response
- While loading: shows a centered RefreshCw spinner (match existing skeleton pattern)
- On error: shows the error message in a destructive-tinted block + a Retry button that re-runs the fetch
- On success: renders an H1 ("Dashboard"), then 3 sections in this order:
1. 2×2 grid of KpiCardMobile (4 entries from response.kpis), tone derived from `kpi.tone`, caption from `kpi.caption`
2. NeedsAttentionStrip with `items=response.needsAttention` mapped to NeedsAttentionItem
3. WorkerStatusRow with `entries=response.workers` mapped to WorkerStatusEntry
- Header refresh button (RefreshCw icon, top-right of the H1 row) re-runs the fetch
- Page imports zero recharts/chart libraries
Completely replace the contents of `app/mobile/dashboard/page.tsx`. The new file is one self-contained client component plus typed state.
Required structure:
'use client';
/* /mobile/dashboard — phase 03 (DASH-01..04).
*
* Three sections, top-to-bottom:
* 1. 2×2 KPI grid (DASH-01)
* 2. Needs Attention (DASH-02)
* 3. Worker/backup row (DASH-03)
*
* No charts on phone widths (DASH-04). Header + bottom nav are provided
* by app/mobile/layout.tsx; this page only renders the H1 and body. */
import { useEffect, useState } from 'react';
import { RefreshCw } from 'lucide-react';
import { KpiCardMobile } from '@/components/mobile/KpiCardMobile';
import { NeedsAttentionStrip } from '@/components/mobile/NeedsAttentionStrip';
import { WorkerStatusRow } from '@/components/mobile/WorkerStatusRow';
import type { MobileDashboardResponse } from '@/app/api/mobile/dashboard/route';
export default function MobileDashboard() {
const [data, setData] = useState<MobileDashboardResponse | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
async function load() {
setLoading(true);
setError(null);
try {
const r = await fetch('/api/mobile/dashboard');
if (!r.ok) {
const body = (await r.json().catch(() => ({}))) as { error?: string; message?: string };
throw new Error(body.message ?? body.error ?? `HTTP ${r.status}`);
}
setData((await r.json()) as MobileDashboardResponse);
} catch (e) {
setError(e instanceof Error ? e.message : 'Unknown error');
} finally {
setLoading(false);
}
}
useEffect(() => { void load(); }, []);
return (
<div className="p-4 space-y-5">
<div className="flex items-center justify-between">
<h1 className="text-xl font-bold">Dashboard</h1>
<button
type="button"
onClick={load}
disabled={loading}
aria-label="Refresh dashboard"
className="p-2 rounded-full hover:bg-accent disabled:opacity-40"
>
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
</button>
</div>
{error && !loading && (
<div className="rounded-xl border border-destructive/50 bg-destructive/5 p-4">
<p className="text-sm font-medium text-destructive">Failed to load</p>
<p className="text-xs text-muted-foreground mt-1">{error}</p>
<button
type="button"
onClick={load}
className="mt-3 text-xs font-medium text-primary hover:underline"
>
Retry
</button>
</div>
)}
{loading && !data && (
<div className="flex items-center justify-center h-64">
<RefreshCw className="w-6 h-6 animate-spin text-muted-foreground" />
</div>
)}
{data && (
<>
{/* DASH-01: 2×2 KPI grid */}
<div className="grid grid-cols-2 gap-3">
{data.kpis.map(kpi => (
<KpiCardMobile
key={kpi.id}
label={kpi.label}
value={kpi.value}
caption={kpi.caption}
tone={kpi.tone ?? 'default'}
/>
))}
</div>
{/* DASH-02: Needs Attention horizontal strip */}
<NeedsAttentionStrip
items={data.needsAttention.map(a => ({
id: a.id,
label: a.label,
count: a.count,
href: a.href,
}))}
/>
{/* DASH-03: Worker/backup status row */}
<WorkerStatusRow
entries={data.workers.map(w => ({
id: w.id,
label: w.label,
value: w.value,
status: w.status,
href: w.href,
}))}
/>
</>
)}
</div>
);
}
Constraints:
import type { MobileDashboardResponse } from '@/app/api/mobile/dashboard/route'— type-only import is fine in Next.js 16 (the route file marks the export asinterface, no runtime cost). If TypeScript complains about importing types from a route file, fall back to redefining the same shape locally in this file asinterface MobileDashboardResponse { ... }matching the source-of-truth in plan 01's SUMMARY exactly. Either is acceptable.- Do NOT import any of these legacy fields used by the old page:
open_total,by_status,by_queue,by_priority,sla,recent. - Do NOT add a separate "header" — the layout already provides one (HeaderBar in
app/mobile/layout.tsx). The H1 inside the page body is per spec §5.1 ("No page title in the header — pages render their own H1"). - Do NOT introduce recharts, react-day-picker, framer-motion, swr, or react-query. The constraint is strict (DASH-04).
- Do NOT introduce a
next/dynamicimport for charts. Just don't use charts. - Keep the file under ~120 lines. The body should look like the example above, not a re-skin of the old page.
npx tsc --noEmit --pretty 2>&1 | grep "app/mobile/dashboard/page.tsx" || echo "OK: type-check clean"
<acceptance_criteria>
- File starts with
'use client';(verify:head -1 app/mobile/dashboard/page.tsxreturns'use client';) - File has a default export named
MobileDashboard(verify:grep -E "^export default function MobileDashboard" app/mobile/dashboard/page.tsxreturns 1 line) - File imports all three new components (verify:
grep -c "from '@/components/mobile/\(KpiCardMobile\|NeedsAttentionStrip\|WorkerStatusRow\)'" app/mobile/dashboard/page.tsxreturns 3) - File fetches
/api/mobile/dashboard(verify:grep "fetch('/api/mobile/dashboard')" app/mobile/dashboard/page.tsxreturns 1 line) - File contains a
grid-cols-2section for the KPI grid (verify:grep "grid-cols-2" app/mobile/dashboard/page.tsxreturns >= 1 line) - File contains an
<h1>Dashboard</h1>(verify:grep -E "<h1[^>]*>Dashboard</h1>" app/mobile/dashboard/page.tsxreturns 1 line) - File does NOT import recharts/swr/react-query/framer-motion (verify:
grep -E "from 'recharts'|from 'swr'|from '@tanstack/react-query'|from 'framer-motion'" app/mobile/dashboard/page.tsxreturns nothing) - File does NOT contain any of the old field names (verify:
grep -E "by_status|by_queue|by_priority|response_met|resolution_met|PRIORITY_COLOR|PRIORITY_TEXT" app/mobile/dashboard/page.tsxreturns nothing) - File does NOT contain a
<Link>to/mobile/tickets/${...}(the old "Recent Activity" list is gone) (verify:grep "/mobile/tickets/\${" app/mobile/dashboard/page.tsxreturns nothing) - Type-check passes for the page (verify:
npx tsc --noEmit --pretty 2>&1 | grep "app/mobile/dashboard/page.tsx"returns nothing) - File is at most 130 lines (verify:
wc -l app/mobile/dashboard/page.tsxreturns a number <= 130) </acceptance_criteria>/mobile/dashboardrenders the new 3-section layout against the plan-01 API. Type-check clean. No charts.
- File starts with
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| client → server | No new boundaries — page consumes the existing authenticated /api/mobile/dashboard endpoint. |
| /mobile/* → /admin/*, /backup-status, /tickets | All link destinations are existing authenticated routes; Better Auth middleware enforces session on the destination. |
STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|---|---|---|---|---|
| T-03-06 | Elevation of Privilege | Tile/card click destinations | accept | All href values are emitted by the server endpoint (plan 01) and rendered as next/link. The client cannot influence destinations beyond what the server returned, and Better Auth middleware enforces the session/role required for each destination route. No new privilege boundary. |
| T-03-07 | Information Disclosure | Error rendering | mitigate | Server errors are surfaced via body.message ?? body.error ?? 'HTTP {status}'. No stack trace or DB schema is rendered. The endpoint only returns sanitized { error, message } per CLAUDE.md convention. |
| T-03-08 | Information Disclosure | Empty/zero counts | accept | Counts of 0 are rendered as "0" rather than hidden. This is intentional — a manager seeing "0 overdue tickets" is the desired signal. No PII surfaced. |
| </threat_model> |
<success_criteria>
/mobile/dashboardpage renders 2×2 KPI grid, Needs Attention strip, worker/backup status row in that order- All Needs Attention cards and worker rows are tappable links to the documented destinations
- No charts/recharts on the page (DASH-04)
- Type-check passes
- Human verification approves the layout (Task 2) </success_criteria>