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
/me 依赖。.learnings、.superpowers。.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.Files:
.gitignoreInterfaces:
/Users/yuchen/Documents/hairstylistmainProduces: feature branch feature/dashboard-workbench-minimal-entry
[ ] Step 1: Create the root Git ignore file before initializing the repository
# 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:
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.
Run:
git add .
git status --short
Expected: node_modules, backend/dist, .learnings, and .superpowers do not appear in the staging list because .gitignore excludes them.
Run:
git commit -m "chore: baseline stylist login case"
git log --oneline -n 1
Expected: the latest commit message is chore: baseline stylist login case.
Run:
git checkout -b feature/dashboard-workbench-minimal-entry
git branch --show-current
Expected: the current branch is feature/dashboard-workbench-minimal-entry.
username To The Login Token And Lock It With Backend TestsFiles:
backend/src/modules/auth/auth.service.tsbackend/src/modules/auth/auth.service.spec.tsbackend/test/auth.e2e-spec.tsdocs/superpowers/specs/2026-06-28-stylist-login-minimal-closed-loop-design.mdInterfaces:
AuthService.login(input: StylistLoginDto): Promise<LoginResultDto>{ 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
// 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
// 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:
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
// 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<string>('JWT_EXPIRES_IN') ?? '7d') as StringValue,
},
);
[ ] Step 5: Update the login spec so the token contract matches the implementation
## 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:
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
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"
Files:
frontend/src/services/http/session.tsfrontend/src/services/http/__tests__/session.test.tsfrontend/src/pages/dashboard/DashboardPage.tsxfrontend/src/pages/dashboard/__tests__/DashboardPage.test.tsxInterfaces:
getSessionIdentity(): { username: string | null }getToken(): string | nullProduces: DashboardPage welcome header uses token-derived username first, then falls back to pageCopy.greetingName
[ ] Step 1: Write the failing token-decoder unit test
// frontend/src/services/http/__tests__/session.test.ts
import { describe, expect, it } from 'vitest'
import { clearSession, getSessionIdentity, setSession } from '../session'
function createToken(payload: Record<string, unknown>) {
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
// 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<string, unknown>) {
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(
<HelmetProvider>
<MemoryRouter initialEntries={['/zh/dashboard']}>
<Routes>
<Route path="/:locale/dashboard" element={<DashboardPage />} />
</Routes>
</MemoryRouter>
</HelmetProvider>,
)
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:
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
// frontend/src/services/http/session.ts
type SessionIdentity = {
username: string | null
}
function decodeJwtSegment(segment: string): Record<string, unknown> | 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<string, unknown>
} 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
// 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 (
<DashboardLayout>
<section className="stylist-workbench-top ipad-top-space-margin">
<div className="container">
<div className="row align-items-end justify-content-between mb-4">
<div className="col-lg-10">
<span className="bg-base-color d-inline-block mb-15px fw-600 text-white text-uppercase border-radius-30px ps-20px pe-20px fs-12">{pageCopy.badge}</span>
<h1 className="fw-700 text-dark-gray ls-minus-1px mb-10px">
{greeting}
<br />
{stylistName}
</h1>
<p className="fs-18 lh-30 mb-0">{pageCopy.summary}</p>
</div>
<div className="col-lg-4 text-lg-end mt-3 mt-lg-0">
<span className="d-block fs-14 text-medium-gray">{dateLabel}</span>
</div>
</div>
</div>
</section>
</DashboardLayout>
)
}
[ ] Step 6: Re-run the new frontend tests and confirm the welcome header now passes
Run:
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
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"
Files:
frontend/src/pages/dashboard/DashboardPage.tsxfrontend/src/i18n/locales/zh/translation.jsonfrontend/src/i18n/locales/ja/translation.jsonfrontend/src/pages/dashboard/__tests__/DashboardPage.test.tsxInterfaces:
/:locale/customers/new, /:locale/simulation, /:locale/templatesgetWorkbenchData, getCustomerDashboardSummary, getTemplateDashboardSummary, or getStylistMeProduces: 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
// 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(
<HelmetProvider>
<MemoryRouter initialEntries={['/zh/dashboard']}>
<Routes>
<Route path="/:locale/dashboard" element={<DashboardPage />} />
</Routes>
</MemoryRouter>
</HelmetProvider>,
)
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(
<HelmetProvider>
<MemoryRouter initialEntries={['/zh/dashboard']}>
<Routes>
<Route path="/:locale/dashboard" element={<DashboardPage />} />
</Routes>
</MemoryRouter>
</HelmetProvider>,
)
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
// frontend/src/i18n/locales/zh/translation.json
"dashboardPage": {
"undeveloped": {
"statValue": "--",
"badge": "当前版本暂未开发",
"body": "当前版本仅保留工作台入口,后续将从工作台聚合数据恢复该区域。",
"disabledAction": "当前版本暂未开发"
}
}
// 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:
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
// 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 (
<DashboardLayout>
<section className="stylist-workbench-top ipad-top-space-margin">
<div className="container">
<div className="row align-items-end justify-content-between mb-4">
<div className="col-lg-10">
<span className="bg-base-color d-inline-block mb-15px fw-600 text-white text-uppercase border-radius-30px ps-20px pe-20px fs-12">{pageCopy.badge}</span>
<h1 className="fw-700 text-dark-gray ls-minus-1px mb-10px">{greeting}<br />{stylistName}</h1>
<p className="fs-18 lh-30 mb-0">{pageCopy.summary}</p>
</div>
<div className="col-lg-4 text-lg-end mt-3 mt-lg-0">
<span className="d-block fs-14 text-medium-gray">{dateLabel}</span>
{/* 当前版本未开发欢迎区统计,后续从 workbench 聚合接口恢复预约与跟进真实数据。 */}
</div>
</div>
<div className="row row-cols-1 row-cols-lg-3 row-cols-md-3 g-4 mb-5">
{/* keep only the three real entry cards for this version */}
</div>
<div className="row row-cols-2 row-cols-lg-4 row-cols-md-4 g-3 mb-5">
{undevelopedStats.map((key) => (
<div key={key} className="col">
<div className="workbench-stat bg-white border-radius-8px p-20px opacity-75">
<span className="d-block fs-26 fw-800 text-dark-gray">{undevelopedCopy.statValue}</span>
<span className="fs-14 d-block">{pageCopy.stats[key]}</span>
<span className="fs-13 text-medium-gray d-block mt-2">{undevelopedCopy.badge}</span>
</div>
</div>
))}
</div>
</div>
</section>
<section className="pt-0">
<div className="container">
<div className="row g-4">
{undevelopedPanels.map((panelKey) => (
<div key={panelKey} className={panelKey === 'records' ? 'col-12' : panelKey === 'today' ? 'col-xl-7' : 'col-xl-5'}>
<div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px h-100 opacity-75">
<div className="d-flex align-items-center justify-content-between mb-25px">
<div>
<span className="fs-14 fw-700 text-base-color text-uppercase">{pageCopy.panels[panelKey].badge}</span>
<h2 className="fw-700 text-dark-gray fs-26 mb-0">{pageCopy.panels[panelKey].title}</h2>
</div>
<button type="button" className="btn btn-small btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700" disabled>
{undevelopedCopy.disabledAction}
</button>
</div>
{/* 当前版本未开发该工作台区块,后续从 workbench 聚合接口恢复列表数据与导航动作。 */}
<p className="fs-15 text-medium-gray mb-0">{undevelopedCopy.body}</p>
</div>
</div>
))}
</div>
</div>
</section>
</DashboardLayout>
)
}
[ ] Step 5: Re-run the focused dashboard regression test and confirm only the intended UI remains
Run:
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
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"
mainFiles:
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:
feature/dashboard-workbench-minimal-entryProduces: merged main branch with a clean working tree
[ ] Step 1: Run the backend and frontend verification commands
Run:
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.
Files to inspect:
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
## 验收标准
- 页面上只有 3 个主操作卡片可以点击。
- 4 个统计卡显示为未开发态,不再依赖真实统计请求。
- 今日顾客、待跟进顾客、最近施术记录 3 个区块显示为未开发态,并使用禁用按钮而非真实跳转。
[ ] Step 4: Commit the final docs-or-fixup changes if Step 3 modified anything
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.
mainRun:
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.
Run:
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.