From eec902b7433c30829ba504e312fd577c23ec93bf Mon Sep 17 00:00:00 2001 From: XingfenD Date: Wed, 30 Sep 2026 19:01:45 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20P7=20admin=20console=20spec=20+=20plan?= =?UTF-8?q?=20=E2=80=94=20audit=20via=20admin=20route=20group=20(owner=20c?= =?UTF-8?q?all),=20last-admin=20guard,=20user=20list=20w/=20role=20ops?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ROADMAP.md | 1 + docs/plans/2026-09-30-admin-console.md | 112 +++++++++++++ docs/specs/2026-09-30-admin-console-design.md | 153 ++++++++++++++++++ 3 files changed, 266 insertions(+) create mode 100644 docs/plans/2026-09-30-admin-console.md create mode 100644 docs/specs/2026-09-30-admin-console-design.md diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 7c08d50..c812fc8 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -56,3 +56,4 @@ | 创作者中心 `/creator`(维护者直接需求) | `docs/specs/2026-09-30-creator-center-design.md` | `docs/plans/2026-09-30-creator-center.md`(已执行,2026-09-30) | | P2 CI+测试基线 | `docs/specs/2026-09-30-ci-test-baseline-design.md` | `docs/plans/2026-09-30-ci-test-baseline.md`(已执行,2026-09-30) | | P3 备份+可观测 | `docs/specs/2026-09-30-backup-observability-design.md` | `docs/plans/2026-09-30-backup-observability.md` | +| P7 管理后台增强 | `docs/specs/2026-09-30-admin-console-design.md` | `docs/plans/2026-09-30-admin-console.md` | diff --git a/docs/plans/2026-09-30-admin-console.md b/docs/plans/2026-09-30-admin-console.md new file mode 100644 index 0000000..d3679ea --- /dev/null +++ b/docs/plans/2026-09-30-admin-console.md @@ -0,0 +1,112 @@ +# P7 管理后台增强 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** crearte-server 0.12.0(audit_log 表 + 审计中间件 + admin 路由收敛组 + 用户列表/角色端点 + 最后 admin 护栏)与 crearte 0.18.0(AdminUsersView / AdminAuditView / apiRepo 三方法 / e2e 扩流),端到端验证后合主。deploy 零改动。 + +**Architecture:** 见 spec `docs/specs/2026-09-30-admin-console-design.md`。核心决策:D-A 只记管理员动作;D-B 中间件统一记录 + **维护者定稿:admin 路由收敛为 gin 组(空前缀 `engine.Group("", RequireAdmin, AuditMiddleware)`),组内继续用既有 `RouteAdmin*` 全路径常量注册,禁止逐条手挂**;D-C 列表=身份字段+角色操作;D-D 唯一 admin 不可降级(service 层,CLI/API 双覆盖)。SetUserRole 现成语义:`UpdateRoleByEmail` SQL 自带 `token_version+1`(作废被改者全部 token),**不要**另加 `BumpTokenVersion` 调用。 + +**Tech Stack:** Go 1.24(gin 1.11 / pgx 5.8;**禁新增第三方依赖**)、Vue 3 + vue-router + vitest + Playwright(前端既有栈)、migrations 0010。 + +## Global Constraints + +- 工作目录:`/root/.openclaw/workspace/coder/crearte-monorepo/crearte-server/src`(Go 源码根)与 `…/crearte/src`(前端,package.json 在 src/ 下)。 +- 分支:两仓各从 `master` 切 `feat/admin-console`;**master 上禁直接提交**(各仓 AGENTS.md);合回一律 `--no-ff` 由控制者执行。 +- **Go 命令一律走容器**(AGENTS.md 本地约定,proxy.golang.org 不可达): + `docker run --rm -v /root/.openclaw/workspace/coder/crearte-monorepo/crearte-server:/work -v crearte_gomod:/go/pkg/mod -v crearte_gocache:/gocache -e GOCACHE=/gocache -e GOPROXY=https://goproxy.cn,direct -e GOSUMDB=sum.golang.google.cn -w /work/src golang:1.24-alpine sh -c ""`(首次冷 1–3 分钟,background+poll)。 +- 集成层:`docker compose --profile debug up -d db-test`(宿主 5434,fresh volume),`TEST_DATABASE_URL=postgres://crearte:***@localhost:5434/crearte?sslmode=disable go test -p 1 ./...`;守卫语义=不许出现 "TEST_DATABASE_URL not set" skip。跑完 `down` 清场(controller 负责最终清场;子代理用独占端口自查后自清)。 +- 审计中间件行为红线:actor 仅在 RequireAdmin 放行后记录(401/403 未遂不记);写失败 log-only 绝不影响响应;status 含 4xx/5xx(失败的 admin 尝试同样入史)。 +- 分页钳制逐字照抄 `ListQueue` 语义(limit≤0 或 >200→50;offset<0→0)。 +- 前端:admin 面两 GET 不缓存(无 ETag 依赖);错误经 `toContentMessage`;降级确认弹层文案必须含「对方登录态立即失效」与「最后一个 admin 会被拒绝」两点。 +- CHANGELOG:server 追加 `## [0.12.0] - 2026-09-30`(顶版 0.11.1)、crearte 追加 `## [0.18.0] - 2026-09-30`(顶版 0.17.0);英文行+中文行连续。 +- 完成定义:Task 3/4 各自测试绿 + 控制者验收(spec §7 四条)+ Task 5 合主。 + +--- + +### Task 1: server — 仓储与服务层(迁移 0010 + audit/user repo + memory parity + 护栏) + +**Files:** +- Create: `internal/repository/migrations/0010_audit_log.sql`(spec §3.1 原样) +- Create: `internal/repository/audit.go` + `audit_test.go`(单测走 sqlmock? 否——本仓无 sqlmock 先例;纯逻辑测 scan/组装即可,真库覆盖交给集成层) +- Modify: `internal/repository/user.go`(+ `List(ctx,limit,offset,q) ([]model.User,int,error)`、`CountAdmins`) +- Modify: `internal/repository/memory.go`(同名 parity:List 过滤/排序/total、CountAdmins、audit Insert/List) +- Modify: store 聚合处(`UserStore` 接口或等价,看现码)接线新方法 +- Modify: `internal/service/auth.go`(`ErrLastAdmin`;`SetUserRole` 加护栏;新 `SetUserRoleByID`) +- Create: `internal/service/audit.go`(`RecordAdminAction` / `ListAudit`)+ 表驱动单测 +- Modify: `internal/service/auth_test.go`(护栏矩阵) +- Modify: `docs/CHANGELOG.md`(0.12.0 条目,Added+护栏) + +**Interfaces(Produces):** +- `repository.AuditRepository`、`PostgresUserRepository.List/CountAdmins` +- `service.SetUserRole(ctx, store, email, role)`(签名不变+新错误)、`service.SetUserRoleByID(ctx, store, id, role)` +- `service.ListAudit(ctx, store, limit, offset, routeFilter)` +- `model.AuditEntry{ID, ActorID, ActorEmail, Method, Route, Path string; Status int; CreatedAt time.Time}` + +**Steps:** +- [ ] 读 spec §3.1–§3.3 与现有 `internal/repository/user.go`、`memory.go`、`service/auth.go` 全文 +- [ ] 迁移 0010;repo 三新方法;memory parity +- [ ] service 护栏:唯一 admin 降→`ErrLastAdmin`(判序:role 合法→找用户→当前 role=admin 且目标 user 且 CountAdmins==1→拒);`SetUserRoleByID` 复用同判 +- [ ] `audit.go`:Record(吞错 log)/ List(钳制+倒序+route 过滤+total) +- [ ] 单测:护栏矩阵(唯一 admin 降拒 / 双 admin 降一 OK / 升 OK / 非 admin 目标降 OK no-op 合法)、ListAudit 钳制、SetUserRoleByID 404 +- [ ] 容器四连 RC=0:`gofmt -l`、`go vet ./...`、`go build ./...`、`go test ./...`(无 TEST_DATABASE_URL,集成自动 skip 属预期) +- [ ] commit 到 `feat/admin-console`(本地,不 push) + +### Task 2: server — API 层(admin 组收敛 + AuditMiddleware + 三端点) + +**Depends:** Task 1。 + +**Files:** +- Modify: `internal/api/router.go`(admin 路由全部收敛进组:`adminGroup := engine.Group("", RequireAdmin(deps.AuthService), AuditMiddleware(deps.AuditStore))`,组内逐条 `adminGroup.GET/POST/PUT/PATCH(RouteAdmin*, handler)`;URL 常量与 `FullPath()` 输出不得变化) +- Create: `internal/api/audit.go`(AuditMiddleware + 三 handler:ListUsers/SetRole/ListAudit,或按现码 admin handler 布放) +- Modify: `internal/api/admin.go` / deps 结构(AuditStore 注入) +- Modify: `cmd/serve.go`(store 注入链) +- Modify: `internal/api/integration_test.go` 或新 `admin_users_test.go`(真库腿) +- Modify: `docs/CHANGELOG.md`(追加端点条目) + +**Endpoints(spec §3.4):** +- `GET /api/admin/users?limit&offset&q` → `{users:[{id,email,username,display_name,role,created_at}], total}` +- `PATCH /api/admin/users/:id/role` `{role}` → 200 用户体;400 非法 role;404 无此人;409 `last_admin` +- `GET /api/admin/audit?limit&offset&route` → `{entries:[...], total}` 倒序 + +**Steps:** +- [ ] 组收敛改造,跑既有 admin 测试证明 401/403 行为零变化 +- [ ] AuditMiddleware:`c.Next()` 后取 `handler.SetUser` 存的 user + `c.FullPath()` + `c.Writer.Status()`;user 非 admin 则跳过(理论不可达,防御) +- [ ] 三端点 + 错误映射(复用 WriteError code 体系,新增 `last_admin`) +- [ ] 集成测试(TEST_DATABASE_URL 空库):列表 q/分页/total;PATCH 升角色后**旧 token 打 /api/me 必 401**;409;一次 approve 后 audit 首行=该动作;404 的 revoke 也入史(status=404);401/403 未遂不入史 +- [ ] 容器四连 + 集成 `-p 1` RC=0;commit + +### Task 3: 前端 — apiRepo + 两 view + 路由入口 + vitest + +**Files(`crearte/src`):** +- Modify: `app/data/apiRepo.ts` + `app/data/types.ts`(listAdminUsers / setUserRole / listAudit;`AdminUser`、`AuditEntry` 类型) +- Create: `app/views/AdminUsersView.vue`、`app/views/AdminAuditView.vue` +- Create: 对应 `*.test.ts` +- Modify: router 注册(找现 AdminView 注册处仿写,admin 守卫一致);`app/views/AdminView.vue` 顶部加两入口链接 +- Modify: `docs/CHANGELOG.md`(0.18.0) + +**Steps:** +- [ ] 读 AdminView.vue / AdminSubmissionView.vue / apiRepo.ts 现有模式(StatePanel、分页、toContentMessage) +- [ ] apiRepo 三方法 + 类型 + 单测(请求形状、错误映射、不缓存) +- [ ] AdminUsersView:表列 用户名/邮箱/显示名/角色/注册时间;搜索防抖;分页 50;升降按钮 + 降级确认弹层(双要点文案)+ 409 原文回显 +- [ ] AdminAuditView:倒序流水(时间本地化/操作者/动作标签映射+未知回退原文/对象 path 提取/状态码着色)+ route 过滤下拉 +- [ ] vitest + typecheck RC=0;主套件 e2e 不受影响自查(夹具模式不依赖新后端端点的 spec 别动) +- [ ] commit 到 `feat/admin-console`(本地,不 push) + +### Task 4: 前端 — e2e 扩流(admin-flow.spec) + +**Depends:** Task 2 端点形状冻结 + Task 3。**写测试所需夹具**:e2e 走 `e2e-stack` 真栈则直接打真端点;若 admin-flow 是夹具模式则补夹具路由。**先读 spec/e2e/admin-flow.spec.ts 现状再定**,与既有用例同构。 + +- [ ] 流①:admin 进 /admin/users → 搜索用户 → 升 admin → 列表即时更新 → 降回 → 确认弹层出现 +- [ ] 流②:执行一个管理动作 → /admin/audit 首行即该动作(时间/对象/结果码断言) +- [ ] e2e 主套件 + 若走真栈则 CREARTE_STACK_REQUIRED=1 下 full-loop 同步绿;commit + +### Task 5: 控制者 — 本机验收六腿 + 合主 + +- [ ] 双路审查子代理(server / fe)对照 spec 逐条 PASS/FAIL +- [ ] 验收=spec §7 四条(含手工冒烟 CLI/API 护栏一致性、废 token 端到端) +- [ ] 合主:两仓 `--no-ff` 合 `feat/admin-console` → push → 删分支;wrapper:ROADMAP P7 状态、CHANGELOG 0.2.6、文档索引登记 spec+plan → push +- [ ] 台账 `.superpowers/sdd/progress.md` + +--- + +**风险注记:** ① 组收敛若与 gin 版本路由注册交互有意外(如 FullPath 变组前缀),以「测试零改动通过」为准绳修正注册写法,不许改常量值。② `List` 返回 `[]model.User` 含 `password_hash`?看 `userColumns` 现码——若模型带 hash,序列化前必须裁(新 `AdminUserView` 投影结构体或 JSON tag 控制),集成测试断言响应体无 hash 字段。 diff --git a/docs/specs/2026-09-30-admin-console-design.md b/docs/specs/2026-09-30-admin-console-design.md new file mode 100644 index 0000000..92eac82 --- /dev/null +++ b/docs/specs/2026-09-30-admin-console-design.md @@ -0,0 +1,153 @@ +# P7 管理后台增强 — 设计规格 + +- 日期:2026-09-30 +- 涉及仓库:`crearte-server`(0.11.1 → 0.12.0)、`crearte`(0.17.0 → 0.18.0)。`crearte-deploy` 零改动。 +- 状态:设计已获维护者批准(2026-09-30 18:40 会话,四项决策 + 设计定稿拍板,均选推荐项)。 +- 前置:P0/P1/P2 完成(当前 master 为 P2 完成态;P3 备份+可观测已按维护者要求回退,与本设计无关)。 + +## 1. 目标与范围 + +ROADMAP P7 定义为「用户列表 / 角色管理 UI(替代 CLI `user set-role`)+ 审计日志」。 + +**做:** +1. 审计日志:管理员动作的统一记录(新表 + 中间件 + 查询端点 + 前端流水页)。 +2. 用户管理:管理员列表查询端点 + 角色升降端点(复用既有 `SetUserRole` 语义)+ 前端用户管理页。 +3. 最后管理员护栏:禁止把系统降级为无 admin(API 与 CLI 两条路径同受约束)。 + +**不做(明确排除):** +- 普通用户写操作(投稿创建/编辑)不进审计——`work_versions`/`submissions` 本身已是历史。 +- 登录事件不进审计(决策 D-A:只记管理员动作)。 +- 用户详情抽屉(作品列表/收藏展开)留给 P8 候选。 +- 审计保留策略/自动修剪:表自然增长,量级(管理员手速)短期无虞;出现压力另立小项。 +- 审计内容不做前后值 diff(决策 D-B:中间件统一记录,路由模板 + 路径 + 状态码即事实源)。 + +## 2. 已批准的决策 + +| # | 决策 | 内容 | +|---|------|------| +| D-A | 审计范围 | **只记管理员动作**。approve/reject、unpublish/republish、features、revoke、角色变更。 | +| D-B | 审计机制 | **中间件统一记录 + admin 路由收敛为 gin 组**(维护者定稿:组上声明一次中间件,不逐条手挂)。零侵入七个现有 handler;不存请求体。 | +| D-C | 用户列表深度 | **身份字段 + 角色操作**:用户名/邮箱/显示名/角色/注册时间 + 升降角色 + 搜索 + 分页。无聚合计数列。 | +| D-D | 最后管理员护栏 | **禁止降级最后一个 admin**:service 层 `CountAdmins` 检查,唯一 admin 降角色 → 409;CLI 同样生效。 | + +被否掉的备选(留档):服务层逐动作手写语义事件(要改七个 handler + 新增管道,回归面大);PATCH 端点按 email 定位(URL 携带 PII,选 id);不限制作死自杀式操作。 + +## 3. 后端设计(crearte-server) + +### 3.1 迁移 `0010_audit_log.sql` + +```sql +CREATE TABLE audit_log ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + actor_id uuid NOT NULL, -- 不加 FK:用户注销后审计仍可读 + actor_email text NOT NULL, -- 冗余快照,同上理由 + method text NOT NULL, -- GET/POST/PUT/DELETE + route text NOT NULL, -- gin FullPath() 模板,如 /api/admin/submissions/:id/approve + path text NOT NULL, -- 具体请求路径,含对象 id + status integer NOT NULL, -- 响应 HTTP 码(含 4xx/5xx:失败的管理尝试同样入史) + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX audit_log_created_at_idx ON audit_log (created_at DESC); +``` + +语义要点: +- **动作名 = method + route 模板**,不另造 action 字段;对象从 `path` 读。前端展示层负责把 route 模板映射为中文标签。 +- `actor` 只在通过 `RequireAdmin`(即确认是 admin)后才有值;401/403 的未遂访问**不记**——中间件在 `c.Next()` 之后检查 context 里的 user 是否存在且 role=admin,否则跳过。审计回答的是「管理员干了什么」,不是「谁试图闯进来」(后者属 D-A 被排除的登录事件范畴)。 +- 迁移编号 0010(现最大 0009)。空库 CI 集成腿每 PR 全量迁移,无需回填。 + +### 3.2 仓储层 + +`internal/repository/audit.go`:`AuditRepository`(照抄 user repository 的 DBTX/store 分层模式) +- `Insert(ctx, entry model.AuditEntry) error` +- `List(ctx, limit, offset int, routeFilter string) ([]model.AuditEntry, int, error)`(时间倒序,返回 total;`routeFilter` 为空则全量) + +`internal/repository/user.go` 增补: +- `List(ctx, limit, offset int, query string) ([]model.User, int, error)` — `query` 对 email/username 做 ILIKE `%q%`;排序 `created_at DESC`;返回 total。 +- `CountAdmins(ctx) (int, error)` +- `GetByID` 已存在,角色端点用它定位。 + +`PostgresStore`(聚合接口)同步接线两个新方法组;memory store(集成测试用)同样补齐,语义一致。 + +### 3.3 服务层 + +`internal/service/audit.go`(或并入 content.go 同级新文件,实现者定,倾向新文件): +- `RecordAdminAction(ctx, store, entry)` — 尽力而为:写失败 `log.Printf("audit: ...")` 吞掉,绝不影响业务响应。 +- `ListAudit(ctx, store, limit, offset, route)` — 分页钳制**逐字照抄 `ListQueue` 语义**(`limit<=0||limit>200 → 50`,`offset<0 → 0`)。 + +`internal/service/auth.go` 的 `SetUserRole` 改造(D-D 落点): +- 新错误 `ErrLastAdmin`。 +- 流程:校验 role 合法(既有)→ 按 email 找用户 → **若目标 role=user 且该用户当前 role=admin 且 `CountAdmins()==1` → `ErrLastAdmin`** → 更新角色 + `BumpTokenVersion`(既有语义保持:角色变更必废旧 token)。 +- CLI `user set-role` 与 API 走同一函数,护栏自动双覆盖。 + +新 `SetUserRoleByID(ctx, store, id, role)`:按 id 版本(API 用 `:id`),同样护栏与废 token 语义;找不到 → `ErrUserNotFound`。 + +### 3.4 API 层 + +路由常量(`internal/api/router.go` 现有 `RouteAdmin*` 旁): +- `GET /api/admin/users` → `RouteAdminUsers` +- `PATCH /api/admin/users/:id/role` → `RouteAdminUserRole` +- `GET /api/admin/audit` → `RouteAdminAudit` + +**审计中间件挂载方式**(维护者定稿 2026-09-30 18:59:admin 路由改用 gin 分组,不再逐条挂 `RequireAdmin`): +- 新 `AuditMiddleware(store)` gin 中间件;router.go 将七条既有 admin 路由 + 三条新路由**收敛为一个组**:`adminGroup := engine.Group("", RequireAdmin(deps.AuthService), AuditMiddleware(deps.AuditStore))`,组内逐条注册时继续引用既有 `RouteAdmin*` 全路径常量(`adminGroup.GET(RouteAdminSubmissions, deps.Admin.ListSubmissions)` …)。全路径常量不变 → 对外 URL、`FullPath()`、测试引用零变化;改的只是注册行前缀由 `engine.` 变 `adminGroup.`,中间件声明一次。注:若用 `Group("/api/admin")` + 相对子路径重写常量,会改动 `RouteAdmin*` 定义与既有测试引用,面更大;选空前缀组形态(定稿)。 +- 中间件仅当请求进入 admin 面时执行,`c.Next()` 后读 user + `c.FullPath()` + `c.Writer.Status()` 组装 entry。 + +端点行为: +- `GET /api/admin/users?limit=&offset=&q=` → 200 `{ users: [ {id, email, username, display_name, role, created_at} ], total }`。email 完整暴露给 admin——角色操作按 email 的 CLI 语义需要它,且 admin 本就是全权角色(决策:不脱敏)。 +- `PATCH /api/admin/users/:id/role` body `{"role":"user"|"admin"}` → 200 返回更新后用户(含 `token_version`);非法 role → 400 `validation`;id 不存在 → 404;**唯一 admin 降级 → 409 `last_admin`**(message: "cannot demote the last admin")。 +- `GET /api/admin/audit?limit=&offset=&route=` → 200 `{ entries: [...], total }`,倒序。 +- 三条新路由同样被审计——包括「改角色」本身(管理员最重要的动作必须可查)与对 audit 的读(记录 GET 略噪,但保持一致性优先;`route` 过滤参数正好用于自查时排除)。 + +### 3.5 错误与日志 + +- 复用现有 `handler.WriteError` 的 code 体系(`forbidden`/`internal`/`validation`/新增 `last_admin`)。 +- 审计写失败只 log,不 5xx。 + +## 4. 前端设计(crearte) + +### 4.1 apiRepo 新方法(`app/data/apiRepo.ts` + 类型) + +- `listAdminUsers(params)` → `AdminUserPage` +- `setUserRole(id, role)` → `AdminUser` +- `listAudit(params)` → `AuditPage` + +沿用现有 `request()` 封装、ETag 策略(管理面**不缓存**:两 GET 均 `cache: 'no-store'` 语义,数据随任意管理员动作即时变)、错误映射进 `toContentMessage`。 + +### 4.2 `/admin/users` — AdminUsersView.vue + +- 复用 AdminView 的 StatePanel / 分页 / 错误直出模式。 +- 表格列:用户名、邮箱、显示名、角色、注册时间;行内「升为 admin / 降为 user」按钮。 +- **降级确认弹层**:文案必须写明两件事——「对方所有登录态立即失效」+「若其为最后一个管理员将被拒绝(409)」,并给出后端 409 的原文回显。 +- 搜索框(防抖,按 email/username)+ limit/offset 分页(默认 50,与后端钳制一致)。 + +### 4.3 `/admin/audit` — AdminAuditView.vue + +- 倒序流水表:时间(本地化)、操作者邮箱、动作(route 模板→中文标签映射,映射外模板回退显示原文)、对象(从 path 提取 :id/:user/:slug 段)、结果(状态码着色:2xx 绿、4xx 黄)。 +- `route` 过滤下拉(选项 = 已知动作标签集)。 + +### 4.4 入口与路由 + +- router 增两条 `/admin/users`、`/admin/audit`,沿用现有 admin 路由守卫(非 admin 跳登录/403 行为与 AdminView 完全一致)。 +- AdminView 顶部现有「审核」链接旁加两个入口:用户管理、操作日志。 + +## 5. 测试策略 + +- **server 单测**:`SetUserRole`/`SetUserRoleByID` 护栏矩阵(唯一 admin 降→ErrLastAdmin;两个 admin 降一个→OK;升→OK)、`ErrInvalidRole` 保持、audit entry 组装纯函数。 +- **server 集成层**(TEST_DATABASE_URL,空库全迁移):用户列表 q/分页/total;PATCH 升角色成功且**旧 token 立即失效断言**(改前签发的 token 调 `/api/me` → 401);409 唯一 admin;CLI 与 API 行为一致;跑一次 approve 后 `GET /api/admin/audit` 出现对应行(含 status=200),失败动作(404 的 revoke)也入史。 +- **前端 vitest**:apiRepo 三方法(请求形状/错误映射)、两新 view 的渲染与角色操作确认流(mock repo)。 +- **e2e**(主套件,夹具模式):`admin-flow.spec.ts` 扩两条:① admin 进用户页升降角色 → 列表即时更新;② 执行一个管理动作后审计页首行即该动作。真栈 full-loop 不动(P2 既有腿保持绿即回归通过)。 +- **容器四连**:gofmt/vet/build/test RC=0。 + +## 6. 版本与账目 + +- server:CHANGELOG 新 `## [0.12.0]`(现顶 0.11.1)。 +- crearte:CHANGELOG 新 `## [0.18.0]`(现顶 0.17.0)。 +- deploy:无。 +- wrapper:ROADMAP P7 状态行 + CHANGELOG 0.2.6 记账 + 文档索引登记本 spec 与实现计划。 + +## 7. 验收标准(本机) + +1. 容器四连 + 集成层(`-p 1` 官方跑法,空库)全绿,0 skip。 +2. 前端 vitest + typecheck + 主套件 e2e 绿;真栈 full-loop 不回归。 +3. 手工冒烟(compose debug/dev 栈或 e2e-stack 编排):CLI 建 admin → 登录 → 用户列表 → 升第二个用户为 admin → 降级第一个成功 → 试图降级最后一个 → 409 → 审计页/端点能看到全部动作含 409 那次失败尝试。 +4. 被降级用户旧 token 调 `/api/me` → 401(废 token 端到端验证)。