Skip to content

ReadVocab 产品待办清单(全生态上架/运营级)

创建日期:2026-04-13 · 2026-07-19 从 admin/docs/todo-product.md 上移到根 docs(它覆盖 RB+RVH+admin+landing 全生态——签名/商店/许可/Sentry/CI,是产品级内容,非 admin 私有) 状态标记:⬜ 待办 | 🔄 进行中 | ✅ 完成

backlog.md 的分工:本文件 = 上架/发行/运营基建(跨全部子项目);backlog.md = 功能/缺陷/体验迭代。两者互补,不重复。


Phase A — 上架准备(发布前必须)

A1. 隐私政策 + 服务条款(roadmap 1-6,as-built privacy-terms-plan.md

  • ✅ 编写隐私政策页面(全产品共用;白话诚实版 + 商店字段,点名 Supabase/Sentry/Azure/LLM,中英双语,2026-07-28)
  • ✅ 编写服务条款(同上,内测 as-is 版)
  • ✅ 部署到 Landing Page + 域名绑定 + 线上复验(2026-07-28):域名 lampio.app 已注册并绑定 Vercel(NEXT_PUBLIC_SITE_URL 已设);curl 实测 /zh|en/privacy /terms 渲染正常、无前缀 /privacy→307→/zh、OG/metadataBase 基址均为 https://lampio.app(预览图不裂)。非阻塞小注:未显式输出 <link rel="canonical">(SEO 可选增强,未配 alternates.canonical
  • ⬜ 在 RVH iOS/Android 配置中填入隐私政策 URL(RVH 新会话)——canonical = https://lampio.app/privacy(proxy 按语言重定向;线上已可达,✅ 可填)

A2. ReadBrowser 桌面端签名与分发(roadmap 1-1 / 1-2,2026-08-01 了结)

  • ✅ 申请 Apple Developer 账号——Developer ID Application: xiangliang li (SW87SQBZ66)
  • ✅ 配置 macOS 代码签名(Developer ID Application 证书)——CI 走 APPLE_CERTIFICATE/_PASSWORD/KEYCHAIN_PASSWORD 导入 + identity 派生;hardened runtime flags=0x10000 + entitlements 嵌 device.audio-input
  • ✅ 配置 macOS 公证(notarization)——APPLE_ID/APPLE_PASSWORD/APPLE_TEAM_ID;dev.10 CI 一次过(status Accepted + stapled)。真机验证:干净机首开是「downloaded from Internet → Open」良性框( Gatekeeper 拦截),landing 的「未签名/xattr 绕行」文案已撤。遗留观察:release.sh verify-notarized 曾验错对象(对 DMG 容器而非内部 .app),已修
  • ✅ 配置 Tauri Updater 自动更新(roadmap 1-2,2026-07-26,as-built updater-version-unify-plan.md)——minisign 签名(独立于 Apple 公证)+ 人工闸门 promote-updater + 前端 TitleBar 药丸/Settings 小节。GitHub Secrets TAURI_SIGNING_PRIVATE_KEY+_PASSWORD 已配(2026-07-26,gh secret list 可见);端到端更新已由 dev.8/9→dev.10 真机验证
  • ✅ 统一版本号(package.json / Cargo.toml / tauri.conf.json)——真相源 = tauri.conf 带 -dev.Nscripts/set-version.mjs + CI check:version-sync
  • 🔄 确定分发渠道——内测阶段已定:landing 直下载公开仓 GitHub Releases(ttfishnet/lampio-releasesdownload.tsx 钉 TAG)+ updater endpoint 固定 updater-latest release 的 latest.json。未决:Homebrew Cask / Mac App Store(正式版前再评估)。Windows 仍未签名(SmartScreen 首拦,landing 保留该文案)
  • ✅ Supabase anon key/url 改构建期注入(roadmap 1-5,2026-07-23)——防项目身份跟随 fork 源码树流出;build.rs 从 .env.local/CI env 注入,缺配置 fail-fast。✅ CI Secrets 已配RB_SUPABASE_URL + RB_SUPABASE_ANON_KEY,2026-07-23 设置,gh secret list 可见;release.yml build 步骤已接线 env);实际签名构建生效验证挂下次 /release。旧 key 作废需轮换(跨端,见 backlog)

A3. RVH 移动端上架准备

  • ⬜ 统一 iOS / Android Bundle ID(当前不一致)
    • iOS: com.nikos.englishLearningApp
    • Android: com.nikos.reading_vocab_helper
  • ⬜ 创建 Android Release Keystore(当前使用 debug key)
  • ⬜ 配置 Android release signing(build.gradle)
  • ⬜ 准备 App Store 元数据(截图、描述、分类、关键词)
  • ⬜ 准备 Play Store 元数据
  • ⬜ iOS 配置 Distribution Provisioning Profile

A4. 许可协议

  • ⬜ 确定许可类型(MIT / proprietary / 其他)
  • ⬜ ReadBrowser 添加 LICENSE 文件
  • ⬜ RVH 添加 LICENSE 文件
  • ⬜ ReadVocab Admin 添加 LICENSE 文件
  • ⬜ ReadBrowser Cargo.toml 补充 license / authors / description

A5. Landing Page 完善

  • ✅ 添加下载链接指向分发渠道(landing/components/landing/download.tsx:Win x64 setup.exe / mac aarch64+x64 dmg 三平台直链,指向公开仓 Releases;每次发版只改 TAG 一行,当前 v0.1.0-dev.10
  • ⬜ 添加 App Store / Play Store 徽章(等 A3 RVH 上架)
  • ✅ 注册并绑定正式域名 lampio.app(替代 vercel.app 默认域,2026-07-28;Vercel 已绑定 + NEXT_PUBLIC_SITE_URL 已设 + 线上复验通过)

Phase B — 运营基础(发布后 1 周内)

B1. 错误监控

  • ✅ ReadBrowser 接入 Sentry(Tauri + React)(roadmap 1-3,2026-07-26,as-built sentry-integration-plan.md)——两层各一 DSN、构建期注入复用 anon-key 范式(空 DSN=自动禁用,不 fail-fast);send_default_pii=false+before_send 脱敏;release.yml 已接 RB_SENTRY_DSN/VITE_SENTRY_DSN env。ingest 实测双 project HTTP 200;应用内 SDK 冒烟实机双端均通过(Rust panic hook 收到 event id ×2;前端 captureException+flush=true + WKWebView fetch 探针 200);GitHub Secrets 已配RB_SENTRY_DSN+VITE_SENTRY_DSN,生效验证挂 /release)。注:代理下 ingest 往返 5–20s(异步不阻塞)
  • ⬜ RVH 重新启用 sentry_flutter(解决 Kotlin 版本冲突)
  • ✅ Admin 接入 Sentry(@sentry/nextjs 10.70,2026-08-14,as-built admin-hardening-plan.md §B1)—— client/server/edge 三处 init + onRequestError(RSC 错误只能靠它)+ global-error.tsx; 空 DSN=自动禁用不 fail-fast(同 RB 取舍)。比 RB 多一层值级兜底:RB 那套是键名黑名单, 挡不住密钥被拼进异常 message / URL —— 而这正是 admin 最可能的泄漏形态(createClient 报错、 fetch 面包屑、堆栈闭包)。故 redactSecrets 对所有字符串做 JWT / sb_secret_* 模式替换; URL 只留 origin+pathname(Supabase magic-link 把 access_token 放在 hash 里)。 回归守门 tests/unit/sentry-scrub.spec.ts 18 条 —— 脱敏层错了不报错、只静默泄漏,必须有断言兜着。 剩余人工动作:Sentry 后台建 project → 把 DSN 配到 Vercel NEXT_PUBLIC_SENTRY_DSN(在此之前 SDK 全程 no-op)

B2. SEO 与安全

  • ✅ Landing Page 添加 robots.txt(landing/app/robots.ts,force-static,全站可抓 + 指向 sitemap,2026-08-13)
  • ✅ Landing Page 添加 sitemap.xml(landing/app/sitemap.ts,2026-08-13)——双语站的关键不是有 sitemap 而是别只列一半localizedPaths × locales = 6 条(/zh /en × 首页/privacy/terms),每条自带全组 hreflang + x-default (指向不带前缀的 URL,由 proxy.ts 按 Accept-Language 跳转 = Google 说的「自动跳转通用页」,重定向 URL 本身不进条目)。 同时补齐 head 里的 canonical + hreflang只放页面级——放 layout 会被 privacy/terms 继承成全指首页的错 canonical)。 hreflang 码与 <html lang> 共用 dictionaries.ts::bcp47;lastmod 首页取 site.ts::CONTENT_LAST_MODIFIED、法务页取 legal.ts::LAST_UPDATED(不用构建时间,否则每次部署都变 → 爬虫无视)
  • ✅ Admin 添加安全头(2026-08-14,as-built admin-hardening-plan.md §B2)—— 单一定义处 admin/lib/security-headers.ts,三个消费者共用(next.config / proxy / E2E), 避免「配置改了但测试还在断言旧值」。下发拆两处:静态头走 next.config.tsheaders() (HSTS / X-Frame-Options / nosniff / Referrer-Policy / Permissions-Policy / COOP / CORP), CSP 走 proxy.ts —— 它带 per-request nonce,静态 headers() 装不下。 🔒 app/layout.tsxforce-dynamic 是 nonce-CSP 的硬前提不是性能取舍:静态预渲染发生在 build 期拿不到 nonce,被静态化的路由其内联 bootstrap 脚本(self.__next_f.push)会被 CSP 拦 → HTTP 200、HTML 完整、但 hydrate 不了(白屏/点什么都没反应),header 断言完全抓不到这类故障。 刻意不用 'strict-dynamic'(它让浏览器忽略 'self',任一没打 nonce 的 <script src> 即整页白屏); dev 不发 nonce(与 HMR 需要的 unsafe-inline 互斥)→ 故 E2E 对 next build && next start 跑而非 dev server。 HSTS 不带 preload(当前在 *.vercel.app,非自有 apex 域)

B3. ReadBrowser 版本管理

  • ✅ 统一 package.json / Cargo.toml / tauri.conf.json 版本号(roadmap 1-2,2026-07-26)——scripts/set-version.mjs(真相源 tauri.conf)+ CI check:version-sync
  • ✅ 建立版本发布流程(tag → build → upload → updater endpoint)——/release skill 编排(bump → tag-push → build → publish → promote-updater);updater endpoint = 公开仓固定 updater-latest release 的 latest.json

Phase C — 工程质量(持续完善)

C1. 自动化测试

真相源 = roadmap §五「测试自动化金字塔」(product-iteration-roadmap-2026h2.md 5-1~5-6),本节只记结论。 投入按本仓真实风险分布排 ROI:只护 sync 数据丢失类 + 三端行为漂移类;UI 全自动 E2E 明确降级(tauri-driver 不支持 macOS)。

  • ✅ ReadBrowser:添加 Rust 后端单元测试(roadmap 5-1,2026-07-22)——SM-2 / sync merge 谓词 / reconcile_cloze_pool / 迁移链 / lemmatizer propose_stemcargo test --lib 62 passed
  • 🔄 ReadBrowser:前端测试(roadmap 5-3,2026-07-22)——vitest 4.1.10 + happy-dom,52 测全绿(url/fontScale/cloze/sm2 纯函数 + commands.ts mock 边界演示 rssHelpers)。未做:React 面板组件测试(需 @testing-library/react,属增量覆盖非新范式)
  • ✅ 三端 SM-2 黄金向量(roadmap 5-2,2026-07-22)——docs/cross-end/sm2-golden-vectors.json 单份 fixture,Rust(3) + TS(22) + RVH Dart(21) 三端消费,一致性锁从人工纪律变机器纪律
  • 🟢 半自动冒烟替代 E2E(roadmap 5-5)——docs/smoke-test-runbook.md 6 条核心链路 + 客观断言(rb-debug MCP 驱动真实 app)。:写驱动步骤(存词/评分)+ 复习入口选择器待下次实跑固化
  • ✅ Admin:Playwright E2E(2026-08-14,as-built admin-hardening-plan.md §C1)—— 39 tests 全绿,两个 project:unit(脱敏函数,不开浏览器)/ e2e(chromium)。 覆盖:安全头逐条对值 + CSP 指令 + nonce 逐请求变化 + HTML 内每个 script 都带 nonce; 真浏览器 CSP 违规监听 + 「输入框真的能打字」证明 hydration 活着(唯一能抓到「CSP 把页面搞成 静默半死」的手段);🔒 service_role 不下发浏览器;未登录访问 /admin 落到登录页。 no-secret-leak 扫的是实际下发的字节(HTML 含 RSC flight payload + 全部 JS chunk,经 HTTP 抓), 不扫 .next/ 磁盘产物 —— 因为破坏该纪律不需要谁写错 import,Server Component 把含 key 的对象 当 prop 传给 Client Component 就会被序列化进 HTML。含阳性对照断言(必须扫到 role=anon 的 JWT): 否定断言最危险的失效方式是扫描器根本没扫到东西、于是安静全绿
  • 🔄 RVH:维护现有 62 个测试用例,补充覆盖率——已 +21(SM-2 黄金向量,见上);覆盖率补充未系统做

C2. CI/CD 流水线

  • ✅ ReadBrowser:GitHub Actions(roadmap 5-4,2026-07-22)——三 workflow 分工:ci.yml(ubuntu×1:i18n 双守门 + schema 冻结守门 + version-sync + vitest + build)/ ci-rust.yml(windows×2,paths: src-tauri/**cargo test --lib + clippy informational)/ release.yml(仅 v* tag 打包发布)。未纳入门pnpm lint(全仓 507 errors,多在 supabase/functions/ Deno 代码,存量待清)
  • ✅ Admin:GitHub Actions 质量门(2026-08-14,as-built admin-hardening-plan.md §C2)—— ⚠️ 先澄清一个既有误解:Vercel 自动部署早就有了vercel project inspect 实查:project lampio-admin 已接本仓 Git、Root Directory=admin、deployment 带 git-main alias,push 即部署; admin/.claude/skills/deploy 里那句 vercel deploy --prod 是合仓前写的,已非唯一路径)。 真实缺口是部署前没有任何质量门。故新增 .github/workflows/ci-admin.ymlpaths: admin/**, 独立文件——on.paths 是 workflow 级不是 job 级,加进 ci.yml 会把桌面端的门一起过滤掉, 同 ci-rust.yml 先例):typecheck → lint → Playwright(含浏览器缓存)→ 传报告。 CI 全套 dummy 凭据、不注入任何真 key(本套 E2E 不需要真连 Supabase)→ CI 日志/产物/fork PR 都接触不到生产凭据;dummy anon key 刻意做成合法 JWT 且 role=anon,否则阳性对照静默失效。 已用该 env 本地实测跑通。 ⚠️ 这是事后告警不是事前闸门——Vercel 收到 push 就部署、不等 Actions。要真卡住需走 PR 流程 (分支 → PR 触发 CI → 绿了再合 main,即根 CLAUDE.md §8 短命分支触发条件 ③)
  • ⬜ RVH:补充 CD 流程(自动构建发布到 TestFlight / Play Console)

C3. 产品分析

  • 选择分析工具(PostHog / Plausible / Mixpanel)已被 roadmap 4-1 取代,不再评估三方工具: 选定自建 Supabase 事件表usage_events + 本地缓冲 usage_events_local,migration v28)。 理由:中国大陆可达性已被现网同步链路验证 · 不新增第三方数据处理方(隐私政策少一个点名对象)· 复用 anon key 构建期注入 + observability 既有范式。as-built telemetry-opt-in-plan.md

    ⚠️ 这条曾以「待选型」形态挂了很久,2026-08-12 对账时发现它其实早被 4-1 决掉了 —— 留着 ⬜ 会让人反复重新评估(roadmap §六「明确不做」存在的正是为了防这个)。

  • ✅ 客户端接入使用分析 → 同上,已落地且默认关(opt-in、install 级匿名、只发数值/枚举, admin observability 看板已消费)。
  • ✅ Landing Page 接入访问统计(2026-08-13)——选 Vercel Web Analytics@vercel/analyticslanding/app/[lang]/layout.tsx<Analytics />)。决策理由:脚本从本站域名下发 (/_vercel/insights/*)→ 无 cookie、无跨站标识、大陆可达性与站点同命(Cloudflare 那条 static.cloudflareinsights.com 在大陆会大面积漏采);且 Vercel 本就是 landing 托管方 = 不新增第三方数据处理方,隐私政策不必新点名一家公司。🔒 它不读任何密钥, 「landing 无 Supabase / 无 service_role」隔离未破。与客户端 opt-in 遥测(usage_events) 不同源、不合流。已 enable + 已上线实测(2026-08-14)/robots.txt /sitemap.xml 均 200 (sitemap 6 条 loc + 6 组 x-default)、线上 head 有 canonical+3 条 hreflang、beacon POST …/view → 200 ×2、Cookie/localStorage/sessionStorage 全空(政策里「不用 Cookie」经得起查)。 注:beacon 实际走每项目随机化的同源路径/<hash>/script.js + /view)而非文档写的 /_vercel/insights/* —— Vercel 用来绕广告拦截器;排查时别按文档路径去 grep 而误判成没接上。 隐私政策已同步(landing/lib/legal.ts 中英各三处 + LAST_UPDATED bump 到 2026-08-13)

C4. 数据安全(2026-08-13 落地,runbook docs/supabase-backup-restore-runbook.md

  • ✅ Supabase 数据库定期导出备份(超出免费版 7 天自动备份)——scripts/backup-supabase.sh
    • scripts/backup-supabase-storage.mjs备份对象是两份:① Postgres(10 张 user_* 同步表 + 3 张推荐表 + 治理表 + auth.userspg_dump -Fc)② Storage reading-snapshots/{user_id}/... 的文件正文——它不在 pg_dump 里,而客户端 cached_file_path 指向它,丢了 =「回原文」全空。凭据走 macOS Keychain(仓外); 产物落 ~/Backups/lampio(脚本硬拒 BACKUP_ROOT 在仓库内,0700/0600,.gitignore 再兜一道 *.dump);保留 8 份滚动,Storage 靠 size+eTag 硬链接去重使「每周全量」近似增量。 四道自检闸任一不满足即 exit 1(10 张同步表数据段必须在 TOC 里 / 每对象字节数校验 / 落盘数=清单数 / BACKUP_ROOT 越界)。定时 = 本机 launchd 每周日 03:30 (--install-launchd,失败弹通知)+ --check 巡检新鲜度。 首次实跑:852 KB / 69 秒 ✅

    未选 GitHub Actions:那要把生产连接串 + service_role key 放进 Secrets, 且每周把全部用户数据拉进托管 runner —— 对隐私敏感产品是更差的信任边界。

  • ✅ 建立数据恢复演练流程——已真做一次(2026-08-13,导入临时 Supabase project)。 三项验证全过:① 23 张表 + auth.users/identities 行数逐表一致,跨表孤儿/悬挂 FK 全为 0; ② deleted_at 墓碑原样保留、server_updated_at 未被 trigger 改写(恢复后最近 1 小时内 0 行) → 恢复不会触发客户端全量重拉;RLS 23/23 + 23 policy + 92 索引 + 20 trigger 自动重建; ③ Storage 7/7 按 {user_id}/{rel} 传回并逐字节回读一致。 演练推翻了两处原以为需要的步骤(--disable-triggers 多余、无需手工跑 sync-tables.sql), 并固化了三个坑(新 project 无直连须走 pooler session 模式 / Keychain 交互提示 128 字符 静默截断 / 粘贴带尾随空格),全部写进 runbook §5-§6。

    ⚠️ 收尾未完:临时 project 尚未删除(内含全部真实用户数据 + auth 邮箱与密码哈希), 见 runbook §6 收尾清单。 ⚠️ 恢复流程里 RTO 最长的一环不是数据库而是发版supabase_url/anon_keybuild.rs 注入的编译期常量,换 project-ref 必须重新发版(RVH 还要过商店审核), 故灾难恢复应优先「保住原 project-ref」。见 runbook §5.5。

  • 备份异地化已启用(2026-08-13)——backup-supabase.sh 第 6b 步:gpg AES-256 加密整个备份目录 → iCloud Drive LampioBackups/,保留 4 份,写完做完整解密+列归档 自检才算成功。刻意不引入新信任方——用已在用的云盘 + 本机加密,云端只见密文 (加密让 iCloud 账号不再是单点)。实测密文零可读关键词、解密还原逐字节一致。
    • 🔴 口令须另存密码管理器:另两个凭据(连接串 / service_role)丢了都能在 Dashboard 重置,唯独这条丢了是终局——异地副本永久打不开,而平台侧实测无任何自动备份。 Keychain 跟着这台机器,异地副本的意义恰恰是这台机器没了。
    • 恢复路径已补齐并实测:runbook 新增 §5b「场景 C——这台 Mac 也没了」。 此前 §4/§5 的每条命令都假设 ~/Backups/lampio/LATEST/… 在本地,而异地副本存在的 唯一场景恰恰是本地没了——路径一条都不成立。§5b 覆盖新机器依赖(gnupg 非 macOS 自带) → 解密还原到 §4/§5 期望的位置 → 验证。模拟新机器实跑通过:只用一个 .gpg 还原出 26 个 TABLE DATA 段、10 张同步表齐全、8 个 Storage 对象。
  • 数据一致性巡检scripts/db-audit.sh,2026-08-13)——只读多组检查,已挂进每周 launchd(备份成功后 && 串跑,异常弹通知)。补的是发现能力而非处置能力: database-operations.md §2b 里同步引擎/发版类事故全部是静默的,应急流程只能在 「已经知道出事了」之后起作用。覆盖:归属完整性 · 父子归属一致(跨用户泄漏红线 #5i 唯一可机器检测的特征) · 悬挂 FK · 墓碑回声环(红线 #6d)· push 停摆(PGRST204 整批失败)· Storage 指针悬空 · 备份新鲜度。 自检做过:往演练库注入三类真实异常,三条全部报红、退出码 1。
  • 运维文档 + 事故 skill(2026-08-13)——docs/database-operations.md(运维总纲 / 场景清单 / 隐私事故 / 变更流程)+ /db-incident skill(场景路由 → 止血 → 取证 → 只读诊断 → 定点修复 → 验证;诊断完成前禁止任何生产写操作)。 关键项目特异认知:10 张同步表全是软删,「误删」多半数据还在,且 🔴 只清云端 deleted_at 会被客户端本地墓碑在下一轮 sync 自动撤销pull/ 十处同款:「本地墓碑靠 push 传播,绝不被远端活行复活」)。

C5. 监控告警

  • 数据层监控 + 告警上线supabase/sql/monitoring.sql,2026-08-13)—— pg_cron 每天 04:15 跑 ops_daily_monitor(),异常聚合成一条经 pg_net 发 Sentry (复用已接入的通道,不新引入 Resend/SendGrid = 不新增会过期会失效的东西; 实测 HTTP 200)。三张 ops_* 表开 RLS 且无 policy,实测 anon 读全空。
    • 🔴 核心是心跳倒置(dead man's switch):「备份新鲜度」在本机自查是逻辑上无效的 ——电脑关机时既不备份也不检查,永远不会告警。改为「备份成功 → 上报心跳 → 服务端 每天查心跳陈旧 → 告警」,恰好在本地方案失效的场景下生效。 负向实测:心跳倒回 10 天 → 正确报「240 小时没报平安(阈值 192)」。
    • 顺带覆盖了本条原第三项:巡检含 cron.job_run_details 近 3 天失败检查 (analyze-articles / discover-sites)。
    • ⚠️ 已知边界:监控查不了自己2026-08-14 已补(见下条)。原补法设想是 Sentry cron check-in(免费额度未评估),实际用了更省的办法。
  • /admin/ops 运维面 —— 把上面这套接出界面(2026-08-14,as-built docs/database-operations.md §3.3)。上面那套的唯一出口是 Sentry 告警(push), 缺的是 pull:想知道「现在健不健康」时能看一眼。这个缺口在本项目尤其贵 —— 事故绝大多数静默,于是「没收到告警」有两种无法区分的含义:一切正常,或监控自己死了
    • 🔴 核心价值 = 补「监控查不了自己」:admin 是独立于 pg_cron 的进程,读一眼 max(ops_audit_findings.checked_at) 就知道巡检是不是几天没跑了(>36h 判停)。 这一环 SQL 侧永远做不到,也因此不必再评估 Sentry cron check-in。
    • 只读既有产物,不新增第三份判据(判据仍只有 run_db_audit()db-audit.sh 那一对)。
    • 新增 SQL 对象(monitoring.sql 第二批):ops_credentials 台账 + ops_platform_stats() RPC(cron/storage/pg_catalog 不在 PostgREST 暴露范围,须 SECURITY DEFINER 包一层, 🔒 建完必须 revoke —— 新函数默认 grant execute to public)+ ops_probe_endpoints()
    • 写入面只开三样:白名单阈值 / 限时静音 / 手动跑只读巡检。🔒 Sentry 通道三段不给界面入口 (改错它 = 静默失去全部告警,后果恰恰是收不到告警)。静音必须带时长、到点自动恢复 —— 忘记恢复的静音比没有告警更危险
    • 🔒 顺带堵住一个既有缺口:admin 鉴权是纯客户端的(session 在 localStorage), Server Component 会把数据序列化进未登录请求也能拿到的 RSC flight payload (实测 curl /admin/observability 无凭据即取到运营数据)。ops 页含凭据台账, 故改为「空壳页 + loadOpsSnapshot(token)requireAdmin 验签」,并加回归断言。 其余 dashboard 页与 3 个既有 Server Action 仍是老写法 —— 见下方新增待办 (→ 已于 2026-08-15 根治,那条待办现已 ✅;ops 的空壳页写法保留,作为不依赖 URL 匹配的兜底,但 accessToken 显式传参已收敛掉)。
  • ✅ Admin 添加健康检查端点(/api/health)(2026-08-14)—— admin + landing 各一个, 返回 ok/commit/env/startedAt;landing 版顺带自报它 download.tsx 钉的下载 TAG。 🔒 刻意不查数据库:这是无鉴权公开端点,带 DB 查询 = 任何人都能驱动我们的数据库查询, 而收益近乎为零(探测器本身就跑在那个数据库里)。 ⚠️ 落地时发现 landing 的 proxy.ts 会把 /api/health 语言重定向成 /zh/api/health → 404, 已加 /api/ 早退;症状很隐蔽(端点"存在"但探测器恒拿 404)。
  • ✅ 配置 Uptime 监控(2026-08-14)—— 没有引入 UptimeRobot / Better Uptime: 用 Supabase pg_cron + pg_net 每 30 分钟打 landing / admin 的 health,非 2xx 连续 N 次发 Sentry。 理由是它与 dead man's switch 同构(检查者必须独立于被检查者,admin 自己 fetch 自己无效), 且复用现成告警通道 = 零新账号、零新会过期的东西。 ⚠️ pg_net 异步:响应落 net._http_response,故必须两步法(本轮发请求、下轮判上轮响应)。
  • ⬜ Supabase Edge Functions 执行状态告警 → 已被上一条部分覆盖(cron 失败检查); Edge Function 自身的运行时错误仍走 edge_run_log + admin 看板,未接实时告警
  • 【2026-08-14 发现 → 2026-08-15 根治】admin 鉴权只在客户端,服务端完全没有闸门 —— 原缺口实测:① curl /admin/observability 无任何凭据即可从 RSC flight payload 取到 运营数据(AuthGuard"use client",挡界面不挡字节);② app/actions/{recommendations, recommend-config,vocabulary}.ts 的 14 个 Server Action 无服务端鉴权,任何人都能 POST 改推荐配置 / 黑名单 / 词条 CEFR。根因是 session 存 localStorage(用的是普通 createClient 而非 @supabase/ssr),服务端拿不到任何 auth 上下文。
    • 走了路线 (a) + 动作级锁(当时列的两条路里,(b)「逐页逐 action 照 ops 改」实测更贵: 6 个页面各要拆一次空壳页、14 个 action 改签名、7 个客户端组件传 token,且新页面仍要 靠人记得)。cookie session 让第二层几乎免费 —— requireAdmin() 自己读 cookie, 14 个 action 各加一行,调用点一个字没改
    • lib/supabase-client.ts@supabase/ssrcreateBrowserClient(session 落 cookie)→ proxy.ts/admin/** 做服务端门,getUser() 向 Supabase 真验签 (不是 getSession() 解本地 cookie —— 那是攻击者可随便捏造的字节),再查邮箱白名单。 拦在渲染之前:不放行就没有 Server Component 执行,也就没有数据可序列化。 env 缺失 / Supabase 不可达一律 fail closed
    • app/actions/** 每个导出动作首行 await requireAdmin()。门是按 URL 前缀拦的, 换个路由布局就可能不覆盖;Next 官方口径也是 Server Action 按公开端点对待、函数内部鉴权。
    • 顺带:白名单判定完全收回服务端 —— AuthGuard 不再接收 adminEmails prop (它现在只是「session 过期就跳走」的交互层,不再是安全边界);ops 的 requireAdmin(accessToken) 显式传参一并收敛,全仓统一无参从 cookie 读。
    • 两层各自单独验过(不是只验了合起来的效果):门 = 7 条 dashboard 路径未登录 GET/POST 全部 307 且响应里无页面内容;锁 = 临时关掉门重新构建后,未登录直打 loadOpsSnapshot / exportOpml / removeBlocklist 均 500(服务端 Error: 无权限, 客户端只见 digest,不回显原因)。回归断言见 admin/tests/e2e/auth-gate.spec.ts (含伪造 cookie 不被采信、以及扫源码断言每个 action 首行都有 requireAdmin()—— 这次缺口的成因正是「新加的记得了、存量的没人记得」,人记不住,测试可以)。
    • ⚠️ 未自动验证:成功登录的 cookie 往返(需要管理员密码,不该由 Claude 经手)。 部署前请手工确认一次:登录 → 进 /admin → 刷新页面仍在登录态 → 登出后回登录页。

C6. 平台整合裁定(2026-08-14)

写下"不做"是为了防反复评估(同 roadmap §六)。判据四条:出事时静不静默 / 原生界面是否已更好 / 需要什么凭据(admin 已持 service_role,每加一个 token 就把它升级成"持全套基础设施凭据"的单点)/ 变化频率。

平台裁定理由
Supabase✅ 深度整合数据同库、零新凭据,且几乎所有静默事故都在这一层
Sentry🟡 只做健康灯 + 链接(+ 可选只读 token 拉 24h issue 数)已有邮件与原生界面;真正值钱的是 DSN 配没配的自检(未配 = SDK 全程 no-op = 永远收不到告警,且无任何报错)
Vercel❌ 不接 API部署真相用各自 /api/health 自报 commit 覆盖,零凭据;原生 dashboard + 邮件已够
GitHub🟡 轻量只要一条:release / updater / landing 下载链三者版本一致性(公开 URL,零凭据)。promote-updater 忘跑 = 存量用户永远收不到更新,静默
Cloudflare不整合实测其角色仅 DNS 托管(NS=*.ns.cloudflare.com,A 记录指 Vercel anycast),一年动零次。真实风险是①域名到期→归凭据台账;②站点不可达→归 pg_cron 探测。接 token 的收益远低于爆炸半径