Skip to content

Lampio Admin — 项目指南

本文件是 Claude Code 的项目控制中心(嵌套 CLAUDE.md,工作在 admin/ 下时自动加载)。 修改时间以 git log -1 -- admin/CLAUDE.md 为准(手写「更新日期」必然漂移)。

📎 同目录还有一份 admin/AGENTS.md,5 行,不要并进来 —— 它是 Next.js 工具链自己维护的注入块(<!-- BEGIN:nextjs-agent-rules -->END), 内容是「本版 Next.js 有破坏性变更,写代码前先读 next 包自带的 dist/docs/ 目录」。 (那个路径刻意不写进反引号 —— 它在 node_modules 下,CI 不装 admin 依赖, 写成反引号会让 check:claude-paths 在 CI 上恒红而本机恒绿。2026-08-30 踩过一次。) 带 BEGIN/END 标记的块会被工具重新写回,手工删掉多半下次 next 升级又长回来 —— 所以这里只引用。(2026-08-30 裁定,原 backlog 条目建议「并入」,实测后改判。)

⚠️ 2026-07-19 合仓:本项目已 git subtree 迁入 Lampio 单仓,物理位置 = admin/(原独立仓 ttfishnet/readvocab-admin 归档——历史仓名,非当前品牌)。Supabase 的 SQL/edge functions 就在同仓 supabase/——schema 契约可同会话原子改,不再跨仓手抄。在 admin/ 下改代码用 scoped skill admin:code-review / admin:doc-sync-check / deploy(RB 的 arch-check/build-check/code-review 已排除 admin/)。详见 docs/plans/archive/admin-merge-plan.md


1. 项目概览

产品定位

Lampio 产品的管理后台。

  • /已下线:重定向到 /admin。公开落地页 2026-07-19 拆为独立子项目 landing/(独立 Vercel 部署,不与本后台同体,见 landing/CLAUDE.md
    • docs/plans/archive/landing-split-plan.md)。
  • /admin — 管理后台(需登录,内容管理 / 数据分析 / 配置管理 / 词库管理)
  • 使用者:个人开发者(1 人)

技术栈

技术
框架Next.js 16.2.3 (App Router)
语言TypeScript (strict mode)
样式Tailwind CSS 4 + shadcn/ui
图表Recharts
数据层@supabase/supabase-js (service_role for admin)
AuthSupabase Auth + @supabase/ssrcookie session —— 服务端要能鉴权,见 §3 红线 6);admin email 白名单
部署Vercel(静态 + Serverless)
包管理pnpm

项目结构

admin/
├── app/
│   ├── layout.tsx                         ← 全局 layout
│   ├── page.tsx                           ← 根路径,redirect('/admin')(landing 已拆出)
│   ├── actions/                           ← Server Actions
│   │   ├── recommendations.ts             ← 站点/feed 写 + pin/批准/否决/恢复/OPML
│   │   ├── recommend-config.ts            ← 阈值配置/黑名单写 + 手动触发 edge
│   │   └── vocabulary.ts
│   └── admin/
│       ├── layout.tsx                     ← Admin 根 layout (pass-through)
│       ├── (auth)/login/page.tsx          ← 登录页(无侧边栏)
│       └── (dashboard)/
│           ├── layout.tsx                 ← Dashboard layout(Sidebar + AuthGuard)
│           ├── page.tsx                   ← Dashboard 概览
│           ├── recommendations/page.tsx   ← 推荐内容管理(sites 含 pin 列)
│           ├── observability/page.tsx     ← 运营监控 (force-dynamic, 6 tab)
│           ├── ops/page.tsx               ← 基础设施运维(**空壳页**,数据走鉴权 Action —— 不依赖 URL 门的兜底)
│           ├── analytics/page.tsx         ← 数据分析
│           ├── vocabulary/page.tsx        ← 词库管理 (force-dynamic)
│           └── settings/page.tsx          ← 配置管理 (force-dynamic)
├── components/
│   ├── ui/                                ← shadcn/ui 组件
│   └── admin/                             ← Admin 专用组件(landing 组件已拆到 landing/)
├── lib/
│   ├── supabase-server.ts                 ← Server-side Supabase client (service_role)
│   ├── supabase-client.ts                 ← 浏览器 Supabase client(anon,@supabase/ssr → cookie session)
│   ├── auth.ts                            ← 🔒 白名单 + `requireAdmin()`(从 cookie 验签,见 §3 红线 6)
│   ├── queries.ts                         ← 产品运营查询(推荐/成本/遥测)
│   ├── queries-ops.ts                     ← 基础设施运维查询(备份/巡检/容量/凭据)
│   ├── ops-external.ts                    ← 外部平台只读探查(GitHub/Sentry/health),不 throw
│   ├── ops-constants.ts                   ← ops 纯常量(客户端可安全 import,见 §3.7)
│   ├── ops-status-rules.ts                ← 🔒 ops 页**判据**唯一真相源(见 §3 红线 9)
│   ├── security-headers.ts                ← 🔒 安全头/CSP 单一定义处(3 个消费者共用)
│   ├── sentry-scrub.ts                    ← Sentry 脱敏层 + 三处 init 共用配置
│   └── utils.ts                           ← cn() helper
├── proxy.ts                               ← Next 16 中间件:🔒 `/admin/**` 服务端鉴权门 + 下发带 nonce 的 CSP
├── instrumentation.ts                     ← Sentry 按 runtime 分派 + onRequestError
├── instrumentation-client.ts              ← Sentry 浏览器侧 init
├── sentry.{server,edge}.config.ts         ← Sentry Node / Edge runtime init
├── playwright.config.ts                   ← E2E 配置(webServer 跑生产构建,非 dev)
├── tests/
│   ├── unit/sentry-scrub.spec.ts          ← 脱敏函数
│   ├── unit/ops-status-rules.spec.ts      ← ops 判据(每组成对:正向 + 🔴 旧判据反向断言)
│   ├── unit/release-consistency.spec.ts   ← 发布链路三路比对
│   └── e2e/                               ← 安全头 / CSP 运行时 / 密钥不外泄 / 🔒 鉴权门 auth-gate
├── docs/                                  ← 项目文档
├── .env.example                           ← 变量清单模板(入库,无值)
├── .env.local                             ← 环境变量(不入库)
└── CLAUDE.md                              ← 本文件

加固细节(为什么 CSP 拆两处下发、为什么 app/layout.tsx 必须 force-dynamic、 为什么不用 'strict-dynamic'、E2E 为什么对生产构建跑)见 docs/plans/admin-hardening-plan.md


2. 跨项目协作

关联项目

项目路径技术栈关系
Lampio (RB)仓根Tauri 2 + Rust + React + SQLite桌面客户端
Lampio 移动端 (RVH)rvh(2026-08-28 filter-repo 合入)Flutter/Dart移动客户端
本项目admin(2026-07 subtree 迁入)Next.js 16.2.3管理后台

RB + admin + RVH 现同处一个 git 仓(Supabase 契约共享;RVH 于 2026-08-28 合入,见 docs/plans/archive/rvh-merge-plan.md)。三者通过 Supabase 共享数据库和 Auth。

Supabase Schema 源

  • DDL 源文件(权威)supabase/sql/sync-tables.sql(10 张 user_ 同步表 + 3 张推荐表 + RLS)+ supabase/sql/{recommend-pool-governance,observability-tables}.sql(治理表)。现在同仓,相对路径 ../supabase/sql/
  • vocabulary 表:Supabase 侧是「预装库外缓冲池」(PK=word,无 id 列;发版清空)。权威词库=RB/RVH 共享预装 lampio_dict.db
  • Schema 变更规则:涉及 Supabase 同步表结构变更需同时评估 RB 和 RVH(RVH 已同仓,但工具链是 flutter/dart ⇒ 改 rvh/lib/** 仍需新会话,判据见根 CLAUDE.md §9)。改动 admin 手抄的表/列前,先核对 ../supabase/sql/sync-tables.sql 当前列集——2026-07 表名重命名 + 2026-04-28 vocabulary word-PK 改造都曾让 admin 静默漂移。
  • 单一 schema 真相源(gen-types,2026-07-20 起)lib/database.types.tspnpm gen:types 从 live 库自动生成(supabase gen types typescript --linked,无需 Docker)。它是 admin 侧列/类型的权威——vocabularyidpos_definitions: Json 等已由类型编码,谁再手抄错列编译期即报错。schema 改动后重跑 pnpm gen:typestsc --noEmit 兜底。
    • P1 ② 完成(2026-07-20)lib/supabase-{server,client}.tscreateClient<Database>lib/queries.ts 手抄的行/表 interface 全部改派生(Row<...> / Pick<Row<...>>),纯计算/投影 DTO 保留显式 interface;admin/docs/database.md 瘦身为「Admin 使用」用法映射 + pos_definitions JSON 形状领域知识(列明细改指生成类型)。真实类型暴露的可空列 drift 已在 queries + 6 组件收窄。见 docs/plans/archive/admin-integration-followups.md §五。

跨项目决策参考

当需要理解全局架构时,读取:

  • RB 全貌:仓根 CLAUDE.md(含仓库地图、双端契约红线、git 纪律)
  • RVH 全貌:rvh/CLAUDE.md(含 Clean Architecture 和 14 模块结构)
  • 数据库全貌:docs/database-schema.md
  • 本项目架构:docs/architecture.md

3. 技术红线

以下规则必须遵守,违反可能导致安全事故或数据损坏:

  1. service_role key 绝不暴露给浏览器

    • 只在 Server Components / Server Actions / Route Handlers 中使用
    • Client Components 只用 anon key(仅做 Auth 登录/登出)
    • 绝不在 "use client" 文件中 import supabase-server.ts
    • ⚠️ 破坏这条不需要谁真的写错 import:只要 Server Component 把含 key 的对象(或一条带 key 的报错)当 prop 传给 Client Component,Next 就会把它序列化进 HTML 的 RSC flight payload。
    • 回归守门:tests/e2e/no-secret-leak.spec.ts实际下发到浏览器的字节(HTML + 全部 JS chunk)。 改动涉及 server→client 边界后跑一次 pnpm test:e2e
  2. 隐私数据只做聚合查询

    • 分析页只用 COUNT / AVG / GROUP BY,不返回单个用户数据
    • 不展示用户学习的具体单词、笔记内容
  3. 🔒 app/layout.tsxforce-dynamic 是 nonce-CSP 的硬前提,不是性能取舍

    • nonce 每请求现生成,而静态预渲染发生在 build 期(那时没有请求头、拿不到 nonce)。
    • 某路由一旦被静态化,产物 HTML 里 Next 的内联 bootstrap 脚本(self.__next_f.push, 承载 RSC payload)就没有 nonce,却要在带 nonce 的 CSP 下执行 → 被拦 → JS 文件能加载 但无法 hydrate
    • ⚠️ 表现是白屏或「点什么都没反应」,HTTP 依然 200 —— header 断言完全抓不到这类故障。
    • 回归守门:tests/e2e/csp-runtime.spec.ts 用真浏览器 +「输入框真的能打字」来验。
    • 📌 不要因为「这页数据量小、不必 dynamic」就摘掉它 —— 本条与页面大小无关。 (完整推理与另两个刻意取舍见 docs/plans/admin-hardening-plan.md; 该 plan 归档后本条仍以此处为准。)
  4. Server Components 优先

    • 默认使用 Server Components,需要交互时才用 Client Components
  5. 所有 Supabase 查询封装在 lib/queries*.ts

    • 不在页面组件中直接写 Supabase 调用
    • 写操作通过 app/actions/ 中的 Server Actions
    • 运维相关的查询在 lib/queries-ops.ts(与产品运营查询分文件,读者与改动频率不同)
  6. 🔒 鉴权是两层:proxy.ts 的门 + 每个 Server Action 里的锁。两层都不能省。

    session 存在 cookielib/supabase-client.ts@supabase/ssrcreateBrowserClient)→ 服务端每个请求都拿得到 auth 上下文。

    在哪管什么
    proxy.ts/admin/**渲染之前拦截:getUser() 验签 + 邮箱白名单,不过就 307 到登录页
    app/actions/** 每个导出动作首行 await requireAdmin()动作执行前再验一次,不依赖 URL 匹配
    • lib/supabase-client.ts 回普通 createClient = 把洞重新打开。 session 一旦回到 localStorage,服务端又变成「无凭据可查」,Server Component 渲染的数据照样进 未登录请求也能拿到的 RSC flight payload(2026-08-14 实测 curl /admin/observability 无凭据即取到运营数据 —— AuthGuard 挡界面不挡字节)。
    • 门为什么不够:它按 URL 前缀拦,而 Server Action 的 POST 只是恰好打在 /admin/** 上。换个路由布局这个「恰好」就没了。Next 官方口径:Server Action 按公开 HTTP 端点对待,在函数内部鉴权。
    • 锁为什么不够:它拦不住 Server Component —— 页面渲染不经过任何 action。
    • 验签必须用 getUser()不能用 getSession():后者只解本地 cookie 的内容, 那是攻击者可以随便捏造的字节。
    • AuthGuard 不是安全边界,只是「session 过期就跳走」的交互层,且不再接收 adminEmails —— 白名单是服务端策略,序列化给客户端既无用又多泄一份。
    • 新增敏感页:直接写常规 Server Component 即可,门会保护它;新增 Server Action: 首行必须 await requireAdmin()。两者都有回归断言守着,见 tests/e2e/auth-gate.spec.ts(其中一条会扫源码,漏写 requireAdmin 直接红)。
    • ⚠️ 新增 app/api/ 下的 Route Handler 不在门的覆盖范围内(只有 /admin/** 过门)—— 要么放到 /admin 前缀下,要么自己调 requireAdmin()/api/health 是刻意公开的。
    • as-built:docs/plans/product-launch-todo.md §C5。
  7. 🔒 外部平台 token 只在服务端读,且必须同步三处

    • SENTRY_READ_TOKEN / GITHUB_READ_TOKEN 绝不加 NEXT_PUBLIC_,只在 Server Component / Action 里读,只把结论(数字/版本号)交给渲染层。
    • 新增任何持 token 的外部调用时,必须同时:① 往 lib/sentry-scrub.tsSECRET_VALUE_PATTERNS 加该 token 的形状(否则它被拼进异常 message / URL 就直接进 Sentry,且不会报错);② 补 tests/unit/sentry-scrub.spec.ts 断言; ③ 补 tests/e2e/no-secret-leak.spec.ts 的字节扫描。
    • Client Component 要用的常量放 lib/ops-constants.ts —— 从 lib/queries-ops.ts import 值会把 supabase-server(持 service_role)拖进 client graph。
  8. 🔒 ops 页每个状态判据只能有一份,写在 lib/ops-status-rules.ts

    卡片、总览条、首页横幅调用同一个函数。展示组件里不许出现自己算颜色的 三元表达式。

    • 这条不是洁癖,是本页最贵的缺陷来源。 判据一旦有两份就会漂:卡片三档、 总览两档 → 一条 present 的 warn finding 卡片黄、总览绿;容量卡看 DB + Storage 两条水位、总览只看 DB → Storage 涨到 95% 卡片红、总览绿; 发布链路则反过来,卡片 warn、总览 critical。三个方向都出现过(2026-08-25 实测)。
    • ⚠️ 失效是静默的:两处各自都「看起来对」,没有任何东西会报错。 修一次同步一次没有用 —— 下次照样漂。要点是让它们没有第二份判据可用
    • 该模块必须保持纯函数、零 IO、不 import 任何服务端模块(它被 Client Component 引用,拖进 queries-ops 就把 service_role 带进 client graph, 见红线 1 与 7)。入参用结构类型,时间由调用方传 now
    • 改判据必须成对补断言:正向(故障输入会变色)+ 🔴 反向(把旧判据的表达式 原样写进用例,钉住它当时会说 ok)。见 tests/unit/ops-status-rules.spec.ts。 只验通过路径 = 没验。
    • 判据变化同步到 docs/verification/ops-monitoring.md 的 J 行 —— 那份是反复重跑的清单,判据换了而清单没换,下一轮会照着旧问法验一遍。
  9. 环境变量安全

  • .env.local 不入库(已在 .gitignore)
  • NEXT_PUBLIC_ 前缀变量会暴露给浏览器,只放 anon key / URL / Sentry DSN

4. 开发工作流

日常流程

改代码 → pnpm dev 本地调试 → pnpm typecheck → pnpm test → /code-review → /doc-sync-check → git commit

验证命令

命令说明
pnpm typechecktsc --noEmit不是 RB 的 cargo / vite)
pnpm linteslint(当前 0 error,保持住)
pnpm testPlaywright 全套(unit + e2e,60 tests:58 pass + 2 条按需 skip)
pnpm test:unit只跑脱敏函数等纯逻辑
pnpm test:e2e只跑浏览器侧

E2E 的 webServer 跑的是 next build && next start(生产构建),不是 next dev —— dev 与 prod 的 CSP 刻意不同,对着 dev server 跑等于什么都没验。故首次跑要等一次 build。

部署(现状)

Vercel project lampio-admin 已接本仓 Git(Root Directory=admin), push 到 main 即自动部署.github/workflows/ci-admin.ymladmin/** 变更时跑质量门, 但 Vercel 不等它 —— CI 是事后告警。要真正卡住得走 PR 流程(分支 → PR → CI 绿 → 合 main)。 /deploy skill 里的 vercel deploy --prod 是合仓前的路径,现在只在需要手动补发时用。

Commit 格式

<type>(<scope>): <subject>

Co-Authored-By: Claude <noreply@anthropic.com>

type: feat / fix / refactor / docs / chore scope: dashboard / recommendations / vocabulary / analytics / settings / auth

Plan 持久化

进入 Plan Mode 时,将计划保存到 docs/plans/<plan-name>.md,确保跨会话可追溯。

部署流程

使用 /deploy skill 一键完成:build 检查 → git commit → push → vercel deploy --prod


5. 安全操作规则

需要用户确认的操作

类别操作
Gitgit reset --hard, git push --force, git branch -D
文件删除非临时文件、修改 .env.local
数据库修改 Supabase 表结构、删除数据、修改 RLS 策略
部署vercel deploy --prod(通过 /deploy skill 自动触发,需确认)

安全操作(无需确认)

pnpm dev, pnpm build, git status/log/diff, 读取文件, 运行 ESLint


6. 项目 Skills

Skill触发用途
/code-review代码变更后深度审查:Next.js 规范、安全、性能
/doc-sync-check功能完成后检查哪些文档需要同步更新
/deploy准备发布时build + commit + push + vercel deploy

推荐链条:/code-review/doc-sync-check/deploy


7. Supabase 信息

连接信息

  • Project URL: https://jdtbyteiwnciqnfppztz.supabase.co
  • anon key: 存在 .env.local(不入库)
  • service_role key: 存在 .env.local(不入库,admin 后台使用)

表结构概览

用户同步表(10 张,RLS per-user)— 2026-07 全端重命名后的当前名

权威列集见 ../supabase/sql/sync-tables.sql。旧名(notebook_entries / reading_sources / word_sources / excluded_words 等)已废弃——admin 若还引用旧名即为漂移。

说明admin 是否读
user_learning_entriesSM-2 学习状态(原 user_notebook_entries;word 关联 vocabulary,无 vocabulary_id 列✅ 分析页
user_reading_pages页面来源(原 user_reading_sources)✅ source_type 分布
user_reading_notes阅读笔记容器
user_word_page_links词-来源关联(原 user_word_sources)
user_word_cloze_contexts复习卡 cloze 语境(v24 起同步)
user_known_words已认识词(原 user_excluded_words)
user_favorite_sites收藏站点
user_rss_feedsRSS 订阅
user_domain_prefs域名偏好
user_page_annotations页面标注

user_reference_words 已废弃(reference_words 改纯系统共享表,不再同步);user_word_contexts 旧表已下线。

推荐公开表(3 张)

说明
recommended_sites推荐站点池(稳定核心,Phase 2 加 is_pinned/admitted_at/cefr_band)
recommended_feeds推荐 RSS 源
recommended_articlesAI 推荐文章(7 天过期)

推荐治理表(5 张,service-only,DDL 源在 RB)

Phase 1+2 由 RB edge 写入,admin(Phase 3)只读 + 写治理表。仅 service_role 可读,绝不进 client bundle。

说明
llm_call_log每次 LLM 调用:token/cost/latency/status
edge_run_log每次 function 运行:mode/trigger/status(含 budget_skipped)/summary/cost
recommend_audit_logrecommend 决策审计:action/reason/score_detail(多维分 jsonb)
recommend_config单行(id=1) typed 阈值表(pool_target_size / min_admit_quality / monthly_llm_budget_usd 等 8 字段)
recommended_blocklist持久拒绝记忆:domain UNIQUE + source(auto_reject/eliminated_dead/manual)

词库表

说明
vocabulary预装库外缓冲池(发版清空),PK=word,无 id 列(2026-04-28 word-PK 改造)。列:word / primary_cefr_level / cefr_inferred / cefr_source / pos_definitions(jsonb) / etymology / word_tags / frequency_rank / ipa_pronunciation / pronunciation_url / source / word_forms / word_family / audio_local_path / 时间戳。权威词库=RB/RVH 预装 lampio_dict.db

RPC 函数

函数用途
get_user_count查询 auth.users 总数
get_registration_trend查询近 N 天注册趋势

8. 环境信息

项目版本
Node.js22.14.0
pnpm10.8.0
Next.js16.2.3

9. 代码规范

  • 使用 TypeScript strict mode
  • 组件使用 PascalCase,文件名 kebab-case
  • Server Components 优先,需交互时用 Client Components
  • shadcn/ui 新版不支持 asChild,使用 buttonVariants + Link 替代
  • Select onValueChange 传入 string | null,需处理 null
  • Recharts label 函数需用 PieLabelRenderProps 类型