主题
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 runtimeflags=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 SecretsTAURI_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.N,scripts/set-version.mjs+ CIcheck:version-sync - 🔄 确定分发渠道——内测阶段已定:landing 直下载公开仓 GitHub Releases(
ttfishnet/lampio-releases,download.tsx钉 TAG)+ updater endpoint 固定updater-latestrelease 的 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
- iOS:
- ⬜ 创建 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_DSNenv。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/nextjs10.70,2026-08-14,as-builtadmin-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.ts18 条 —— 脱敏层错了不报错、只静默泄漏,必须有断言兜着。 剩余人工动作:Sentry 后台建 project → 把 DSN 配到 VercelNEXT_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.ts的headers()(HSTS / X-Frame-Options / nosniff / Referrer-Policy / Permissions-Policy / COOP / CORP), CSP 走proxy.ts—— 它带 per-request nonce,静态 headers() 装不下。 🔒app/layout.tsx的force-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)+ CIcheck:version-sync - ✅ 建立版本发布流程(tag → build → upload → updater endpoint)——
/releaseskill 编排(bump → tag-push → build → publish → promote-updater);updater endpoint = 公开仓固定updater-latestrelease 的 latest.json
Phase C — 工程质量(持续完善)
C1. 自动化测试
真相源 = roadmap §五「测试自动化金字塔」(
product-iteration-roadmap-2026h2.md5-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/ 迁移链 / lemmatizerpropose_stem,cargo test --lib62 passed - 🔄 ReadBrowser:前端测试(roadmap 5-3,2026-07-22)——vitest 4.1.10 + happy-dom,52 测全绿(
url/fontScale/cloze/sm2纯函数 +commands.tsmock 边界演示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.md6 条核心链路 + 客观断言(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实查:projectlampio-admin已接本仓 Git、Root Directory=admin、deployment 带git-mainalias,push 即部署;admin/.claude/skills/deploy里那句vercel deploy --prod是合仓前写的,已非唯一路径)。 真实缺口是部署前没有任何质量门。故新增.github/workflows/ci-admin.yml(paths: 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-builttelemetry-opt-in-plan.md。⚠️ 这条曾以「待选型」形态挂了很久,2026-08-12 对账时发现它其实早被 4-1 决掉了 —— 留着 ⬜ 会让人反复重新评估(roadmap §六「明确不做」存在的正是为了防这个)。
- ✅ 客户端接入使用分析 → 同上,已落地且默认关(opt-in、install 级匿名、只发数值/枚举, admin observability 看板已消费)。
- ✅ Landing Page 接入访问统计(2026-08-13)——选 Vercel Web Analytics(
@vercel/analytics,landing/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、beaconPOST …/view → 200×2、Cookie/localStorage/sessionStorage 全空(政策里「不用 Cookie」经得起查)。 注:beacon 实际走每项目随机化的同源路径(/<hash>/script.js+/view)而非文档写的/_vercel/insights/*—— Vercel 用来绕广告拦截器;排查时别按文档路径去 grep 而误判成没接上。 隐私政策已同步(landing/lib/legal.ts中英各三处 +LAST_UPDATEDbump 到 2026-08-13)
C4. 数据安全(2026-08-13 落地,runbook docs/supabase-backup-restore-runbook.md)
- ✅ Supabase 数据库定期导出备份(超出免费版 7 天自动备份)——
scripts/backup-supabase.shscripts/backup-supabase-storage.mjs。备份对象是两份:① Postgres(10 张user_*同步表 + 3 张推荐表 + 治理表 +auth.users,pg_dump -Fc)② Storagereading-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_key是build.rs注入的编译期常量,换 project-ref 必须重新发版(RVH 还要过商店审核), 故灾难恢复应优先「保住原 project-ref」。见 runbook §5.5。 - ✅ 备份异地化已启用(2026-08-13)——
backup-supabase.sh第 6b 步:gpgAES-256 加密整个备份目录 → iCloud DriveLampioBackups/,保留 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-incidentskill(场景路由 → 止血 → 取证 → 只读诊断 → 定点修复 → 验证;诊断完成前禁止任何生产写操作)。 关键项目特异认知: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-builtdocs/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 是独立于 pg_cron 的进程,读一眼
- ✅ 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/ssr的createBrowserClient(session 落 cookie)→proxy.ts对/admin/**做服务端门,getUser()向 Supabase 真验签 (不是getSession()解本地 cookie —— 那是攻击者可随便捏造的字节),再查邮箱白名单。 拦在渲染之前:不放行就没有 Server Component 执行,也就没有数据可序列化。 env 缺失 / Supabase 不可达一律 fail closed。 - 锁:
app/actions/**每个导出动作首行await requireAdmin()。门是按 URL 前缀拦的, 换个路由布局就可能不覆盖;Next 官方口径也是 Server Action 按公开端点对待、函数内部鉴权。 - 顺带:白名单判定完全收回服务端 ——
AuthGuard不再接收adminEmailsprop (它现在只是「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→ 刷新页面仍在登录态 → 登出后回登录页。
- 走了路线 (a) + 动作级锁(当时列的两条路里,(b)「逐页逐 action 照 ops 改」实测更贵: 6 个页面各要拆一次空壳页、14 个 action 改签名、7 个客户端组件传 token,且新页面仍要 靠人记得)。cookie session 让第二层几乎免费 ——
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 的收益远低于爆炸半径 |