# Dashboard Workbench Minimal Entry 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:** Bootstrap Git for the project, extend the login token to carry the stylist username, and shrink the dashboard into a minimal-entry page where only three action cards are interactive. **Architecture:** Keep the existing login response shape, but add `username` to the signed JWT payload so the frontend can render the welcome header without a `/me` request. Refactor `DashboardPage` into a presentation-first page that removes unused workbench API calls, keeps the three real entry cards, and renders all other sections as explicit undeveloped placeholders with focused comments and regression tests. **Tech Stack:** Git, NestJS 11, Prisma, Jest, Supertest, React 19, React Router 7, Vitest, Testing Library, i18next ## Global Constraints - 工作台只暴露当前真正可用的入口,不展示“看似能用”的半成品能力。 - 未开发区域要明确可感知,不做假数据充数,不伪装成已实现功能。 - 当前版本能删掉的无效逻辑就删掉,只保留对 UI 占位真正有帮助的结构。 - 前端优先复用现有页面布局和 copy,避免不必要的重写。 - 用户名展示优先复用登录 token,而不是为了工作台额外增加 `/me` 依赖。 - 注释必须具体说明未来要恢复哪类业务数据,不写泛泛的 TODO。 - 当前版本只保留 3 个主操作卡片可点击,其余区域全部不可进入新页面。 - 本 case 不新增新的工作台专属后端接口。 - 当前项目尚未被 Git 管理,因此需要先初始化仓库,再将登录 case 形成基线提交,然后在新的分支上开发本 case,开发完成后再合并回主分支。 - 禁止在代码中使用硬编码的绝对路径。 - 需要排除 AI Agent 过程文件,如 `.learnings`、`.superpowers`。 --- ## File Structure - `.gitignore`: root-level Git ignore file for `node_modules`, build artifacts, env files, and agent process folders. - `backend/src/modules/auth/auth.service.ts`: add `username` to the signed JWT payload while preserving the existing login response body. - `backend/src/modules/auth/auth.service.spec.ts`: assert the backend signs a JWT payload that includes `username`. - `backend/test/auth.e2e-spec.ts`: verify a real login token contains `username` after decoding. - `frontend/src/services/http/session.ts`: add a focused token-decoding helper that safely reads the current session identity from the stored JWT. - `frontend/src/services/http/__tests__/session.test.ts`: regression tests for token decoding success and malformed-token fallback. - `frontend/src/pages/dashboard/DashboardPage.tsx`: remove workbench/statistics API usage, read the username from session token, hide the welcome stats line, and replace undeveloped blocks with disabled UI plus comments. - `frontend/src/pages/dashboard/__tests__/DashboardPage.test.tsx`: assert the dashboard shows the token-derived username, keeps only three working entry links, and renders undeveloped sections as disabled placeholders. - `frontend/src/i18n/locales/zh/translation.json`: add dashboard placeholder copy for undeveloped cards, sections, and disabled buttons. - `frontend/src/i18n/locales/ja/translation.json`: add the same dashboard placeholder copy in Japanese. - `docs/superpowers/specs/2026-06-28-stylist-login-minimal-closed-loop-design.md`: update the login token contract to mention `username`. ### Task 1: Initialize Git And Create The Login Baseline **Files:** - Create: `.gitignore` **Interfaces:** - Produces: Git repository initialized at `/Users/yuchen/Documents/hairstylist` - Produces: default branch `main` - Produces: feature branch `feature/dashboard-workbench-minimal-entry` - [ ] **Step 1: Create the root Git ignore file before initializing the repository** ```gitignore # Root workspace ignores .DS_Store .idea/ .vscode/ .learnings/ .superpowers/ # Dependency directories node_modules/ frontend/node_modules/ backend/node_modules/ # Build output dist/ build/ coverage/ frontend/dist/ backend/dist/ # Environment files .env .env.local .env.*.local frontend/.env frontend/.env.local backend/.env backend/.env.local # Logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* ``` - [ ] **Step 2: Initialize Git and rename the default branch to `main`** Run: ```bash git init git branch -M main git status --short ``` Expected: Git reports a new repository on branch `main`, and tracked candidates appear as untracked files. - [ ] **Step 3: Stage the existing login case as the baseline commit** Run: ```bash git add . git status --short ``` Expected: `node_modules`, `backend/dist`, `.learnings`, and `.superpowers` do not appear in the staging list because `.gitignore` excludes them. - [ ] **Step 4: Create the baseline commit for the already-finished login case** Run: ```bash git commit -m "chore: baseline stylist login case" git log --oneline -n 1 ``` Expected: the latest commit message is `chore: baseline stylist login case`. - [ ] **Step 5: Create the feature branch for the dashboard case** Run: ```bash git checkout -b feature/dashboard-workbench-minimal-entry git branch --show-current ``` Expected: the current branch is `feature/dashboard-workbench-minimal-entry`. ### Task 2: Add `username` To The Login Token And Lock It With Backend Tests **Files:** - Modify: `backend/src/modules/auth/auth.service.ts` - Modify: `backend/src/modules/auth/auth.service.spec.ts` - Modify: `backend/test/auth.e2e-spec.ts` - Modify: `docs/superpowers/specs/2026-06-28-stylist-login-minimal-closed-loop-design.md` **Interfaces:** - Consumes: `AuthService.login(input: StylistLoginDto): Promise` - Produces: JWT payload `{ sub: number; email: string; username: string }` - Produces: unchanged login response body `{ token, tokenName, stylistId, name, email }` - [ ] **Step 1: Extend the backend unit test so it fails until `username` is signed** ```ts // backend/src/modules/auth/auth.service.spec.ts it('signs the jwt payload with username when credentials are valid', async () => { prisma.hairstylistUser.findFirst.mockResolvedValue({ id: BigInt(1), username: 'Ogawa', email: 'stylist@example.com', password: await hash('Passw0rd!', 10), isDeleted: 0, }); jwtService.signAsync.mockResolvedValue('signed.jwt.token'); await service.login({ email: 'stylist@example.com', password: 'Passw0rd!', remember: false, }); expect(jwtService.signAsync).toHaveBeenCalledWith( { sub: 1, email: 'stylist@example.com', username: 'Ogawa', }, expect.objectContaining({ expiresIn: '7d', }), ); }); ``` - [ ] **Step 2: Extend the e2e test so it decodes a real login token and expects `username`** ```ts // backend/test/auth.e2e-spec.ts it('/api/v1/auth/stylist/login (POST) returns a token payload with username', async () => { const response = await request(app.getHttpServer()) .post('/api/v1/auth/stylist/login') .send({ email: 'stylist@example.com', password: 'Passw0rd!', remember: false, }) .expect(201); const payload = await jwtService.verifyAsync<{ sub: number; email: string; username: string; }>(response.body.data.token); expect(payload.sub).toBe(1); expect(payload.email).toBe('stylist@example.com'); expect(payload.username).toBe('Ogawa'); }); ``` - [ ] **Step 3: Run the backend tests first and confirm they fail on the missing field** Run: ```bash npm test -- auth.service.spec.ts --runInBand npm run test:e2e -- auth.e2e-spec.ts ``` Expected: at least one assertion fails because `jwtService.signAsync` does not yet receive `username` and the decoded token lacks `username`. - [ ] **Step 4: Implement the minimal backend change in `AuthService`** ```ts // backend/src/modules/auth/auth.service.ts const token = await this.jwtService.signAsync( { sub: Number(stylist.id), email: stylist.email, username: stylist.username, }, { expiresIn: (this.configService.get('JWT_EXPIRES_IN') ?? '7d') as StringValue, }, ); ``` - [ ] **Step 5: Update the login spec so the token contract matches the implementation** ```md ## Token 设计 - 本 case 只签发单个 access token。 - token 类型采用 JWT。 - payload 只包含最小必要身份字段,例如 `sub`、`email`、`username`。 - `username` 供工作台欢迎区直接消费,不再额外请求 `/me` 接口。 - 不在 token 中塞入模板数量、客户统计等业务数据,避免 token 承担缓存职责。 ``` - [ ] **Step 6: Re-run the focused backend tests and confirm they pass** Run: ```bash npm test -- auth.service.spec.ts --runInBand npm run test:e2e -- auth.e2e-spec.ts ``` Expected: both suites pass, and the login token decodes with `username`. - [ ] **Step 7: Commit the token-contract change** ```bash git add backend/src/modules/auth/auth.service.ts \ backend/src/modules/auth/auth.service.spec.ts \ backend/test/auth.e2e-spec.ts \ docs/superpowers/specs/2026-06-28-stylist-login-minimal-closed-loop-design.md git commit -m "feat: include username in login token" ``` ### Task 3: Add A Safe Session Identity Helper And Render The Token Username In The Welcome Header **Files:** - Modify: `frontend/src/services/http/session.ts` - Create: `frontend/src/services/http/__tests__/session.test.ts` - Modify: `frontend/src/pages/dashboard/DashboardPage.tsx` - Create: `frontend/src/pages/dashboard/__tests__/DashboardPage.test.tsx` **Interfaces:** - Produces: `getSessionIdentity(): { username: string | null }` - Consumes: `getToken(): string | null` - Produces: `DashboardPage` welcome header uses token-derived username first, then falls back to `pageCopy.greetingName` - [ ] **Step 1: Write the failing token-decoder unit test** ```ts // frontend/src/services/http/__tests__/session.test.ts import { describe, expect, it } from 'vitest' import { clearSession, getSessionIdentity, setSession } from '../session' function createToken(payload: Record) { const encoded = btoa(JSON.stringify(payload)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '') return `header.${encoded}.signature` } describe('session identity', () => { it('reads username from the current jwt payload', () => { setSession(createToken({ sub: 1, email: 'stylist@example.com', username: 'Ogawa' }), 'Authorization') expect(getSessionIdentity()).toEqual({ username: 'Ogawa', }) }) it('returns nulls for malformed tokens', () => { setSession('not-a-jwt', 'Authorization') expect(getSessionIdentity()).toEqual({ username: null, }) }) }) ``` - [ ] **Step 2: Write the failing dashboard test for the personalized greeting** ```tsx // frontend/src/pages/dashboard/__tests__/DashboardPage.test.tsx import { HelmetProvider } from 'react-helmet-async' import { MemoryRouter, Route, Routes } from 'react-router-dom' import { render, screen } from '@testing-library/react' import '../../../../i18n' import { setSession } from '../../../services/http/session' import { DashboardPage } from '../DashboardPage' function createToken(payload: Record) { const encoded = btoa(JSON.stringify(payload)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '') return `header.${encoded}.signature` } it('shows the username from the login token in the welcome header', async () => { setSession(createToken({ sub: 1, email: 'stylist@example.com', username: 'Ogawa' }), 'Authorization') render( } /> , ) expect(await screen.findByRole('heading', { name: /早上好|下午好|晚上好/i })).toBeInTheDocument() expect(screen.getByText('Ogawa')).toBeInTheDocument() }) ``` - [ ] **Step 3: Run the new frontend tests and confirm they fail** Run: ```bash npm run test -- src/services/http/__tests__/session.test.ts src/pages/dashboard/__tests__/DashboardPage.test.tsx ``` Expected: failures because `getSessionIdentity` does not exist yet and `DashboardPage` still depends on `getStylistMe()`. - [ ] **Step 4: Implement the token-decoding helper in `session.ts`** ```ts // frontend/src/services/http/session.ts type SessionIdentity = { username: string | null } function decodeJwtSegment(segment: string): Record | null { try { const normalized = segment.replace(/-/g, '+').replace(/_/g, '/') const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=') return JSON.parse(atob(padded)) as Record } catch { return null } } export function getSessionIdentity(): SessionIdentity { const token = getToken() if (!token) { return { username: null } } const [, payload] = token.split('.') if (!payload) { return { username: null } } const decoded = decodeJwtSegment(payload) return { username: typeof decoded?.username === 'string' ? decoded.username : null, } } ``` - [ ] **Step 5: Refactor `DashboardPage` to consume the local token identity instead of `/me`** ```tsx // frontend/src/pages/dashboard/DashboardPage.tsx import { useMemo } from 'react' import { getSessionIdentity } from '../../services/http/session' export function DashboardPage() { const { locale, path, copy } = usePageContext() const pageCopy = copy.dashboardPage const sessionIdentity = useMemo(() => getSessionIdentity(), []) const stylistName = sessionIdentity.username || pageCopy.greetingName const now = useMemo(() => new Date(), []) const dateLabel = useMemo( () => new Intl.DateTimeFormat(locale === 'ja' ? 'ja-JP' : 'zh-CN', { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long', }).format(now), [locale, now], ) return (
{pageCopy.badge}

{greeting}
{stylistName}

{pageCopy.summary}

{dateLabel}
) } ``` - [ ] **Step 6: Re-run the new frontend tests and confirm the welcome header now passes** Run: ```bash npm run test -- src/services/http/__tests__/session.test.ts src/pages/dashboard/__tests__/DashboardPage.test.tsx ``` Expected: the token helper tests pass, and the dashboard test renders `Ogawa` without any `/me` mock. - [ ] **Step 7: Commit the session-identity change** ```bash git add frontend/src/services/http/session.ts \ frontend/src/services/http/__tests__/session.test.ts \ frontend/src/pages/dashboard/DashboardPage.tsx \ frontend/src/pages/dashboard/__tests__/DashboardPage.test.tsx git commit -m "feat: read dashboard username from session token" ``` ### Task 4: Shrink The Dashboard To Three Real Entry Cards And Explicit Undeveloped UI **Files:** - Modify: `frontend/src/pages/dashboard/DashboardPage.tsx` - Modify: `frontend/src/i18n/locales/zh/translation.json` - Modify: `frontend/src/i18n/locales/ja/translation.json` - Modify: `frontend/src/pages/dashboard/__tests__/DashboardPage.test.tsx` **Interfaces:** - Produces: only three interactive links: `/:locale/customers/new`, `/:locale/simulation`, `/:locale/templates` - Produces: no calls to `getWorkbenchData`, `getCustomerDashboardSummary`, `getTemplateDashboardSummary`, or `getStylistMe` - Produces: undeveloped dashboard sections with disabled buttons and explicit placeholder copy - [ ] **Step 1: Expand the dashboard test so it fails until the page is fully shrunk** ```tsx // frontend/src/pages/dashboard/__tests__/DashboardPage.test.tsx it('keeps only three real entry links and hides the welcome stats line', async () => { setSession(createToken({ sub: 1, email: 'stylist@example.com', username: 'Ogawa' }), 'Authorization') render( } /> , ) expect(screen.queryByText(/今日预约 .* 件 \/ 待跟进 .* 件/i)).not.toBeInTheDocument() const entryLinks = screen.getAllByRole('link').filter((element) => { const href = element.getAttribute('href') || '' return href === '/zh/customers/new' || href === '/zh/simulation' || href === '/zh/templates' }) expect(entryLinks).toHaveLength(3) }) it('renders undeveloped placeholders for stats and workbench panels', async () => { setSession(createToken({ sub: 1, email: 'stylist@example.com', username: 'Ogawa' }), 'Authorization') render( } /> , ) expect(screen.getAllByText('当前版本暂未开发').length).toBeGreaterThanOrEqual(4) expect(screen.getAllByRole('button', { name: '当前版本暂未开发' }).length).toBeGreaterThanOrEqual(3) }) ``` - [ ] **Step 2: Add the missing localized placeholder copy in both translation files** ```json // frontend/src/i18n/locales/zh/translation.json "dashboardPage": { "undeveloped": { "statValue": "--", "badge": "当前版本暂未开发", "body": "当前版本仅保留工作台入口,后续将从工作台聚合数据恢复该区域。", "disabledAction": "当前版本暂未开发" } } ``` ```json // frontend/src/i18n/locales/ja/translation.json "dashboardPage": { "undeveloped": { "statValue": "--", "badge": "このバージョンでは未対応", "body": "このバージョンではワークベンチ入口のみ提供し、後続で集約データを接続します。", "disabledAction": "このバージョンでは未対応" } } ``` - [ ] **Step 3: Run the dashboard test again and confirm it fails on the old API-driven UI** Run: ```bash npm run test -- src/pages/dashboard/__tests__/DashboardPage.test.tsx ``` Expected: failures because the page still shows the headline stat line, renders data-driven sections, and keeps extra interactive links. - [ ] **Step 4: Remove the old workbench logic and replace it with explicit undeveloped placeholders** ```tsx // frontend/src/pages/dashboard/DashboardPage.tsx import { useMemo } from 'react' import { Link } from 'react-router-dom' const undevelopedPanels = ['today', 'followUp', 'records'] as const const undevelopedStats = ['todayCustomers', 'followUp', 'weeklyRecords', 'templates'] as const export function DashboardPage() { const { locale, path, copy } = usePageContext() const pageCopy = copy.dashboardPage const undevelopedCopy = pageCopy.undeveloped return (
{pageCopy.badge}

{greeting}
{stylistName}

{pageCopy.summary}

{dateLabel} {/* 当前版本未开发欢迎区统计,后续从 workbench 聚合接口恢复预约与跟进真实数据。 */}
{/* keep only the three real entry cards for this version */}
{undevelopedStats.map((key) => (
{undevelopedCopy.statValue} {pageCopy.stats[key]} {undevelopedCopy.badge}
))}
{undevelopedPanels.map((panelKey) => (
{pageCopy.panels[panelKey].badge}

{pageCopy.panels[panelKey].title}

{/* 当前版本未开发该工作台区块,后续从 workbench 聚合接口恢复列表数据与导航动作。 */}

{undevelopedCopy.body}

))}
) } ``` - [ ] **Step 5: Re-run the focused dashboard regression test and confirm only the intended UI remains** Run: ```bash npm run test -- src/pages/dashboard/__tests__/DashboardPage.test.tsx ``` Expected: the test passes with exactly three interactive entry links, no welcome stats line, and disabled undeveloped sections. - [ ] **Step 6: Commit the dashboard shrink** ```bash git add frontend/src/pages/dashboard/DashboardPage.tsx \ frontend/src/pages/dashboard/__tests__/DashboardPage.test.tsx \ frontend/src/i18n/locales/zh/translation.json \ frontend/src/i18n/locales/ja/translation.json git commit -m "feat: shrink dashboard to minimal entry page" ``` ### Task 5: Run Verification, Update Diagnostics, And Merge Back To `main` **Files:** - Modify: `docs/superpowers/specs/2026-06-28-dashboard-workbench-minimal-entry-design.md` (only if the implemented copy or disabled-button behavior differs from the spec during execution) **Interfaces:** - Consumes: all commits on `feature/dashboard-workbench-minimal-entry` - Produces: merged `main` branch with a clean working tree - [ ] **Step 1: Run the backend and frontend verification commands** Run: ```bash cd /Users/yuchen/Documents/hairstylist/backend && npm test -- auth.service.spec.ts --runInBand && npm run test:e2e -- auth.e2e-spec.ts cd /Users/yuchen/Documents/hairstylist/frontend && npm run test -- src/services/http/__tests__/session.test.ts src/pages/dashboard/__tests__/DashboardPage.test.tsx src/pages/public/__tests__/login-page.test.tsx src/shared/__tests__/RequireAuth.test.tsx cd /Users/yuchen/Documents/hairstylist/frontend && npm run typecheck ``` Expected: all targeted backend/frontend tests pass, and frontend type checking succeeds. - [ ] **Step 2: Check diagnostics on the changed frontend and backend files** Files to inspect: ```text backend/src/modules/auth/auth.service.ts backend/src/modules/auth/auth.service.spec.ts backend/test/auth.e2e-spec.ts frontend/src/services/http/session.ts frontend/src/services/http/__tests__/session.test.ts frontend/src/pages/dashboard/DashboardPage.tsx frontend/src/pages/dashboard/__tests__/DashboardPage.test.tsx frontend/src/i18n/locales/zh/translation.json frontend/src/i18n/locales/ja/translation.json ``` Expected: no newly introduced diagnostics remain unresolved. - [ ] **Step 3: Update the dashboard spec only if the implementation forced a small copy or structure adjustment** ```md ## 验收标准 - 页面上只有 3 个主操作卡片可以点击。 - 4 个统计卡显示为未开发态,不再依赖真实统计请求。 - 今日顾客、待跟进顾客、最近施术记录 3 个区块显示为未开发态,并使用禁用按钮而非真实跳转。 ``` - [ ] **Step 4: Commit the final docs-or-fixup changes if Step 3 modified anything** ```bash git add docs/superpowers/specs/2026-06-28-dashboard-workbench-minimal-entry-design.md git commit -m "docs: align dashboard spec with implementation" ``` Expected: skip this step entirely if no spec text changed. - [ ] **Step 5: Merge the feature branch back into `main`** Run: ```bash git checkout main git merge --no-ff feature/dashboard-workbench-minimal-entry git status --short ``` Expected: the merge succeeds without conflicts, and `git status --short` is empty. - [ ] **Step 6: Verify the final branch history** Run: ```bash git log --oneline --decorate -n 5 git branch --show-current ``` Expected: the current branch is `main`, and the merge commit includes the feature branch work.