diff --git a/docs/superpowers/plans/2026-09-04-backend.md b/docs/superpowers/plans/2026-09-04-backend.md index e0d2bb7..2b407da 100644 --- a/docs/superpowers/plans/2026-09-04-backend.md +++ b/docs/superpowers/plans/2026-09-04-backend.md @@ -71,14 +71,14 @@ docker-compose.yml # web, api(xN), postgres, redis, testprofile - Consumes: 无 - Produces: `config.Config{Addr, DatabaseURL, RedisURL string, JWTSecret []byte, AdminUser, AdminPassword, BooksDir, CacheDir string, ScanInterval time.Duration, UploadMaxMB int64}`;`config.Load() (*Config, error)`;`api.NewRouter(cfg *config.Config) *gin.Engine` -- [ ] **Step 1: 初始化 module** +- [x] **Step 1: 初始化 module** ```bash cd backend && go mod init booklib go get github.com/gin-gonic/gin@latest github.com/jackc/pgx/v5@latest github.com/redis/go-redis/v9@latest github.com/golang-jwt/jwt/v5@latest golang.org/x/crypto@latest ``` -- [ ] **Step 2: 写失败测试 `internal/api/router_test.go`** +- [x] **Step 2: 写失败测试 `internal/api/router_test.go`** ```go package api @@ -107,12 +107,12 @@ func TestHealthz(t *testing.T) { } ``` -- [ ] **Step 3: 跑测试确认失败** +- [x] **Step 3: 跑测试确认失败** Run: `cd backend && go test ./internal/api/ -run TestHealthz -v` Expected: 编译失败 "undefined: NewRouter" -- [ ] **Step 4: 实现 config + router** +- [x] **Step 4: 实现 config + router** `internal/config/config.go`: @@ -202,12 +202,12 @@ func NewRouter(cfg *config.Config) *gin.Engine { } ``` -- [ ] **Step 5: 跑测试确认通过** +- [x] **Step 5: 跑测试确认通过** Run: `cd backend && go test ./internal/api/ -v` Expected: PASS -- [ ] **Step 6: Commit** +- [x] **Step 6: Commit** ```bash git init -b main . && git add backend && git commit -m "feat(backend): module scaffold, config, gin router with healthz" @@ -268,7 +268,7 @@ func db.Connect(ctx, url string) (*pgxpool.Pool, error) func db.Migrate(ctx, p *pgxpool.Pool) error // 执行内嵌 schema.sql,幂等 ``` -- [ ] **Step 1: 起测试用 PG/Redis** +- [x] **Step 1: 起测试用 PG/Redis** `deploy/docker-compose.dev.yml`: @@ -287,7 +287,7 @@ services: Run: `docker compose -f deploy/docker-compose.dev.yml up -d --wait` Expected: 两个 healthy。后续所有集成测试用 `DATABASE_URL=postgres://lib:lib@localhost:5433/lib?sslmode=disable`。 -- [ ] **Step 2: 写 schema.sql(内嵌,幂等)** +- [x] **Step 2: 写 schema.sql(内嵌,幂等)** `internal/db/schema.sql`: @@ -315,7 +315,7 @@ CREATE TABLE IF NOT EXISTS reading_progress ( PRIMARY KEY (user_id, library_id, book_path)); ``` -- [ ] **Step 3: 写失败测试 `store_test.go`(全部 SQL 行为在此锁死)** +- [x] **Step 3: 写失败测试 `store_test.go`(全部 SQL 行为在此锁死)** ```go package store @@ -438,12 +438,12 @@ func TestProgressUpsertAndJoin(t *testing.T) { (BookMeta 只带 diff 需要的 ID/Size/ModTS/Format;page_count 走 GetBook,测试里已有覆盖。) -- [ ] **Step 4: 跑测试确认失败** +- [x] **Step 4: 跑测试确认失败** Run: `cd backend && DATABASE_URL=postgres://lib:lib@localhost:5433/lib?sslmode=disable go test ./internal/store/ -v` Expected: 编译失败 "booklib/internal/db: no such package" -- [ ] **Step 5: 实现 db.go 与 store.go** +- [x] **Step 5: 实现 db.go 与 store.go** `internal/db/db.go`: @@ -790,12 +790,12 @@ func (s *Store) GetProgress(ctx context.Context, userID, libID int64, bookPath s } ``` -- [ ] **Step 6: 跑测试确认通过** +- [x] **Step 6: 跑测试确认通过** Run: `cd backend && DATABASE_URL=postgres://lib:lib@localhost:5433/lib?sslmode=disable go test ./internal/store/ -v` Expected: 2 个测试 PASS(无 DATABASE_URL 时 SKIP) -- [ ] **Step 7: Commit** +- [x] **Step 7: Commit** ```bash git add backend deploy && git commit -m "feat(backend): pg schema + store queries incl. path-keyed progress" @@ -824,7 +824,7 @@ auth.Parse(secret []byte, token string) (*auth.Claims, error) seed.Admin(ctx context.Context, s *store.Store, user, pass string) error // users 为空才建;user/pass 空则跳过;失败仅由调用方记日志 ``` -- [ ] **Step 1: 写失败测试** +- [x] **Step 1: 写失败测试** `internal/auth/auth_test.go`: @@ -917,12 +917,12 @@ func TestSeedOnlyWhenEmpty(t *testing.T) { } ``` -- [ ] **Step 2: 跑,确认编译失败** +- [x] **Step 2: 跑,确认编译失败** Run: `cd backend && go test ./internal/auth/ ./internal/seed/ -v` Expected: FAIL — undefined -- [ ] **Step 3: 实现** +- [x] **Step 3: 实现** `internal/auth/auth.go`: @@ -1017,12 +1017,12 @@ func Admin(ctx context.Context, s *store.Store, user, pass string) error { } ``` -- [ ] **Step 4: 跑测试确认通过** +- [x] **Step 4: 跑测试确认通过** Run: `cd backend && go test ./internal/auth/ -v && DATABASE_URL=... go test ./internal/seed/ -v` Expected: PASS -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add backend && git commit -m "feat(backend): bcrypt+jwt auth package and admin seeding" @@ -1055,7 +1055,7 @@ api 中间件设置 ctx key: "uid" int64 / "role" string;helpers api.uid(c)/api. NewRouter(cfg *config.Config, st *store.Store, rdb *redispkg.R) *gin.Engine // scanner 参数 Task 9 再加 ``` -- [ ] **Step 1: 写失败测试 `auth_test.go`** +- [x] **Step 1: 写失败测试 `auth_test.go`** ```go package api @@ -1193,12 +1193,12 @@ func TestDisabledIsSafe(t *testing.T) { } ``` -- [ ] **Step 2: 跑,确认失败** +- [x] **Step 2: 跑,确认失败** Run: `cd backend && DATABASE_URL=... go test ./internal/api/ ./internal/redispkg/ -v` Expected: 编译失败 undefined: redispkg / NewRouter 签名不符 -- [ ] **Step 3: 实现 redispkg** +- [x] **Step 3: 实现 redispkg** `internal/redispkg/redis.go`: @@ -1283,7 +1283,7 @@ func (r *R) Lock(ctx context.Context, key string, ttl time.Duration) (func(), bo } ``` -- [ ] **Step 4: 实现 api struct/错误/中间件 + auth handlers,重写 NewRouter** +- [x] **Step 4: 实现 api struct/错误/中间件 + auth handlers,重写 NewRouter** `internal/api/api.go`: @@ -1442,12 +1442,12 @@ func NewRouter(cfg *config.Config, st *store.Store, rdb *redispkg.R) *gin.Engine } ``` -- [ ] **Step 5: 跑测试确认通过** +- [x] **Step 5: 跑测试确认通过** Run: `cd backend && DATABASE_URL=postgres://lib:lib@localhost:5433/lib?sslmode=disable REDIS_URL=redis://localhost:6380 go test ./internal/api/ ./internal/redispkg/ -v` Expected: PASS(REDIS_URL 不给也应 PASS — no-op 路径) -- [ ] **Step 6: Commit** +- [x] **Step 6: Commit** ```bash git add backend && git commit -m "feat(backend): jwt middleware, login with redis rate limit, /auth/me" @@ -1470,7 +1470,7 @@ git add backend && git commit -m "feat(backend): jwt middleware, login with redi `bookfile.FormatFromExt(name string) string` — `.cbz→"cbz" .pdf→"pdf" .epub→"epub" .txt→"txt" .md→"md"`,其他 `"..."` → `""` `bookfile.SafeName(s string) string` — 去目录、去控制符、trim、限长 200、保留空格中文 -- [ ] **Step 1: 写失败测试(bookfile 先行,纯函数)** +- [x] **Step 1: 写失败测试(bookfile 先行,纯函数)** `internal/bookfile/bookfile_test.go`: @@ -1511,7 +1511,7 @@ func TestSafeName(t *testing.T) { (`"tar.gz"→""`:扩展名是 `.gz` 不在白名单,正确。`"con\ntl.bin"` → base 后去 `\t` 得 `cntl.bin`。) -- [ ] **Step 2: 跑确认失败;实现 `bookfile.go`** +- [x] **Step 2: 跑确认失败;实现 `bookfile.go`** ```go package bookfile @@ -1560,7 +1560,7 @@ func SafeName(s string) string { Run: `go test ./internal/bookfile/ -v` → PASS。 -- [ ] **Step 3: 写失败测试 users/libraries(API)** +- [x] **Step 3: 写失败测试 users/libraries(API)** `internal/api/users_test.go`: @@ -1732,7 +1732,7 @@ func uploadBody(filename string, content []byte) (*bytes.Buffer, *multipart.Writ (`libraries_test.go` 需 import `bytes`;`httptest.NewRequest("POST", url, buf)` 直接收 `*bytes.Buffer`。) -- [ ] **Step 4: 跑确认失败;实现 users.go / libraries.go / router 接线** +- [x] **Step 4: 跑确认失败;实现 users.go / libraries.go / router 接线** 先修 `auth_test.go` 的 setupAPI(upload 测试需要,libRoot 校验依赖 BooksDir;auth_test.go 补 import `path/filepath`): @@ -2038,12 +2038,12 @@ func (a *api) uniquePath(root, name string) (string, error) { libs.POST("/:id/upload", a.adminOnly(), a.upload) ``` -- [ ] **Step 5: 跑测试确认通过** +- [x] **Step 5: 跑测试确认通过** Run: `cd backend && DATABASE_URL=... go test ./internal/api/ ./internal/bookfile/ -v` Expected: PASS -- [ ] **Step 6: Commit** +- [x] **Step 6: Commit** ```bash git add backend && git commit -m "feat(backend): user + library admin API, atomic sanitized upload" @@ -2068,7 +2068,7 @@ bookfile.NaturalLess(a, b string) bool bookfile.Hash(size, modTS int64) string // sha256("size:mtime") hex 前 16 字符 ``` -- [ ] **Step 1: 写失败测试** +- [x] **Step 1: 写失败测试** `internal/bookfile/zip_test.go`。测试内构造 zip 夹具(不落盘,bytes.Buffer + zip.Writer): @@ -2162,12 +2162,12 @@ func TestHash(t *testing.T) { 注意 `{"007","7"}` 与 `{"A","a"}` 两个用例:数字相等时按原始串比较(`"007" < "7"`,字典序),纯 ASCII 大小写按字节序。 -- [ ] **Step 2: 跑,确认失败** +- [x] **Step 2: 跑,确认失败** Run: `cd backend && go test ./internal/bookfile/ -run 'PageIndex|ReadEntry|Natural|Hash' -v` Expected: 编译失败 undefined -- [ ] **Step 3: 实现 `zip.go` 与 `hash.go`** +- [x] **Step 3: 实现 `zip.go` 与 `hash.go`** `internal/bookfile/zip.go`: @@ -2290,7 +2290,7 @@ func Hash(size, modTS int64) string { } ``` -- [ ] **Step 4: 跑确认通过;Commit** +- [x] **Step 4: 跑确认通过;Commit** Run: `cd backend && go test ./internal/bookfile/ -v` → PASS @@ -2315,7 +2315,7 @@ bookfile.CBZCover(f io.ReaderAt, size int64) (img []byte, ext string, err error) bookfile.EPUBCover(f io.ReaderAt, size int64) (img []byte, ext string, err error) // 无封面 → err ``` -- [ ] **Step 1: 写失败测试** +- [x] **Step 1: 写失败测试** `internal/bookfile/cover_test.go`(内存构造真 epub:container.xml + OPF + cover.png): @@ -2398,7 +2398,7 @@ func TestEPUBNoCover(t *testing.T) { } ``` -- [ ] **Step 2: 跑确认失败;实现 `cover.go`** +- [x] **Step 2: 跑确认失败;实现 `cover.go`** ```go package bookfile @@ -2530,7 +2530,7 @@ func EPUBCover(f io.ReaderAt, size int64) ([]byte, string, error) { (cover_test 中 `_ = url.PathUnescape` 那行不写,url 只在实现里 import。) -- [ ] **Step 3: 跑确认通过;Commit** +- [x] **Step 3: 跑确认通过;Commit** Run: `cd backend && go test ./internal/bookfile/ -v` → PASS @@ -2561,7 +2561,7 @@ bookfile.SweepStale(cacheDir string, live map[string]bool) (int, error) // 删 衍生文件永不算旧即永不原地改:更新的书 hash 变 → 新 key 目录;旧 key 目录由清扫删除。 -- [ ] **Step 1: 写失败测试** +- [x] **Step 1: 写失败测试** `internal/bookfile/cache_test.go`: @@ -2621,7 +2621,7 @@ func TestPaths(t *testing.T) { } ``` -- [ ] **Step 2: 跑确认失败;实现 `cache.go`** +- [x] **Step 2: 跑确认失败;实现 `cache.go`** ```go package bookfile @@ -2687,7 +2687,7 @@ func (s *Store) BookHashes(ctx context.Context) (map[int64][2]int64, error) { } ``` -- [ ] **Step 3: 跑确认通过;Commit** +- [x] **Step 3: 跑确认通过;Commit** Run: `cd backend && go test ./internal/bookfile/ -v` → PASS @@ -2718,7 +2718,7 @@ NewRouter(cfg *config.Config, st *store.Store, rdb *redispkg.R, sc *scanner.Scan 行为规则(spec §3.1/§6.1):锁内执行;单本失败 → `state=error` 继续;删行不删进度(path 键控);扫描尾清扫缓存。**不删磁盘文件**(删文件只有 API DELETE)。 -- [ ] **Step 1: 写失败测试 `scanner_test.go`(集成,依赖 PG;Redis 可为空走 no-op)** +- [x] **Step 1: 写失败测试 `scanner_test.go`(集成,依赖 PG;Redis 可为空走 no-op)** ```go package scanner @@ -2864,11 +2864,11 @@ func TestScanBrokenCBZStateError(t *testing.T) { } ``` -- [ ] **Step 2: 跑确认失败** +- [x] **Step 2: 跑确认失败** Run: `cd backend && DATABASE_URL=... go test ./internal/scanner/ -v` → 编译失败 -- [ ] **Step 3: 实现 scanner** +- [x] **Step 3: 实现 scanner** `internal/scanner/scanner.go`: @@ -3126,7 +3126,7 @@ func (s *Scanner) sweepCache(ctx context.Context) { 坏 cbz 的判定规则已合并在上面 `add`/`update` 终版中:`PageIndex` 失败(含 `ErrNotZip`、`ErrUnsafeZip`,Task 6 已定义)→ `state=error`;cover 提取失败只 log。 -- [ ] **Step 4: 接线 API** +- [x] **Step 4: 接线 API** `NewRouter` 加第 4 参 `sc *scanner.Scanner`,存进 `api` struct(`sc *scanner.Scanner`);`internal/api/libraries.go` 的 `scanLibrary` 整函数替换: @@ -3153,7 +3153,7 @@ func (a *api) scanLibrary(c *gin.Context) { (Task 10 会把 setupAPI 整体替换为 4 返回值终版,这里先保编译。)`api` struct 增加字段 `sc *scanner.Scanner`。 -- [ ] **Step 5: 跑全部后端测试确认通过;Commit** +- [x] **Step 5: 跑全部后端测试确认通过;Commit** Run: `cd backend && DATABASE_URL=... REDIS_URL=... go test -p 1 ./... -v` Expected: 全 PASS @@ -3217,7 +3217,7 @@ func setupAPI(t *testing.T) (*store.Store, *scanner.Scanner, http.Handler, strin 既有 call site 全部改为按需要接收:`_, _, h, _ := setupAPI(t)`(共 5 处:auth_test 2、users_test 2、libraries_test 1);`TestMemberCannotWriteUsers` 里那行无效的 `tok, _ := auth.Sign(...)` 删除,`auth_test.go` 的 `booklib/internal/auth` import 随之删除。setupAPI 需补 import:`path/filepath`、`booklib/internal/scanner`。 -- [ ] **Step 1: 写失败测试** +- [x] **Step 1: 写失败测试** `internal/api/books_test.go`(fixture 助手 `newLibrary/writeCBZ/scanNow/loginAs/adminToken` 定义在这里,Task 11/12 的测试文件复用): @@ -3372,7 +3372,7 @@ func loginAs(t *testing.T, h http.Handler, user, pass string) string { `adminToken` 终版(users_test.go 中):`func adminToken(t *testing.T, h http.Handler) string { return loginAs(t, h, "alice", "pw12345") }`。 -- [ ] **Step 2: 跑确认失败;实现 books.go + 路由** +- [x] **Step 2: 跑确认失败;实现 books.go + 路由** `internal/api/books.go`: @@ -3521,7 +3521,7 @@ func (a *api) deleteBook(c *gin.Context) { p.DELETE("/books/:id", a.adminOnly(), a.deleteBook) ``` -- [ ] **Step 3: 跑确认通过;Commit** +- [x] **Step 3: 跑确认通过;Commit** Run: `cd backend && DATABASE_URL=... go test ./internal/api/ -v` → PASS @@ -3551,7 +3551,7 @@ GET /api/books/:id/pages/:n → 第 n 页(0-based),落盘缓存后吐;immutable (*api) pageIndex(c, b store.Book, root string) ([]string, error) // redis pagesidx 缓存 7d ``` -- [ ] **Step 1: 写失败测试** +- [x] **Step 1: 写失败测试** `internal/api/content_test.go`(fixture 复用 books_test 的 `newLibrary/writeCBZ/scanNow/loginAs/adminToken`): @@ -3659,7 +3659,7 @@ func TestFileRangeETag(t *testing.T) { (`content_test.go` import 需含 `net/http/httptest`、`encoding/json`、`strings`、`os`、`path/filepath`、`testing`、`net/http`。) -- [ ] **Step 2: 跑确认失败;实现 content.go** +- [x] **Step 2: 跑确认失败;实现 content.go** ```go package api @@ -3853,7 +3853,7 @@ router 追加: p.GET("/books/:id/pages/:n", a.page) ``` -- [ ] **Step 3: 跑确认通过;Commit** +- [x] **Step 3: 跑确认通过;Commit** Run: `cd backend && DATABASE_URL=... REDIS_URL=... go test ./internal/api/ -v` → PASS @@ -3874,7 +3874,7 @@ git add backend && git commit -m "feat(backend): cover/file/pages endpoints — - Consumes: Task 2/10 - Produces:`PUT /api/books/:id/progress`、`GET /api/progress` -- [ ] **Step 1: 写失败测试** +- [x] **Step 1: 写失败测试** `internal/api/progress_test.go`: @@ -3959,7 +3959,7 @@ func TestProgressRoundTrip(t *testing.T) { (progress_test 不用 context,import 列表不要 `context`。) -- [ ] **Step 2: 跑确认失败;实现 progress.go** +- [x] **Step 2: 跑确认失败;实现 progress.go** ```go package api @@ -4028,7 +4028,7 @@ router 追加: p.GET("/progress", a.listProgress) ``` -- [ ] **Step 3: 跑确认通过;Commit** +- [x] **Step 3: 跑确认通过;Commit** Run: `cd backend && DATABASE_URL=... go test ./internal/api/ -v` → PASS @@ -4049,7 +4049,7 @@ git add backend && git commit -m "feat(backend): reading progress upsert + listi - Consumes: 全部 - Produces: 可执行 `/server`,env 驱动;`NewRouter` 最终签名不变。 -- [ ] **Step 1: config 单测(先失败)** +- [x] **Step 1: config 单测(先失败)** `internal/config/config_test.go`: @@ -4083,7 +4083,7 @@ func TestLoad(t *testing.T) { } ``` -- [ ] **Step 2: 实现 main.go** +- [x] **Step 2: 实现 main.go** `cmd/server/main.go`: @@ -4149,7 +4149,7 @@ func main() { } ``` -- [ ] **Step 3: PG 不可用 → 503(spec §9 降级)** +- [x] **Step 3: PG 不可用 → 503(spec §9 降级)** `internal/api/api.go` 追加(`import` 补 `errors`、`io`、`log`、`net`、`syscall`、`github.com/jackc/pgx/v5/pgxpool`): @@ -4168,7 +4168,7 @@ func dbErr(c *gin.Context, e error) { 把所有 handler 中 `"internal", "db error"` 的 500 分支替换为 `dbErr(c, <该处的 error 变量>)`:auth.go(login 1 处)、users.go(listUsers/createUser/deleteUser 各 1 处)、libraries.go(listLibraries)、books.go(listBooks/deleteBook)、content.go 无 DB 500 分支、progress.go(putProgress/listProgress)。替换后 `go test -p 1 ./...` 全绿即可(既有测试不覆盖 PG 宕机路径)。 -- [ ] **Step 4: 全量回归 + 本地起服烟测** +- [x] **Step 4: 全量回归 + 本地起服烟测** ```bash cd backend && go vet ./... && DATABASE_URL=postgres://lib:lib@localhost:5433/lib?sslmode=disable REDIS_URL=redis://localhost:6380 go test -p 1 ./... @@ -4187,7 +4187,7 @@ kill %1 Expected: 全 PASS + 末行输出含 `"title":"a"`。 -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add backend && git commit -m "feat(backend): main wiring, graceful shutdown, local smoke green" @@ -4206,7 +4206,7 @@ git add backend && git commit -m "feat(backend): main wiring, graceful shutdown, - Consumes: Task 13 的可运行服务 - Produces: `docker compose up -d --scale api=2` 起 web/api×2/pg/redis;`scripts/smoke.sh` 一条命令验收整个后端 -- [ ] **Step 1: 部署文件** +- [x] **Step 1: 部署文件** `deploy/Dockerfile.api`: @@ -4309,7 +4309,7 @@ ADMIN_PASSWORD=change-me-min-8 SCAN_INTERVAL_SEC=60 ``` -- [ ] **Step 2: 冒烟脚本 `scripts/smoke.sh`** +- [x] **Step 2: 冒烟脚本 `scripts/smoke.sh`** ```bash #!/usr/bin/env bash @@ -4370,7 +4370,7 @@ curl -fsS -o /dev/null -X DELETE "$API/books/$BID" -H "$AUTH" || die "delete" say "ALL SMOKE TESTS PASSED" ``` -- [ ] **Step 3: 跑通** +- [x] **Step 3: 跑通** ```bash cp .env.example .env && sed -i '' -E "s/change-me-openssl-rand-hex-32/$(openssl rand -hex 32)/; s/^ADMIN_PASSWORD=.*/ADMIN_PASSWORD=smokeadmin1/" .env @@ -4382,7 +4382,7 @@ docker compose down -v && rm -f library_upload_note.txt Expected: `ALL SMOKE TESTS PASSED`。(Linux 上 `sed -i` 去掉 `''` 参数。) -- [ ] **Step 4: Commit** +- [x] **Step 4: Commit** ```bash git add docker-compose.yml .env.example deploy scripts && git commit -m "feat(deploy): compose w/ nginx api-prefix ingress, scaled stateless api, e2e smoke" diff --git a/docs/superpowers/plans/2026-09-04-frontend.md b/docs/superpowers/plans/2026-09-04-frontend.md index d6be27a..2808bfc 100644 --- a/docs/superpowers/plans/2026-09-04-frontend.md +++ b/docs/superpowers/plans/2026-09-04-frontend.md @@ -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 }`;`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/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 .. diff --git a/docs/superpowers/specs/2026-09-04-book-comic-library-design.md b/docs/superpowers/specs/2026-09-04-book-comic-library-design.md index 68d1a1d..9cd11f5 100644 --- a/docs/superpowers/specs/2026-09-04-book-comic-library-design.md +++ b/docs/superpowers/specs/2026-09-04-book-comic-library-design.md @@ -1,7 +1,7 @@ # 个人书库 / 漫画库 — 设计文档 日期:2026-09-04 -状态:待评审 +状态:已实现(Plan 1 backend+deploy、Plan 2 frontend 均已合并至 master) ## 1. 概述