docs: tick completed plan checkboxes, mark spec as implemented

This commit is contained in:
2026-09-07 17:27:24 +08:00
parent 73af5c1d78
commit 22ae1717f4
3 changed files with 138 additions and 138 deletions
+74 -74
View File
@@ -110,7 +110,7 @@ README.md # Task 12:补前端 dev/部署说明
- Consumes: 无
- Produces: 可 `npm run check` 的工程;`npm run dev`(:5173,`/api` 代理到 :8080)
- [ ] **Step 1: 初始化工程并装依赖(一次性,后续任务不再装)**
- [x] **Step 1: 初始化工程并装依赖(一次性,后续任务不再装)**
```bash
cd web
@@ -124,7 +124,7 @@ npm install -D typescript vite @vitejs/plugin-react tailwindcss @tailwindcss/vit
Expected: 无 error 级输出;`node_modules`、`package-lock.json` 生成。epubjs 锁 0.3.93(上游停更,锁死可复现);其余用 latest。若 `npm install` 报 ERESOLVE(vite-plugin-pwa 与已装 vite 大版本 peer 冲突):降级安装最后一个兼容版 `npm i -D vite-plugin-pwa@0.21.2`,Task 12 的 `VitePWA({...})` 配置键在 0.21→1.x 间未变,后续代码不用改。
- [ ] **Step 2: 写配置文件**
- [x] **Step 2: 写配置文件**
`web/.gitignore`:
@@ -180,7 +180,7 @@ export default defineConfig({
});
```
- [ ] **Step 3: 写入口文件**
- [x] **Step 3: 写入口文件**
`web/index.html`:
@@ -247,12 +247,12 @@ export default function App() {
}
```
- [ ] **Step 4: 跑门槛确认通过**
- [x] **Step 4: 跑门槛确认通过**
Run: `cd web && npm run check`
Expected: `tsc` 无错、vitest `No test files匹配 → passWithNoTests OK`、`vite build` 产出 `dist/`。
- [ ] **Step 5: Commit**
- [x] **Step 5: Commit**
```bash
git add web && git commit -m "feat(web): vite+react+ts+tailwind4 scaffold, vitest, /api dev proxy"
@@ -313,7 +313,7 @@ export const api: {
}
```
- [ ] **Step 1: 写失败测试 `web/test/client.test.ts`**
- [x] **Step 1: 写失败测试 `web/test/client.test.ts`**
```ts
import { afterEach, beforeEach, expect, it, vi } from "vitest";
@@ -380,12 +380,12 @@ it("putProgress keepalive 透传 + JSON body", async () => {
});
```
- [ ] **Step 2: 跑,确认失败**
- [x] **Step 2: 跑,确认失败**
Run: `cd web && npx vitest run test/client.test.ts`
Expected: FAIL(`../src/api/client` 不存在)
- [ ] **Step 3: 实现 `src/api/types.ts` 与 `src/api/client.ts`**
- [x] **Step 3: 实现 `src/api/types.ts` 与 `src/api/client.ts`**
`src/api/types.ts` — 内容即 Interfaces 块中 types.ts 声明,原样落成(加 `export` 每行):
@@ -600,12 +600,12 @@ export const api = {
};
```
- [ ] **Step 4: 跑门槛确认通过**
- [x] **Step 4: 跑门槛确认通过**
Run: `cd web && npm run check`
Expected: vitest 7 个 case 全 PASS,tsc/build 无错。
- [ ] **Step 5: Commit**
- [x] **Step 5: Commit**
```bash
git add web && git commit -m "feat(web): typed api client with bearer token, 401 logout, binary channels"
@@ -630,7 +630,7 @@ git add web && git commit -m "feat(web): typed api client with bearer token, 401
UI 装配任务无纯逻辑单测,门槛 = `npm run check`。
- [ ] **Step 1: 实现 `src/lib/qc.ts`**
- [x] **Step 1: 实现 `src/lib/qc.ts`**
```ts
import { QueryClient } from "@tanstack/react-query";
@@ -640,7 +640,7 @@ export const queryClient = new QueryClient({
});
```
- [ ] **Step 2: 实现 `src/components/ui.ts`**
- [x] **Step 2: 实现 `src/components/ui.ts`**
```ts
export const btn =
@@ -652,7 +652,7 @@ export const input =
export const card = "rounded-xl border border-zinc-800 bg-zinc-900";
```
- [ ] **Step 3: 实现 `src/components/Toaster.tsx`**
- [x] **Step 3: 实现 `src/components/Toaster.tsx`**
```tsx
import { useEffect, useState, type ReactNode } from "react";
@@ -700,7 +700,7 @@ export function Toaster() {
}
```
- [ ] **Step 4: 实现 `src/components/ErrorBoundary.tsx`**
- [x] **Step 4: 实现 `src/components/ErrorBoundary.tsx`**
```tsx
import { Component, type ReactNode } from "react";
@@ -731,7 +731,7 @@ export class ErrorBoundary extends Component<{ children: ReactNode }, { err: Err
}
```
- [ ] **Step 5: 实现 `src/auth/AuthContext.tsx`**
- [x] **Step 5: 实现 `src/auth/AuthContext.tsx`**
```tsx
import { useQuery, useQueryClient } from "@tanstack/react-query";
@@ -808,7 +808,7 @@ export function RequireAdmin({ children }: { children: ReactNode }) {
}
```
- [ ] **Step 6: 实现 `src/pages/Login.tsx`**
- [x] **Step 6: 实现 `src/pages/Login.tsx`**
```tsx
import { useState, type FormEvent } from "react";
@@ -857,7 +857,7 @@ export default function Login() {
}
```
- [ ] **Step 7: 替换 `src/main.tsx` 与 `src/App.tsx`**
- [x] **Step 7: 替换 `src/main.tsx` 与 `src/App.tsx`**
`src/main.tsx`:
@@ -914,12 +914,12 @@ export default function App() {
}
```
- [ ] **Step 8: 门槛 + 人工冒烟(可选)**
- [x] **Step 8: 门槛 + 人工冒烟(可选)**
Run: `cd web && npm run check` → 全绿。
人工(有环境时):起 dev 后端 + `npm run dev`,浏览器访问 :5173,未登录应跳 `/login`,错误密码出红色 toast。
- [ ] **Step 9: Commit**
- [x] **Step 9: Commit**
```bash
git add web && git commit -m "feat(web): auth context, login page, route guards, toast/error shell"
@@ -941,7 +941,7 @@ git add web && git commit -m "feat(web): auth context, login page, route guards,
- `Cover({ book, className })`(3:4 占位、失败显示重试按钮,spec §9)
- `group.ts`:`interface Group { dir: string; books: Book[] }`;`groupByDir(books: Book[]): Group[]`
- [ ] **Step 1: 写失败测试**
- [x] **Step 1: 写失败测试**
`test/group.test.ts`:
@@ -1005,12 +1005,12 @@ it("失败不入缓存,重试重新请求", async () => {
});
```
- [ ] **Step 2: 跑,确认失败**
- [x] **Step 2: 跑,确认失败**
Run: `cd web && npx vitest run test/group.test.ts test/authImage.test.ts`
Expected: FAIL(模块不存在)
- [ ] **Step 3: 实现 `src/lib/group.ts`**
- [x] **Step 3: 实现 `src/lib/group.ts`**
```ts
import type { Book } from "../api/types";
@@ -1038,7 +1038,7 @@ export function groupByDir(books: Book[]): Group[] {
}
```
- [ ] **Step 4: 实现 `src/lib/authImage.ts` 与 `src/components/Cover.tsx`**
- [x] **Step 4: 实现 `src/lib/authImage.ts` 与 `src/components/Cover.tsx`**
`src/lib/authImage.ts`:
@@ -1120,12 +1120,12 @@ export function Cover({ book, className }: { book: Book; className?: string }) {
}
```
- [ ] **Step 5: 跑门槛确认通过**
- [x] **Step 5: 跑门槛确认通过**
Run: `cd web && npm run check`
Expected: 新增 4 个用例 PASS;build 无错。
- [ ] **Step 6: Commit**
- [x] **Step 6: Commit**
```bash
git add web && git commit -m "feat(web): authed blob image pipeline with LRU, cover card, dir grouping"
@@ -1143,7 +1143,7 @@ git add web && git commit -m "feat(web): authed blob image pipeline with LRU, co
- Consumes: Task 2 `api`;Task 3 `useAuth`、`btn/btnPrimary/input/card`;Task 4 `Cover`、`groupByDir`;`queryClient`
- Produces: 路由 `/`(书架);`TopBar({ right?: ReactNode })`(admin 页在 Task 11 复用);TanStack Query key 约定:`["libraries"]`、`["books", libId, q]`、`["progress"]`、`["book", id]`
- [ ] **Step 1: 实现 `src/components/TopBar.tsx`**
- [x] **Step 1: 实现 `src/components/TopBar.tsx`**
```tsx
import type { ReactNode } from "react";
@@ -1183,7 +1183,7 @@ export function TopBar({ right }: { right?: ReactNode }) {
}
```
- [ ] **Step 2: 实现 `src/pages/Shelf.tsx`**
- [x] **Step 2: 实现 `src/pages/Shelf.tsx`**
```tsx
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
@@ -1384,7 +1384,7 @@ export default function Shelf() {
}
```
- [ ] **Step 3: 替换 `src/App.tsx`**
- [x] **Step 3: 替换 `src/App.tsx`**
```tsx
import { Navigate, Route, Routes } from "react-router-dom";
@@ -1429,12 +1429,12 @@ export default function App() {
}
```
- [ ] **Step 4: 门槛**
- [x] **Step 4: 门槛**
Run: `cd web && npm run check`
Expected: 全绿。
- [ ] **Step 5: Commit**
- [x] **Step 5: Commit**
```bash
git add web && git commit -m "feat(web): shelf with library tabs, search, dir groups, covers, continue-reading, admin delete/scan"
@@ -1456,7 +1456,7 @@ git add web && git commit -m "feat(web): shelf with library tabs, search, dir gr
- `lib/useProgress.ts`:`interface ReaderProps { book: Book; initialLocator?: Record<string, unknown> }`;`useProgressSaver(bookId: number): ProgressSaver`(自动挂 pagehide/visibilitychange→flush)
- `pages/Reader.tsx`:`/book/:id` 壳;4 个 reader 任务各向 `READERS` 表加一行 lazy
- [ ] **Step 1: 写失败测试 `test/progress.test.ts`**
- [x] **Step 1: 写失败测试 `test/progress.test.ts`**
```ts
import { afterEach, beforeEach, expect, it, vi } from "vitest";
@@ -1503,12 +1503,12 @@ it("flush 立刻尾随发送且 keepalive=true;重复值去重;清空挂起", ()
});
```
- [ ] **Step 2: 跑,确认失败**
- [x] **Step 2: 跑,确认失败**
Run: `cd web && npx vitest run test/progress.test.ts`
Expected: FAIL(模块不存在)
- [ ] **Step 3: 实现 `src/lib/progress.ts`**
- [x] **Step 3: 实现 `src/lib/progress.ts`**
```ts
export type ProgressSend = (
@@ -1570,7 +1570,7 @@ export function createProgressSaver(send: ProgressSend, intervalMs = 5000): Prog
}
```
- [ ] **Step 4: 实现 `src/lib/useProgress.ts`**
- [x] **Step 4: 实现 `src/lib/useProgress.ts`**
```ts
import { useQueryClient } from "@tanstack/react-query";
@@ -1613,7 +1613,7 @@ export function useProgressSaver(bookId: number): ProgressSaver {
}
```
- [ ] **Step 5: 实现 `src/pages/Reader.tsx`**
- [x] **Step 5: 实现 `src/pages/Reader.tsx`**
每个 reader 任务会向 `READERS` 加一行(本任务是空表 → 一律"未知格式"占位,先验证壳)。`initialLocator` 由壳统一解析(等 `["progress"]` 加载完再挂载 reader,消除恢复竞态)。
@@ -1716,7 +1716,7 @@ function Msg({ text, retry }: { text: string; retry?: () => void }) {
}
```
- [ ] **Step 6: 替换 `src/App.tsx`**
- [x] **Step 6: 替换 `src/App.tsx`**
```tsx
import { Navigate, Route, Routes } from "react-router-dom";
@@ -1758,12 +1758,12 @@ export default function App() {
}
```
- [ ] **Step 7: 跑门槛确认通过**
- [x] **Step 7: 跑门槛确认通过**
Run: `cd web && npm run check`
Expected: progress 2 用例 PASS;build 无错(空 READERS 是合法态)。
- [ ] **Step 8: Commit**
- [x] **Step 8: Commit**
```bash
git add web && git commit -m "feat(web): throttled progress saver with keepalive flush, reader dispatch shell"
@@ -1782,7 +1782,7 @@ git add web && git commit -m "feat(web): throttled progress saver with keepalive
- Consumes: `api.pageCount(book.pages_url)`→`{count}`;`formatPageUrl(book.page_url_fmt, n)`;`fetchObjectUrl`;`useProgressSaver`/`ReaderProps`
- Produces: `lib/virt.ts`:`class PageHeights { constructor(n: number, est: number); set(i: number, h: number): number /* 高度增量 */; offset(i: number): number; total(): number; pageAt(y: number): number; range(scrollTop: number, viewport: number, overscan: number): [number, number] /* [start, end) */ }`;`readers/CbzReader.tsx` default export `(p: ReaderProps) => JSX`,locator `{page}`(0 基),percent `(page+1)/count`
- [ ] **Step 1: 写失败测试 `test/virt.test.ts`**
- [x] **Step 1: 写失败测试 `test/virt.test.ts`**
```ts
import { expect, it } from "vitest";
@@ -1818,12 +1818,12 @@ it("n=0 时一切归空", () => {
});
```
- [ ] **Step 2: 跑,确认失败**
- [x] **Step 2: 跑,确认失败**
Run: `cd web && npx vitest run test/virt.test.ts`
Expected: FAIL(模块不存在)
- [ ] **Step 3: 实现 `src/lib/virt.ts`**
- [x] **Step 3: 实现 `src/lib/virt.ts`**
```ts
// CBZ 竖滚虚拟列表的高度模型:估高起步,页图加载完用真实高校正,
@@ -1891,7 +1891,7 @@ export class PageHeights {
}
```
- [ ] **Step 4: 实现 `src/readers/CbzReader.tsx`**
- [x] **Step 4: 实现 `src/readers/CbzReader.tsx`**
```tsx
import { useQuery } from "@tanstack/react-query";
@@ -2039,7 +2039,7 @@ export default function CbzReader({ book, initialLocator }: ReaderProps) {
}
```
- [ ] **Step 5: `src/pages/Reader.tsx` 接线 cbz**
- [x] **Step 5: `src/pages/Reader.tsx` 接线 cbz**
先把 react import 行还原为含 `lazy`(见 Task 6 注释),再整块替换 READERS:
@@ -2049,12 +2049,12 @@ const READERS: Partial<Record<Format, LazyExoticComponent<ComponentType<ReaderPr
};
```
- [ ] **Step 6: 跑门槛确认通过**
- [x] **Step 6: 跑门槛确认通过**
Run: `cd web && npm run check`
Expected: virt 3 用例 PASS;build 无错。
- [ ] **Step 7: Commit**
- [x] **Step 7: Commit**
```bash
git add web && git commit -m "feat(web): cbz reader — virtual scroll with height correction, prefetch, progress restore"
@@ -2072,7 +2072,7 @@ git add web && git commit -m "feat(web): cbz reader — virtual scroll with heig
- Consumes: `apiRaw(book.file_url).text()`;`useProgressSaver`/`ReaderProps`
- Produces: `readers/TextReader.tsx` default export `(p: ReaderProps)`,同时服务 txt 与 md;locator `{scrollFraction}`(0..1),percent 同值
- [ ] **Step 1: 实现 `src/readers/TextReader.tsx`**
- [x] **Step 1: 实现 `src/readers/TextReader.tsx`**
```tsx
import DOMPurify from "dompurify";
@@ -2151,7 +2151,7 @@ export default function TextReader({ book, initialLocator }: ReaderProps) {
}
```
- [ ] **Step 2: `index.css` 末尾追加 md 正文样式(Tailwind4 无 typography 插件,不新增依赖)**
- [x] **Step 2: `index.css` 末尾追加 md 正文样式(Tailwind4 无 typography 插件,不新增依赖)**
```css
/* markdown 正文(仅 .md-body 作用域) */
@@ -2172,7 +2172,7 @@ export default function TextReader({ book, initialLocator }: ReaderProps) {
.md-body th, .md-body td { @apply border border-zinc-700 px-2 py-1 text-left; }
```
- [ ] **Step 3: `src/pages/Reader.tsx` 接线**
- [x] **Step 3: `src/pages/Reader.tsx` 接线**
`READERS` 整块替换:
@@ -2184,12 +2184,12 @@ const READERS: Partial<Record<Format, LazyExoticComponent<ComponentType<ReaderPr
};
```
- [ ] **Step 4: 门槛**
- [x] **Step 4: 门槛**
Run: `cd web && npm run check`
Expected: 全绿(vitest 无新增;门槛=编译+构建)。
- [ ] **Step 5: Commit**
- [x] **Step 5: Commit**
```bash
git add web && git commit -m "feat(web): txt/md reader with dompurified markdown and scroll-fraction progress"
@@ -2207,7 +2207,7 @@ git add web && git commit -m "feat(web): txt/md reader with dompurified markdown
- Consumes: `apiRaw(book.file_url).arrayBuffer()`;`useProgressSaver`/`ReaderProps`
- Produces: `readers/PdfReader.tsx` default export `(p: ReaderProps)`;locator `{page}`(0 基,与 CBZ 统一 0 基),percent `(page+1)/numPages`
- [ ] **Step 1: 实现 `src/readers/PdfReader.tsx`**
- [x] **Step 1: 实现 `src/readers/PdfReader.tsx`**
```tsx
import { useEffect, useRef, useState, type KeyboardEvent } from "react";
@@ -2360,7 +2360,7 @@ export default function PdfReader({ book, initialLocator }: ReaderProps) {
}
```
- [ ] **Step 2: `src/pages/Reader.tsx` 接线**
- [x] **Step 2: `src/pages/Reader.tsx` 接线**
`READERS` 整块替换:
@@ -2373,12 +2373,12 @@ const READERS: Partial<Record<Format, LazyExoticComponent<ComponentType<ReaderPr
};
```
- [ ] **Step 3: 门槛**
- [x] **Step 3: 门槛**
Run: `cd web && npm run check`
Expected: 全绿。若 `p.render({canvasContext, viewport})` 在已装 pdfjs-dist 大版本下类型不符(历史上参数名有 `canvas`/`canvasContext` 差异),按 node_modules 内 `types/display.d.xml`→`RenderParameters` 实际字段名调整这一处,**不改其余逻辑**。
- [ ] **Step 4: Commit**
- [x] **Step 4: Commit**
```bash
git add web && git commit -m "feat(web): pdf reader with dpr-aware canvas rendering and keyboard paging"
@@ -2396,13 +2396,13 @@ git add web && git commit -m "feat(web): pdf reader with dpr-aware canvas render
- Consumes: `apiRaw(book.file_url).arrayBuffer()`;`useProgressSaver`/`ReaderProps`
- Produces: `readers/EpubReader.tsx` default export `(p: ReaderProps)`;locator `{cfi}`,percent `(sectionIndex+1)/spineLength`
- [ ] **Step 1: 写 `src/types/shims.d.ts`(epubjs 0.3 类型不完备,兜底声明,包内自带类型时自动失效)**
- [x] **Step 1: 写 `src/types/shims.d.ts`(epubjs 0.3 类型不完备,兜底声明,包内自带类型时自动失效)**
```ts
declare module "epubjs";
```
- [ ] **Step 2: 实现 `src/readers/EpubReader.tsx`**
- [x] **Step 2: 实现 `src/readers/EpubReader.tsx`**
```tsx
import { useEffect, useRef, useState } from "react";
@@ -2509,7 +2509,7 @@ export default function EpubReader({ book, initialLocator }: ReaderProps) {
}
```
- [ ] **Step 3: `src/pages/Reader.tsx` 接线**
- [x] **Step 3: `src/pages/Reader.tsx` 接线**
`READERS` 整块替换:
@@ -2523,12 +2523,12 @@ const READERS: Partial<Record<Format, LazyExoticComponent<ComponentType<ReaderPr
};
```
- [ ] **Step 4: 门槛**
- [x] **Step 4: 门槛**
Run: `cd web && npm run check`
Expected: 全绿。
- [ ] **Step 5: Commit**
- [x] **Step 5: Commit**
```bash
git add web && git commit -m "feat(web): epub reader via epubjs arraybuffer bootstrap with cfi progress"
@@ -2546,7 +2546,7 @@ git add web && git commit -m "feat(web): epub reader via epubjs arraybuffer boot
- Consumes: Task 2 `api.*`;Task 3 `useAuth/RequireAdmin`;Task 5 `TopBar`、ui 常量
- Produces: 路由 `/admin/users`、`/admin/libraries`(TopBar 已挂链接);query key `["users"]`
- [ ] **Step 1: 实现 `src/pages/admin/Users.tsx`**
- [x] **Step 1: 实现 `src/pages/admin/Users.tsx`**
```tsx
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
@@ -2644,7 +2644,7 @@ export default function AdminUsers() {
}
```
- [ ] **Step 2: 实现 `src/pages/admin/Libraries.tsx`**
- [x] **Step 2: 实现 `src/pages/admin/Libraries.tsx`**
```tsx
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
@@ -2756,7 +2756,7 @@ export default function AdminLibraries() {
}
```
- [ ] **Step 3: 替换 `src/App.tsx`(补 admin 路由)**
- [x] **Step 3: 替换 `src/App.tsx`(补 admin 路由)**
```tsx
import { Navigate, Route, Routes } from "react-router-dom";
@@ -2820,12 +2820,12 @@ export default function App() {
}
```
- [ ] **Step 4: 门槛**
- [x] **Step 4: 门槛**
Run: `cd web && npm run check`
Expected: 全绿。人工(可选):member 登录看不到 admin 链接,直敲 /admin/users 被弹回 `/`。
- [ ] **Step 5: Commit**
- [x] **Step 5: Commit**
```bash
git add web && git commit -m "feat(web): admin users CRUD + libraries create/scan/upload"
@@ -2844,7 +2844,7 @@ git add web && git commit -m "feat(web): admin users CRUD + libraries create/sca
- Consumes: Task 1-11 的全部产物;`web/dist`(vite build)
- Produces: 生产镜像 = SPA + nginx(`/api/` 反代);SW:不可变资源 cache-first、其余 /api network-first(spec §6.3)
- [ ] **Step 1: 替换 `web/vite.config.ts`**
- [x] **Step 1: 替换 `web/vite.config.ts`**
```ts
import react from "@vitejs/plugin-react";
@@ -2899,13 +2899,13 @@ export default defineConfig({
});
```
- [ ] **Step 2: `web/tsconfig.json` 的 `"types"` 行替换**
- [x] **Step 2: `web/tsconfig.json` 的 `"types"` 行替换**
```json
"types": ["vite/client", "vite-plugin-pwa/client"],
```
- [ ] **Step 3: 新建 `web/src/pwa.ts` 并在 `main.tsx` 引入**
- [x] **Step 3: 新建 `web/src/pwa.ts` 并在 `main.tsx` 引入**
`src/pwa.ts`:
@@ -2922,7 +2922,7 @@ registerSW({ immediate: true });
import "./pwa";
```
- [ ] **Step 4: `src/auth/AuthContext.tsx` logout 加清 SW 缓存(同设备多账号隔离)**
- [x] **Step 4: `src/auth/AuthContext.tsx` logout 加清 SW 缓存(同设备多账号隔离)**
`logout` 实现整块替换为:
@@ -2938,12 +2938,12 @@ import "./pwa";
},
```
- [ ] **Step 5: 构建验证 SW 产物**
- [x] **Step 5: 构建验证 SW 产物**
Run: `cd web && npm run check && ls dist | grep -E 'sw\.js|workbox|manifest'`
Expected: check 全绿;列出 `sw.js`、`workbox-*.js`、`manifest.webmanifest`。
- [ ] **Step 6: 替换 `deploy/Dockerfile.web`、删除占位、补 .dockerignore**
- [x] **Step 6: 替换 `deploy/Dockerfile.web`、删除占位、补 .dockerignore**
```
FROM node:22-alpine AS build
@@ -2970,7 +2970,7 @@ web/dist
web/dev-dist
```
- [ ] **Step 7: 写全栈冒烟 `scripts/smoke-web.sh`**
- [x] **Step 7: 写全栈冒烟 `scripts/smoke-web.sh`**
```bash
#!/usr/bin/env bash
@@ -3015,7 +3015,7 @@ curl -fsS "$BASE/manifest.webmanifest" >/dev/null || die "manifest 缺失"
say "ALL WEB SMOKE PASSED"
```
- [ ] **Step 8: 跑冒烟**
- [x] **Step 8: 跑冒烟**
```bash
chmod +x scripts/smoke-web.sh
@@ -3026,7 +3026,7 @@ docker compose down
Expected: `ALL WEB SMOKE PASSED`(compose 栈与 Plan 1 `scripts/smoke.sh` 共用,若后端栈在跑先 down)。
- [ ] **Step 9: README 追加前端章节**
- [x] **Step 9: README 追加前端章节**
在 `README.md` 末尾追加(若已有相关章节则合并去重,不重复端口说明):
@@ -3047,7 +3047,7 @@ Expected: `ALL WEB SMOKE PASSED`(compose 栈与 Plan 1 `scripts/smoke.sh` 共用
- 冒烟:`bash scripts/smoke.sh`(后端直连)、`bash scripts/smoke-web.sh`(经 nginx 全栈,需 :8080 空闲)。
```
- [ ] **Step 10: 最终门槛 + Commit**
- [x] **Step 10: 最终门槛 + Commit**
```bash
cd web && npm run check && cd ..