Переглянути джерело

docs: add MailHub UI redesign implementation plan

Task-by-task plan for shared theme tokens, shell/auth restyle, shared
presentation components, dashboard/domain pages, and remaining page sweep.

AI-Co-Authored-By: Grok
chendeben 1 місяць тому
батько
коміт
a16a94b619
1 змінених файлів з 678 додано та 0 видалено
  1. 678 0
      docs/superpowers/plans/2026-07-09-mailhub-ui-redesign.md

+ 678 - 0
docs/superpowers/plans/2026-07-09-mailhub-ui-redesign.md

@@ -0,0 +1,678 @@
+# MailHub UI Visual Redesign 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:** Upgrade MailHub admin and auth UIs to a Modern SaaS visual system (indigo primary, dark sidebar, comfortable density) without changing APIs or business logic.
+
+**Architecture:** Extract a shared theme module used by both Vite entry points (`App.tsx` and `auth/main.tsx`). Redesign `AdminLayout` and auth shell, add shared presentation components under `src/components/common/`, then restyle pages by composition only. Keep handlers, models, and routes unchanged.
+
+**Tech Stack:** React 19, Ant Design 5, Vite 8, TypeScript, existing i18n (`src/frontend/i18n`), Node test suite (`npm test`)
+
+**Spec:** `docs/superpowers/specs/2026-07-09-mailhub-ui-redesign-design.md`
+
+---
+
+## File map
+
+| File | Responsibility |
+|------|----------------|
+| Create `src/frontend/theme.ts` | Shared Ant Design theme config + exported color constants for plots/CSS alignment |
+| Modify `src/frontend/styles.css` | CSS variables, shell, cards, pills, auth, layout polish |
+| Modify `src/frontend/App.tsx` | Import shared theme into admin `ConfigProvider` |
+| Modify `src/frontend/auth/main.tsx` | Import shared theme into auth `ConfigProvider` |
+| Modify `src/layouts/AdminLayout.tsx` | Grouped nav, brand mark, header hierarchy, optional sider footer |
+| Modify `src/frontend/auth/AuthApp.tsx` | Auth shell markup/classes only (no auth API changes) |
+| Create `src/components/common/PageHeader.tsx` | Title / subtitle / actions |
+| Create `src/components/common/MetricCard.tsx` | Primary KPI card |
+| Create `src/components/common/SectionCard.tsx` | Standard elevated content card wrapper |
+| Create `src/components/common/StatusPill.tsx` | Soft semantic status chip |
+| Create `src/components/common/EmptyState.tsx` | Empty list/chart state |
+| Create `src/components/common/CodeBlock.tsx` | Wrap-safe mono value + optional copy |
+| Modify `src/components/common/StatusTag.tsx` | Prefer StatusPill styling (or thin wrapper) |
+| Modify `src/pages/Dashboard.tsx` | 4 MetricCards + secondary metrics + token chart colors |
+| Modify `src/pages/Domains/index.tsx` | PageHeader + SectionCard table chrome |
+| Modify `src/pages/Domains/DomainDetail.tsx` | Section wrappers / tab chrome |
+| Modify `src/components/domain/DomainHealthCard.tsx` | Hero health block |
+| Modify `src/components/domain/DnsRecordCard.tsx` | CodeBlock + StatusPill |
+| Modify remaining pages | PageHeader + SectionCard sweep |
+| Modify `src/frontend/i18n/index.js` | Nav group + minor chrome strings (zh-CN + en-US) |
+| Modify `test/frontend-i18n.test.js` | Assert new i18n keys if added |
+| Create `test/frontend-theme.test.js` | Assert primary token is indigo not Ant default blue |
+
+---
+
+### Task 1: Shared theme module + CSS variables
+
+**Files:**
+- Create: `src/frontend/theme.ts`
+- Modify: `src/frontend/styles.css` (top of file: `:root` tokens; keep existing class hooks, retarget colors)
+- Modify: `src/frontend/App.tsx` (ConfigProvider theme import)
+- Modify: `src/frontend/auth/main.tsx` (ConfigProvider theme import)
+- Create: `test/frontend-theme.test.js`
+
+- [ ] **Step 1: Write the failing theme test**
+
+```js
+// test/frontend-theme.test.js
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { mailhubTheme, brandColors } from '../src/frontend/theme.ts';
+
+test('brand primary is indigo, not Ant Design default blue', () => {
+  assert.equal(brandColors.primary, '#4F46E5');
+  assert.notEqual(brandColors.primary.toLowerCase(), '#1677ff');
+  assert.equal(mailhubTheme.token.colorPrimary, brandColors.primary);
+});
+
+test('layout canvas and ink tokens match redesign spec', () => {
+  assert.equal(brandColors.canvas, '#F4F6FB');
+  assert.equal(brandColors.ink, '#0F172A');
+  assert.equal(mailhubTheme.token.colorBgLayout, brandColors.canvas);
+});
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `node --test test/frontend-theme.test.js`  
+Expected: FAIL (module not found or exports missing)
+
+- [ ] **Step 3: Implement `src/frontend/theme.ts`**
+
+```ts
+import type { ThemeConfig } from 'antd';
+
+export const brandColors = {
+  primary: '#4F46E5',
+  primaryHover: '#4338CA',
+  primarySoft: '#EEF2FF',
+  ink: '#0F172A',
+  textSecondary: '#64748B',
+  textMuted: '#94A3B8',
+  canvas: '#F4F6FB',
+  surface: '#FFFFFF',
+  border: '#E2E8F0',
+  success: '#16A34A',
+  warning: '#D97706',
+  danger: '#DC2626',
+  chartPrimary: '#4F46E5',
+  chartSuccess: '#16A34A',
+  chartDanger: '#DC2626',
+  chartWarning: '#D97706',
+  chartTrack: '#DBEAFE'
+} as const;
+
+export const mailhubTheme: ThemeConfig = {
+  token: {
+    colorPrimary: brandColors.primary,
+    colorSuccess: brandColors.success,
+    colorWarning: brandColors.warning,
+    colorError: brandColors.danger,
+    colorBgLayout: brandColors.canvas,
+    colorBgContainer: brandColors.surface,
+    colorBorderSecondary: brandColors.border,
+    colorText: brandColors.ink,
+    colorTextSecondary: brandColors.textSecondary,
+    borderRadius: 10,
+    borderRadiusLG: 14,
+    fontFamily:
+      'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
+  },
+  components: {
+    Card: {
+      borderRadiusLG: 14
+    },
+    Table: {
+      cellPaddingBlock: 14,
+      cellPaddingInline: 16
+    },
+    Button: {
+      controlHeight: 36,
+      borderRadius: 10
+    },
+    Menu: {
+      darkItemBg: brandColors.ink,
+      darkSubMenuItemBg: brandColors.ink,
+      darkItemSelectedBg: 'rgba(79, 70, 229, 0.22)',
+      darkItemSelectedColor: '#E0E7FF',
+      darkItemHoverBg: 'rgba(255, 255, 255, 0.06)',
+      itemBorderRadius: 10
+    }
+  }
+};
+```
+
+- [ ] **Step 4: Add CSS variables at top of `src/frontend/styles.css` and retarget globals**
+
+```css
+:root {
+  --mh-primary: #4f46e5;
+  --mh-primary-hover: #4338ca;
+  --mh-primary-soft: #eef2ff;
+  --mh-ink: #0f172a;
+  --mh-text-secondary: #64748b;
+  --mh-text-muted: #94a3b8;
+  --mh-canvas: #f4f6fb;
+  --mh-surface: #ffffff;
+  --mh-border: #e2e8f0;
+  --mh-success: #16a34a;
+  --mh-warning: #d97706;
+  --mh-danger: #dc2626;
+  --mh-radius-control: 10px;
+  --mh-radius-card: 14px;
+  --mh-shadow-card: 0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.04);
+}
+
+body {
+  margin: 0;
+  background: var(--mh-canvas);
+  color: var(--mh-ink);
+  font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+}
+```
+
+Replace hard-coded `#1677ff` / `#f5f7fb` / `#111827` shell colors with the CSS variables as you touch those rules (full shell polish in Task 3).
+
+- [ ] **Step 5: Wire shared theme in both entry ConfigProviders**
+
+In `App.tsx`, replace inline theme object with:
+
+```tsx
+import { mailhubTheme } from './theme';
+// ...
+<ConfigProvider theme={mailhubTheme}>
+```
+
+In `auth/main.tsx`:
+
+```tsx
+import { mailhubTheme } from '../theme';
+// ...
+<ConfigProvider theme={mailhubTheme}>
+```
+
+Ensure auth still imports `../styles.css`.
+
+- [ ] **Step 6: Run theme test + full suite**
+
+Run: `node --test test/frontend-theme.test.js && npm test`  
+Expected: theme tests PASS; full suite PASS
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add src/frontend/theme.ts src/frontend/styles.css src/frontend/App.tsx src/frontend/auth/main.tsx test/frontend-theme.test.js
+git commit -m "feat(ui): add shared MailHub indigo theme tokens"
+```
+
+---
+
+### Task 2: i18n chrome strings for nav groups
+
+**Files:**
+- Modify: `src/frontend/i18n/index.js`
+- Modify: `test/frontend-i18n.test.js`
+
+- [ ] **Step 1: Extend i18n test for new keys**
+
+Add assertions (both locales via existing helpers if present):
+
+```js
+// Use existing createTranslator helpers from test/frontend-i18n.test.js
+// (not bare zh()/en() — those may not exist).
+test('nav group chrome strings exist', () => {
+  const zh = createTranslator('zh-CN');
+  const en = createTranslator('en-US');
+  assert.equal(zh('nav.group.overview'), '概览');
+  assert.equal(zh('nav.group.delivery'), '投递');
+  assert.equal(zh('nav.group.system'), '系统');
+  assert.equal(en('nav.group.overview'), 'Overview');
+  assert.equal(en('nav.group.delivery'), 'Delivery');
+  assert.equal(en('nav.group.system'), 'System');
+});
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `node --test test/frontend-i18n.test.js`  
+Expected: FAIL missing keys
+
+- [ ] **Step 3: Add keys to `zh-CN` and `en-US` message maps**
+
+```js
+// zh-CN
+'nav.group.overview': '概览',
+'nav.group.delivery': '投递',
+'nav.group.system': '系统',
+
+// en-US
+'nav.group.overview': 'Overview',
+'nav.group.delivery': 'Delivery',
+'nav.group.system': 'System',
+```
+
+(Only add keys that do not already exist. Keep existing `nav.*` page labels.)
+
+- [ ] **Step 4: Run i18n tests**
+
+Run: `node --test test/frontend-i18n.test.js`  
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/frontend/i18n/index.js test/frontend-i18n.test.js
+git commit -m "feat(i18n): add sidebar nav group labels"
+```
+
+---
+
+### Task 3: AdminLayout shell redesign
+
+**Files:**
+- Modify: `src/layouts/AdminLayout.tsx`
+- Modify: `src/frontend/styles.css` (`.admin-layout`, `.admin-sider`, `.brand*`, `.admin-header`, `.admin-content`, menu active styles)
+
+- [ ] **Step 1: Restructure nav data with groups**
+
+```tsx
+const navGroups: Array<{
+  key: string;
+  labelKey: string;
+  items: Array<{ key: ViewKey; labelKey: string; icon: ReactNode; adminOnly?: boolean }>;
+}> = [
+  {
+    key: 'overview',
+    labelKey: 'nav.group.overview',
+    items: [
+      { key: 'dashboard', labelKey: 'nav.dashboard', icon: <DashboardOutlined /> },
+      { key: 'domains', labelKey: 'nav.domains', icon: <GlobalOutlined /> },
+      { key: 'dns-api', labelKey: 'nav.dnsApi', icon: <CloudServerOutlined /> }
+    ]
+  },
+  {
+    key: 'delivery',
+    labelKey: 'nav.group.delivery',
+    items: [
+      { key: 'smtp', labelKey: 'nav.smtp', icon: <MailOutlined /> },
+      { key: 'tokens', labelKey: 'nav.tokens', icon: <KeyOutlined /> },
+      { key: 'logs', labelKey: 'nav.logs', icon: <SendOutlined /> },
+      { key: 'webhooks', labelKey: 'nav.webhooks', icon: <ApiOutlined /> }
+    ]
+  },
+  {
+    key: 'system',
+    labelKey: 'nav.group.system',
+    items: [
+      { key: 'admin', labelKey: 'nav.admin', icon: <SafetyCertificateOutlined />, adminOnly: true },
+      { key: 'settings', labelKey: 'nav.settings', icon: <SettingOutlined /> }
+    ]
+  }
+];
+```
+
+Build Ant Design `Menu` `items` as group entries (`type: 'group'`) filtering `adminOnly` when `user?.role !== 'admin'`.
+
+- [ ] **Step 2: Upgrade brand + header markup**
+
+- Brand logo: gradient indigo/violet background (`brand-logo` class), white MH text
+- Header left: keep breadcrumb; ensure title hierarchy readable at comfortable size
+- Header right: language, refresh (default), primary add-domain, user dropdown
+- Optional: compact user chip in sider footer (display only; logout stays in header dropdown)
+
+- [ ] **Step 3: CSS for modern sider**
+
+```css
+.admin-sider {
+  background: var(--mh-ink) !important;
+  border-right: 1px solid rgba(255, 255, 255, 0.06);
+  /* sticky full-height behavior already present — keep it */
+}
+
+.brand-logo {
+  background: linear-gradient(135deg, #6366f1, #8b5cf6);
+  color: #fff;
+  border-radius: 10px;
+}
+
+.admin-header {
+  background: var(--mh-surface);
+  border-bottom: 1px solid var(--mh-border);
+  min-height: 64px;
+  padding: 12px 24px;
+}
+
+.admin-content {
+  background: var(--mh-canvas);
+  padding: 28px 28px 40px;
+}
+
+.ant-card {
+  border-color: var(--mh-border);
+  box-shadow: var(--mh-shadow-card);
+}
+```
+
+Tune Menu dark styles so selected item matches indigo soft wash (override if Ant Menu tokens are insufficient).
+
+- [ ] **Step 4: Manual visual smoke (dev)**
+
+Run: `npm run dev:ui` (and `npm run dev` if API needed)  
+Check: sidebar groups, active item color indigo-tint, header CTA primary indigo, no `#1677ff` flash.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/layouts/AdminLayout.tsx src/frontend/styles.css
+git commit -m "feat(ui): redesign admin shell with grouped dark sidebar"
+```
+
+---
+
+### Task 4: Auth shell restyle
+
+**Files:**
+- Modify: `src/frontend/auth/AuthApp.tsx` (classNames / structure only)
+- Modify: `src/frontend/styles.css` (`.auth-*` rules)
+
+- [ ] **Step 1: Align auth brand panel with tokens**
+
+Keep split layout and modes. Update:
+
+- Brand panel bg → `var(--mh-ink)`
+- Logo → same gradient as admin
+- Form card → `var(--mh-radius-card)` (14px), `var(--mh-shadow-card)`, comfortable padding
+- Primary button inherits theme primary
+- Soft right panel gradient using primary-soft + canvas
+
+Do **not** change `submit` / fetch paths / mode state machine.
+
+- [ ] **Step 2: CSS refresh for auth**
+
+Retarget `.auth-page`, `.auth-brand-panel`, `.auth-logo`, `.auth-signal-item`, `.auth-card`, `.auth-eyebrow` to CSS variables and slightly larger radii.
+
+- [ ] **Step 3: Smoke login page**
+
+Run: open Vite login entry or built `login.html`  
+Expected: indigo primary, consistent brand with admin, forms still switch login/register
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/frontend/auth/AuthApp.tsx src/frontend/styles.css
+git commit -m "feat(ui): restyle auth shell to match admin brand"
+```
+
+---
+
+### Task 5: Shared presentation components
+
+**Files:**
+- Create: `src/components/common/PageHeader.tsx`
+- Create: `src/components/common/MetricCard.tsx`
+- Create: `src/components/common/SectionCard.tsx`
+- Create: `src/components/common/StatusPill.tsx`
+- Create: `src/components/common/EmptyState.tsx`
+- Create: `src/components/common/CodeBlock.tsx`
+- Modify: `src/components/common/StatusTag.tsx` (render via StatusPill when `mode === 'tag'`)
+- Modify: `src/frontend/styles.css` (component classes)
+
+- [ ] **Step 1: Implement StatusPill + styles**
+
+```tsx
+// StatusPill.tsx — tone: success | warning | error | info | neutral
+import { Typography } from 'antd';
+import type { ReactNode } from 'react';
+
+export type StatusTone = 'success' | 'warning' | 'error' | 'info' | 'neutral';
+
+export function StatusPill({ tone = 'neutral', icon, children }: {
+  tone?: StatusTone;
+  icon?: ReactNode;
+  children: ReactNode;
+}) {
+  return (
+    <span className={`status-pill status-pill--${tone}`}>
+      {icon}
+      <span>{children}</span>
+    </span>
+  );
+}
+```
+
+```css
+.status-pill {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  border-radius: 999px;
+  padding: 3px 10px;
+  font-size: 12px;
+  font-weight: 600;
+  border: 1px solid transparent;
+}
+.status-pill--success { background: #ecfdf5; color: #15803d; border-color: #bbf7d0; }
+.status-pill--warning { background: #fffbeb; color: #b45309; border-color: #fde68a; }
+.status-pill--error { background: #fef2f2; color: #b91c1c; border-color: #fecaca; }
+.status-pill--info { background: var(--mh-primary-soft); color: #4338ca; border-color: #c7d2fe; }
+.status-pill--neutral { background: #f8fafc; color: #475569; border-color: var(--mh-border); }
+```
+
+Map existing domain/record status colors to tones in StatusTag.
+
+- [ ] **Step 2: Implement PageHeader, MetricCard, SectionCard, EmptyState, CodeBlock**
+
+Minimal APIs:
+
+```tsx
+// PageHeader
+{ title: ReactNode; subtitle?: ReactNode; extra?: ReactNode }
+
+// MetricCard
+{ label: ReactNode; value: ReactNode; hint?: ReactNode; tone?: 'default' | 'warning' | 'danger' }
+
+// SectionCard — wrap antd Card with className="section-card" + consistent props
+{ title?: ReactNode; extra?: ReactNode; children: ReactNode; className?: string }
+
+// EmptyState
+{ description: ReactNode; action?: ReactNode; icon?: ReactNode }
+
+// CodeBlock
+{ value: string; onCopy?: (value: string) => void }
+```
+
+Use Ant Design `Card` / `Typography` / `Button` underneath where helpful; keep components presentational.
+
+- [ ] **Step 3: Point StatusTag tag mode at StatusPill**
+
+Preserve badge mode behavior. Ensure `getRecordStatusMeta` mapping still works.
+
+- [ ] **Step 4: Typecheck**
+
+Run: `npx tsc --noEmit`  
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/components/common src/frontend/styles.css
+git commit -m "feat(ui): add shared presentation components for redesign"
+```
+
+---
+
+### Task 6: Dashboard restyle
+
+**Files:**
+- Modify: `src/pages/Dashboard.tsx`
+- Modify: `src/frontend/styles.css` (metric/chart helpers if needed)
+
+- [ ] **Step 1: Replace 8 equal metric cards with 4 MetricCards**
+
+Primary cards (order fixed per spec):
+
+1. `t('dashboard.todaySent')` → `summary.today`
+2. `t('dashboard.successRate')` → `${summary.successRate}%` with hint showing bounce + complaint rates
+3. `t('dashboard.verifiedDomains')` → `summary.verifiedDomains`
+4. `t('dashboard.dnsIssues')` → `summary.dnsIssues` (warning tone when > 0)
+
+Secondary placement:
+
+- SMTP status → compact tag/chip near PageHeader extra or above charts
+- Last sent → `SectionCard` extra/meta on recent logs card
+
+Keep default-password `Alert`.
+
+- [ ] **Step 2: Wrap charts and lists in SectionCard; recolor plots**
+
+Import `brandColors` from `../frontend/theme` and recolor **all** plots:
+
+```ts
+// Area trend (total / accepted / failed series)
+scale={{ color: { range: [brandColors.chartPrimary, brandColors.chartSuccess, brandColors.chartDanger] } }}
+// Pie status distribution — map with brand success/danger/warning (no leftover Ant defaults)
+// Bar domain ranking — single brand primary or soft indigo range
+// Column hourly heatmap:
+scale={{ color: { range: [brandColors.chartTrack, brandColors.chartPrimary] } }}
+```
+
+Retain panels: trend, status distribution, domain ranking, hourly heatmap, recent failures, domain health, recent logs.
+
+- [ ] **Step 3: Use EmptyState where Empty was used for charts/lists (optional but preferred)**
+
+- [ ] **Step 4: Run tests**
+
+Run: `npm test`  
+Expected: PASS (analytics models unchanged)
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/pages/Dashboard.tsx src/frontend/styles.css
+git commit -m "feat(ui): restyle dashboard metrics and charts"
+```
+
+---
+
+### Task 7: Domains list + domain detail visual upgrade
+
+**Files:**
+- Modify: `src/pages/Domains/index.tsx`
+- Modify: `src/pages/Domains/DomainDetail.tsx`
+- Modify: `src/components/domain/DomainHealthCard.tsx`
+- Modify: `src/components/domain/DnsRecordCard.tsx`
+- Modify: `src/components/domain/AddDomainDrawer.tsx` (light polish only: footer/header spacing)
+- Modify: `src/frontend/styles.css` (`.domain-health-card`, `.dns-record-card`, hero layout)
+
+- [ ] **Step 1: Domains list — PageHeader + SectionCard**
+
+Toolbar (search, status filter, add) stays; wrap table in SectionCard; domain link keeps `table-link` emphasis; statuses use StatusTag/StatusPill.
+
+- [ ] **Step 2: DomainHealthCard hero**
+
+- Large domain title
+- StatusPill for health
+- Stats row (sender host, IP, selector, last sent)
+- Progress + DNS API / last check meta
+- Action stack in right column with primary/default buttons (handlers unchanged)
+
+- [ ] **Step 3: DnsRecordCard**
+
+Use CodeBlock for target/current values; StatusPill for record status; keep copy/recheck actions.
+
+- [ ] **Step 4: DomainDetail chrome**
+
+Tabs unchanged in structure; wrap major sections with SectionCard where it improves hierarchy without breaking two-column DNS layout.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/pages/Domains src/components/domain src/frontend/styles.css
+git commit -m "feat(ui): upgrade domains list and detail hero visuals"
+```
+
+---
+
+### Task 8: Remaining pages sweep
+
+**Files:**
+- Modify: `src/pages/SmtpCredentials.tsx`
+- Modify: `src/pages/ApiTokens.tsx`
+- Modify: `src/pages/SendingLogs.tsx`
+- Modify: `src/pages/DnsApi.tsx`
+- Modify: `src/pages/Settings.tsx`
+- Modify: `src/pages/Admin/index.tsx`
+- Modify: `src/pages/PlaceholderPage.tsx`
+- Modify: `src/frontend/styles.css` as needed
+
+- [ ] **Step 1: Apply PageHeader + SectionCard pattern to each page**
+
+Rules:
+
+- Do not change form field names, payloads, or table column data indexes
+- One primary CTA per toolbar region
+- Token secret modal: stronger warning Alert styling only
+- Placeholder webhooks page: EmptyState with short description
+
+- [ ] **Step 2: Grep for leftover default blue hardcodes in UI source**
+
+Run: `rg -n "#1677ff|#f5f7fb" src --glob '!**/node_modules/**'`  
+Expected: no brand-primary leftovers in frontend UI (tests may still mention old values only if intentional — update if any)
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/pages src/frontend/styles.css
+git commit -m "feat(ui): apply redesign chrome across remaining admin pages"
+```
+
+---
+
+### Task 9: Build, test, and final polish gate
+
+**Files:**
+- Possibly minor CSS fixes only
+
+- [ ] **Step 1: Run full test suite**
+
+Run: `npm test`  
+Expected: all tests PASS
+
+- [ ] **Step 2: Production UI build**
+
+Run: `npm run build`  
+Expected: `tsc --noEmit` + Vite build succeed; assets written under `public/assets/`
+
+- [ ] **Step 3: Manual checklist**
+
+- [ ] Login page: indigo primary, brand panel ink, form works
+- [ ] Admin shell: grouped nav, active state, sticky sider
+- [ ] Dashboard: 4 primary metrics; secondary metrics present; charts colored
+- [ ] Domains list + detail: hero + DNS cards readable
+- [ ] SMTP / Tokens / Logs / Settings load without layout break
+- [ ] Mobile width: sider collapses; auth stacks
+
+- [ ] **Step 4: Final commit if polish remains**
+
+```bash
+git add -A
+git commit -m "fix(ui): polish redesign spacing and build assets"
+```
+
+(Only if there are changes.)
+
+---
+
+## Notes for implementers
+
+- **No CDN fonts.** System font stack only unless product later self-hosts Inter.
+- **Do not** change `src/server.js`, models (except pure presentation helpers if unavoidable), or API contracts.
+- Prefer small commits per task above.
+- If a page is too large, change markup/classNames first; avoid drive-by refactors.
+- After implementation, prefer @superpowers:verification-before-completion before claiming done.
+
+## Execution handoff
+
+Plan complete. Choose:
+
+1. **Subagent-Driven (recommended)** — fresh subagent per task + review between tasks  
+2. **Inline Execution** — execute tasks in this session with checkpoints