# Template Edit Page Basic Info 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:** Add a dedicated template edit page for basic info only, switch the task success CTA to that page, and add backend read/update APIs for template basic info. **Architecture:** Keep template creation and task orchestration unchanged. Split responsibilities clearly: `TemplateMakeTaskPage` handles task completion and redirects into a new `TemplateEditPage`, `TemplateDetailPage` remains the detail-view entry, and the backend `templates` module exposes focused `GET /templates/:templateId` and `PATCH /templates/:templateId` capabilities scoped to the current stylist. **Tech Stack:** React + React Router + Vitest + Testing Library, NestJS + Prisma + Jest ## Global Constraints - Keep `TemplateMakeTaskPage` success CTA text as “编辑模板信息” and route it to `/:locale/templates/:templateId/edit`. - The new edit page may update only `templateName`, `status`, `category`, `tags`, and `description`. - Do not allow editing `coverUrl`, `templateUrl`, `refHairUrl`, `modelUrl`, `clothesUrl`, `backgroundUrl`, or any `hair_template_make_task` fields. - Keep `TemplateDetailPage` as the template detail entry from list cards. - Follow existing workbench page layout with `stylist-workbench-top ipad-top-space-margin mb-2`. - Preserve zero-mock product behavior; only tests may mock dependencies. --- ## File Structure - `backend/src/modules/templates/dto/template-basic-info.dto.ts` - Read model returned by `GET /templates/:templateId` - `backend/src/modules/templates/dto/update-template-basic-info.dto.ts` - Validated PATCH payload for editable fields only - `backend/src/modules/templates/templates.controller.ts` - Add `GET /templates/:templateId` and `PATCH /templates/:templateId` - `backend/src/modules/templates/templates.service.ts` - Add scoped fetch/update methods for `hair_template` - `backend/src/modules/templates/templates.service.spec.ts` - Unit coverage for read/update behavior and field boundaries - `backend/test/templates.e2e-spec.ts` - Endpoint coverage for GET/PATCH template basic info - `frontend/src/config/routes.ts` - Add `templateEdit` route - `frontend/src/App.tsx` - Register `TemplateEditPage` - `frontend/src/services/api/contracts.ts` - Add `TemplateBasicInfo` and `UpdateTemplateBasicInfoInput` - `frontend/src/services/api/templates.ts` - Add `getTemplateBasicInfo()` and `updateTemplateBasicInfo()` - `frontend/src/pages/dashboard/templates/TemplateEditPage.tsx` - New basic-info-only editor page - `frontend/src/pages/dashboard/templates/TemplateMakeTaskPage.tsx` - Change success CTA copy and target route - `frontend/src/pages/dashboard/templates/TemplateDetailPage.tsx` - Remove inline editing actions so it stays detail-oriented - `frontend/src/pages/dashboard/templates/__tests__/TemplateEditPage.test.tsx` - New page test coverage - `frontend/src/pages/dashboard/templates/__tests__/TemplateMakeTaskPage.test.tsx` - Update success CTA expectation - `frontend/src/pages/dashboard/templates/__tests__/TemplateDetailPage.test.tsx` - Lock detail page as display-oriented if needed - `frontend/src/__tests__/App.test.tsx` - Route registration test for edit page - `frontend/src/i18n/locales/zh/translation.json` - Add `templateEditPage` and task CTA copy - `frontend/src/i18n/locales/ja/translation.json` - Add `templateEditPage` and task CTA copy ### Task 1: Backend Basic Info DTOs And Service Methods **Files:** - Create: `backend/src/modules/templates/dto/template-basic-info.dto.ts` - Create: `backend/src/modules/templates/dto/update-template-basic-info.dto.ts` - Modify: `backend/src/modules/templates/templates.service.ts` - Test: `backend/src/modules/templates/templates.service.spec.ts` **Interfaces:** - Consumes: - Existing Prisma `hairTemplate` model fields: `templateName`, `status`, `category`, `tags`, `description`, `coverUrl`, `templateUrl`, `hairstylistId` - Produces: - `TemplatesService.getBasicInfo(stylistId: bigint, templateId: string): Promise` - `TemplatesService.updateBasicInfo(stylistId: bigint, templateId: string, dto: UpdateTemplateBasicInfoDto): Promise` - `TemplateBasicInfoDto` - `UpdateTemplateBasicInfoDto` - [ ] **Step 1: Write the failing service tests** ```ts it('returns template basic info for the owning stylist', async () => { prisma.hairTemplate.findFirst.mockResolvedValue({ id: BigInt(43), templateName: '柔和层次长发', status: 'DRAFT', category: 'LONG', tags: '层次,首次建议', description: '适合轻盈感修饰', coverUrl: 'https://cdn.example.com/cover.jpg', templateUrl: 'https://cdn.example.com/template.zip', hairstylistId: BigInt(1), isDeleted: 0, }) await expect(service.getBasicInfo(BigInt(1), '43')).resolves.toEqual({ id: '43', templateName: '柔和层次长发', status: 'DRAFT', category: 'LONG', tags: ['层次', '首次建议'], description: '适合轻盈感修饰', coverUrl: 'https://cdn.example.com/cover.jpg', templateUrl: 'https://cdn.example.com/template.zip', }) }) it('updates only editable basic info fields', async () => { prisma.hairTemplate.findFirst.mockResolvedValue({ id: BigInt(43), templateName: '旧模板名', status: 'DRAFT', category: 'LONG', tags: '旧标签', description: '旧描述', coverUrl: 'https://cdn.example.com/cover.jpg', templateUrl: 'https://cdn.example.com/template.zip', hairstylistId: BigInt(1), isDeleted: 0, }) prisma.hairTemplate.update.mockResolvedValue({ id: BigInt(43), templateName: '新模板名', status: 'PUBLIC', category: 'MEDIUM', tags: '空气感,层次', description: '新描述', coverUrl: 'https://cdn.example.com/cover.jpg', templateUrl: 'https://cdn.example.com/template.zip', hairstylistId: BigInt(1), isDeleted: 0, }) await service.updateBasicInfo(BigInt(1), '43', { templateName: '新模板名', status: 'PUBLIC', category: 'MEDIUM', tags: ['空气感', '层次'], description: '新描述', }) expect(prisma.hairTemplate.update).toHaveBeenCalledWith({ where: { id: BigInt(43) }, data: { templateName: '新模板名', status: 'PUBLIC', category: 'MEDIUM', tags: '空气感,层次', description: '新描述', }, }) }) ``` - [ ] **Step 2: Run test to verify it fails** Run: ```bash cd /Users/yuchen/Documents/hairstylist/backend npm test -- --runInBand src/modules/templates/templates.service.spec.ts ``` Expected: FAIL because `getBasicInfo` / `updateBasicInfo` and the new DTO behavior do not exist yet. - [ ] **Step 3: Add the DTO files** ```ts // backend/src/modules/templates/dto/template-basic-info.dto.ts export class TemplateBasicInfoDto { id!: string templateName!: string status!: 'DRAFT' | 'PUBLIC' category!: 'LONG' | 'MEDIUM' | 'SHORT' | 'DYE' tags!: string[] description!: string coverUrl!: string templateUrl!: string } ``` ```ts // backend/src/modules/templates/dto/update-template-basic-info.dto.ts import { IsArray, IsIn, IsOptional, IsString } from 'class-validator' export class UpdateTemplateBasicInfoDto { @IsString() templateName!: string @IsIn(['DRAFT', 'PUBLIC']) status!: 'DRAFT' | 'PUBLIC' @IsIn(['LONG', 'MEDIUM', 'SHORT', 'DYE']) category!: 'LONG' | 'MEDIUM' | 'SHORT' | 'DYE' @IsOptional() @IsArray() @IsString({ each: true }) tags?: string[] @IsOptional() @IsString() description?: string } ``` - [ ] **Step 4: Implement minimal service methods** ```ts private async getOwnedTemplate(stylistId: bigint, templateId: string) { const template = await this.prisma.hairTemplate.findFirst({ where: { id: BigInt(templateId), hairstylistId: stylistId, isDeleted: 0, }, }) if (!template) { throw new NotFoundException('模板不存在') } return template } async getBasicInfo(stylistId: bigint, templateId: string): Promise { const template = await this.getOwnedTemplate(stylistId, templateId) return { id: template.id.toString(), templateName: template.templateName, status: template.status, category: template.category, tags: template.tags ? template.tags.split(',').map((tag) => tag.trim()).filter(Boolean) : [], description: template.description ?? '', coverUrl: template.coverUrl, templateUrl: template.templateUrl, } } async updateBasicInfo( stylistId: bigint, templateId: string, dto: UpdateTemplateBasicInfoDto, ): Promise { const template = await this.getOwnedTemplate(stylistId, templateId) const updated = await this.prisma.hairTemplate.update({ where: { id: template.id }, data: { templateName: dto.templateName, status: dto.status, category: dto.category, tags: (dto.tags ?? []).join(','), description: dto.description ?? '', }, }) return { id: updated.id.toString(), templateName: updated.templateName, status: updated.status, category: updated.category, tags: updated.tags ? updated.tags.split(',').map((tag) => tag.trim()).filter(Boolean) : [], description: updated.description ?? '', coverUrl: updated.coverUrl, templateUrl: updated.templateUrl, } } ``` - [ ] **Step 5: Run test to verify it passes** Run: ```bash cd /Users/yuchen/Documents/hairstylist/backend npm test -- --runInBand src/modules/templates/templates.service.spec.ts ``` Expected: PASS with the new read/update service tests included. - [ ] **Step 6: Commit** ```bash cd /Users/yuchen/Documents/hairstylist git add backend/src/modules/templates/dto/template-basic-info.dto.ts \ backend/src/modules/templates/dto/update-template-basic-info.dto.ts \ backend/src/modules/templates/templates.service.ts \ backend/src/modules/templates/templates.service.spec.ts git commit -m "feat: add template basic info service methods" ``` ### Task 2: Backend GET And PATCH Template Endpoints **Files:** - Modify: `backend/src/modules/templates/templates.controller.ts` - Test: `backend/test/templates.e2e-spec.ts` **Interfaces:** - Consumes: - `TemplatesService.getBasicInfo(stylistId, templateId)` - `TemplatesService.updateBasicInfo(stylistId, templateId, dto)` - `UpdateTemplateBasicInfoDto` - Produces: - `GET /api/v1/templates/:templateId` - `PATCH /api/v1/templates/:templateId` - [ ] **Step 1: Write failing e2e coverage** ```ts it('GET /api/v1/templates/:templateId returns template basic info', async () => { await request(app.getHttpServer()) .get('/api/v1/templates/43') .set('Authorization', token) .expect(200) .expect(({ body }) => { expect(body.data).toEqual({ id: '43', templateName: '柔和层次长发', status: 'DRAFT', category: 'LONG', tags: ['层次', '首次建议'], description: '适合轻盈感修饰', coverUrl: 'https://cdn.example.com/cover.jpg', templateUrl: 'https://cdn.example.com/template.zip', }) }) }) it('PATCH /api/v1/templates/:templateId updates only basic info', async () => { await request(app.getHttpServer()) .patch('/api/v1/templates/43') .set('Authorization', token) .send({ templateName: '空气感中长发', status: 'PUBLIC', category: 'MEDIUM', tags: ['空气感', '层次'], description: '新描述', }) .expect(200) }) ``` - [ ] **Step 2: Run test to verify it fails** Run: ```bash cd /Users/yuchen/Documents/hairstylist/backend npm run test:e2e -- templates.e2e-spec.ts ``` Expected: FAIL because controller routes for GET/PATCH template basic info are missing. - [ ] **Step 3: Add controller endpoints** ```ts @Get(':templateId') getById( @Req() request: AuthenticatedRequest, @Param('templateId') templateId: string, ) { return this.templatesService.getBasicInfo( BigInt(request.user.stylistId), templateId, ) } @Patch(':templateId') updateById( @Req() request: AuthenticatedRequest, @Param('templateId') templateId: string, @Body() body: UpdateTemplateBasicInfoDto, ) { return this.templatesService.updateBasicInfo( BigInt(request.user.stylistId), templateId, body, ) } ``` - [ ] **Step 4: Update e2e in-memory store behavior** ```ts hairTemplates = [ { id: BigInt(43), templateName: '柔和层次长发', status: 'DRAFT', category: 'LONG', tags: '层次,首次建议', description: '适合轻盈感修饰', coverUrl: 'https://cdn.example.com/cover.jpg', templateUrl: 'https://cdn.example.com/template.zip', hairstylistId: BigInt(1), isDeleted: 0, }, ] hairTemplate.findFirst = vi.fn(async ({ where }) => { return hairTemplates.find((item) => item.id === where.id && item.hairstylistId === where.hairstylistId && item.isDeleted === where.isDeleted, ) ?? null }) hairTemplate.update = vi.fn(async ({ where, data }) => { const current = hairTemplates.find((item) => item.id === where.id) Object.assign(current, data) return current }) ``` - [ ] **Step 5: Run test to verify it passes** Run: ```bash cd /Users/yuchen/Documents/hairstylist/backend npm run test:e2e -- templates.e2e-spec.ts ``` Expected: PASS with GET/PATCH template basic info endpoint coverage. - [ ] **Step 6: Commit** ```bash cd /Users/yuchen/Documents/hairstylist git add backend/src/modules/templates/templates.controller.ts \ backend/test/templates.e2e-spec.ts git commit -m "feat: add template basic info endpoints" ``` ### Task 3: Frontend Route, API Contracts, And Task Success CTA **Files:** - Modify: `frontend/src/config/routes.ts` - Modify: `frontend/src/App.tsx` - Modify: `frontend/src/services/api/contracts.ts` - Modify: `frontend/src/services/api/templates.ts` - Modify: `frontend/src/pages/dashboard/templates/TemplateMakeTaskPage.tsx` - Modify: `frontend/src/pages/dashboard/templates/__tests__/TemplateMakeTaskPage.test.tsx` - Modify: `frontend/src/__tests__/App.test.tsx` - Modify: `frontend/src/i18n/locales/zh/translation.json` - Modify: `frontend/src/i18n/locales/ja/translation.json` **Interfaces:** - Consumes: - Backend `GET /templates/:templateId` - Backend `PATCH /templates/:templateId` - Produces: - `routes.templateEdit = '/:locale/templates/:templateId/edit'` - `getTemplateBasicInfo(templateId: string): Promise` - `updateTemplateBasicInfo(templateId: string, input: UpdateTemplateBasicInfoInput): Promise` - [ ] **Step 1: Write failing frontend tests** ```tsx it('shows edit template info CTA after task success', async () => { expect(screen.getByRole('link', { name: '编辑模板信息' })).toHaveAttribute( 'href', '/zh/templates/101/edit', ) }) it('renders template edit route', async () => { window.history.replaceState({}, '', '/zh/templates/101/edit') render() await waitFor(() => { expect(screen.getByText('template-edit-page')).toBeInTheDocument() }) }) ``` - [ ] **Step 2: Run test to verify it fails** Run: ```bash cd /Users/yuchen/Documents/hairstylist/frontend npm test -- src/pages/dashboard/templates/__tests__/TemplateMakeTaskPage.test.tsx src/__tests__/App.test.tsx ``` Expected: FAIL because the button still points to detail and the edit route does not exist. - [ ] **Step 3: Add route and API contract changes** ```ts // routes.ts templateEdit: '/:locale/templates/:templateId/edit', ``` ```ts // contracts.ts export type TemplateBasicInfo = { id: string templateName: string status: 'DRAFT' | 'PUBLIC' category: 'LONG' | 'MEDIUM' | 'SHORT' | 'DYE' tags: string[] description: string coverUrl: string templateUrl: string } export type UpdateTemplateBasicInfoInput = { templateName: string status: 'DRAFT' | 'PUBLIC' category: 'LONG' | 'MEDIUM' | 'SHORT' | 'DYE' tags?: string[] description?: string } ``` ```ts // templates.ts export async function getTemplateBasicInfo(templateId: string): Promise { return http.get(`/templates/${encodeURIComponent(templateId)}`) } export async function updateTemplateBasicInfo( templateId: string, input: UpdateTemplateBasicInfoInput, ): Promise { return http.patch(`/templates/${encodeURIComponent(templateId)}`, input) } ``` - [ ] **Step 4: Change task success CTA copy and target** ```tsx {task?.taskStatus === 'STEP2_SUCCESS' ? ( {actions.editTemplate} ) : null} ``` ```json "actions": { "editTemplate": "编辑模板信息", "backToList": "返回模板列表" } ``` - [ ] **Step 5: Register the route in App** ```tsx } /> ``` - [ ] **Step 6: Run test to verify it passes** Run: ```bash cd /Users/yuchen/Documents/hairstylist/frontend npm test -- src/pages/dashboard/templates/__tests__/TemplateMakeTaskPage.test.tsx src/__tests__/App.test.tsx ``` Expected: PASS with the new edit CTA and route registration. - [ ] **Step 7: Commit** ```bash cd /Users/yuchen/Documents/hairstylist git add frontend/src/config/routes.ts \ frontend/src/App.tsx \ frontend/src/services/api/contracts.ts \ frontend/src/services/api/templates.ts \ frontend/src/pages/dashboard/templates/TemplateMakeTaskPage.tsx \ frontend/src/pages/dashboard/templates/__tests__/TemplateMakeTaskPage.test.tsx \ frontend/src/__tests__/App.test.tsx \ frontend/src/i18n/locales/zh/translation.json \ frontend/src/i18n/locales/ja/translation.json git commit -m "feat: route task success to template edit page" ``` ### Task 4: Build The Dedicated Template Edit Page **Files:** - Create: `frontend/src/pages/dashboard/templates/TemplateEditPage.tsx` - Create: `frontend/src/pages/dashboard/templates/__tests__/TemplateEditPage.test.tsx` - Modify: `frontend/src/i18n/locales/zh/translation.json` - Modify: `frontend/src/i18n/locales/ja/translation.json` **Interfaces:** - Consumes: - `getTemplateBasicInfo(templateId: string): Promise` - `updateTemplateBasicInfo(templateId: string, input: UpdateTemplateBasicInfoInput): Promise` - `routes.templateEdit` - Produces: - `TemplateEditPage` - [ ] **Step 1: Write the failing page tests** ```tsx it('loads template basic info into the edit form', async () => { getTemplateBasicInfo.mockResolvedValue({ id: '43', templateName: '柔和层次长发', status: 'DRAFT', category: 'LONG', tags: ['层次', '首次建议'], description: '适合轻盈感修饰', coverUrl: 'https://cdn.example.com/cover.jpg', templateUrl: 'https://cdn.example.com/template.zip', }) renderPage('/zh/templates/43/edit') expect(await screen.findByDisplayValue('柔和层次长发')).toBeInTheDocument() expect(screen.getByDisplayValue('层次, 首次建议')).toBeInTheDocument() }) it('submits only editable basic info fields', async () => { updateTemplateBasicInfo.mockResolvedValue({ id: '43', templateName: '空气感中长发', status: 'PUBLIC', category: 'MEDIUM', tags: ['空气感', '层次'], description: '新描述', coverUrl: 'https://cdn.example.com/cover.jpg', templateUrl: 'https://cdn.example.com/template.zip', }) fireEvent.change(screen.getByLabelText('模板名称 *'), { target: { value: '空气感中长发' } }) fireEvent.click(screen.getByRole('button', { name: '保存' })) await waitFor(() => { expect(updateTemplateBasicInfo).toHaveBeenCalledWith('43', { templateName: '空气感中长发', status: 'DRAFT', category: 'LONG', tags: ['层次', '首次建议'], description: '适合轻盈感修饰', }) }) }) ``` - [ ] **Step 2: Run test to verify it fails** Run: ```bash cd /Users/yuchen/Documents/hairstylist/frontend npm test -- src/pages/dashboard/templates/__tests__/TemplateEditPage.test.tsx ``` Expected: FAIL because `TemplateEditPage` does not exist yet. - [ ] **Step 3: Implement the new edit page** ```tsx export function TemplateEditPage() { const { locale, path, copy } = usePageContext() const { templateId } = useParams() const [form, setForm] = useState({ templateName: '', status: 'DRAFT' as const, category: 'LONG' as const, tags: '', description: '', }) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [error, setError] = useState('') const pageCopy = copy.templateEditPage useEffect(() => { if (!templateId) return getTemplateBasicInfo(templateId) .then((item) => { setForm({ templateName: item.templateName, status: item.status, category: item.category, tags: item.tags.join(', '), description: item.description, }) }) .catch(() => setError(pageCopy.states.notFound)) .finally(() => setLoading(false)) }, [templateId, pageCopy.states.notFound]) const handleSave = async () => { if (!templateId || saving) return setSaving(true) setError('') try { await updateTemplateBasicInfo(templateId, { templateName: form.templateName, status: form.status, category: form.category, tags: form.tags.split(',').map((tag) => tag.trim()).filter(Boolean), description: form.description, }) } catch (err) { setError(err instanceof Error ? err.message : pageCopy.errors.saveFailed) } finally { setSaving(false) } } return
template-edit-page
} ``` - [ ] **Step 4: Flesh out the page with the existing workbench form skeleton** ```tsx
{pageCopy.back}
{copy.common.cancel}
``` - [ ] **Step 5: Run test to verify it passes** Run: ```bash cd /Users/yuchen/Documents/hairstylist/frontend npm test -- src/pages/dashboard/templates/__tests__/TemplateEditPage.test.tsx ``` Expected: PASS with successful load and save coverage. - [ ] **Step 6: Commit** ```bash cd /Users/yuchen/Documents/hairstylist git add frontend/src/pages/dashboard/templates/TemplateEditPage.tsx \ frontend/src/pages/dashboard/templates/__tests__/TemplateEditPage.test.tsx \ frontend/src/i18n/locales/zh/translation.json \ frontend/src/i18n/locales/ja/translation.json git commit -m "feat: add template basic info edit page" ``` ### Task 5: Restore Template Detail Page To Display Role And Run Verification **Files:** - Modify: `frontend/src/pages/dashboard/templates/TemplateDetailPage.tsx` - Test: `frontend/src/pages/dashboard/templates/__tests__/TemplateDetailPage.test.tsx` - Test: `frontend/src/pages/dashboard/templates/__tests__/TemplateMakeTaskPage.test.tsx` - Test: `frontend/src/pages/dashboard/templates/__tests__/TemplateEditPage.test.tsx` - Test: `frontend/src/__tests__/App.test.tsx` - Test: `backend/src/modules/templates/templates.service.spec.ts` - Test: `backend/test/templates.e2e-spec.ts` **Interfaces:** - Consumes: - Existing detail page data loaders - New edit page route - Produces: - Detail page remains display-first, no inline save path - End-to-end validated feature set - [ ] **Step 1: Write a failing detail-page regression test** ```tsx it('does not render inline save controls on the detail page', async () => { renderPage('/zh/templates/43') await waitFor(() => { expect(screen.queryByRole('button', { name: '保存' })).not.toBeInTheDocument() }) }) ``` - [ ] **Step 2: Run test to verify it fails** Run: ```bash cd /Users/yuchen/Documents/hairstylist/frontend npm test -- src/pages/dashboard/templates/__tests__/TemplateDetailPage.test.tsx ``` Expected: FAIL because the current detail page still renders save actions and form controls. - [ ] **Step 3: Remove inline edit actions from the detail page** ```tsx // Replace the top-right save button group with a single read-only action group.
{pageCopy.edit}
``` ```tsx // Replace editable form controls with read-only display blocks.
{pageCopy.info.name} {template.name}
``` - [ ] **Step 4: Run the focused frontend and backend verification set** Run: ```bash cd /Users/yuchen/Documents/hairstylist/frontend npm test -- src/pages/dashboard/templates/__tests__/TemplateMakeTaskPage.test.tsx \ src/pages/dashboard/templates/__tests__/TemplateEditPage.test.tsx \ src/pages/dashboard/templates/__tests__/TemplateDetailPage.test.tsx \ src/__tests__/App.test.tsx npm run typecheck ``` ```bash cd /Users/yuchen/Documents/hairstylist/backend npm test -- --runInBand src/modules/templates/templates.service.spec.ts npm run test:e2e -- templates.e2e-spec.ts ``` Expected: - Frontend tests PASS - `npm run typecheck` PASS - Backend unit tests PASS - Backend e2e PASS - [ ] **Step 5: Commit** ```bash cd /Users/yuchen/Documents/hairstylist git add frontend/src/pages/dashboard/templates/TemplateDetailPage.tsx \ frontend/src/pages/dashboard/templates/__tests__/TemplateDetailPage.test.tsx git commit -m "refactor: separate template detail and edit flows" ``` ## Self-Review - Spec coverage: - New edit page: covered by Task 3 and Task 4 - Task-page CTA rename and reroute: covered by Task 3 - Backend GET/PATCH template basic info APIs: covered by Task 1 and Task 2 - Basic-info-only write boundary: covered by Task 1 and Task 2 - Detail page retained as display entry: covered by Task 5 - Placeholder scan: - No `TBD`, `TODO`, or deferred implementation text remains - Type consistency: - Uses one shared `TemplateBasicInfo` / `TemplateBasicInfoDto` shape - Uses one shared `UpdateTemplateBasicInfoInput` / `UpdateTemplateBasicInfoDto` write contract