- README: add 前端 v2 (React SPA) section (dev/build/codegen/hosting/gates), update directory listing, drop stale Jinja descriptions - architecture-overview: retire '不引入前后端分离' constraint; reflect SPA + JSON API - roadmap: mark M2 done - remove orphaned jinja2 dependency (recompile requirements*.txt; no other churn) - delete empty tests/test_auth.py stub; drop dead _extract_csrf_token in test_api_data - verified image still builds and app imports with the slimmer deps
17 KiB
M2 — 前端 v2(React SPA)
阅读前提:先读
README.md。M2 依赖 M1 完成(单库 + 干净的数据层 + API 建立在合并后的 schema 上)。
1. 目标
用 React SPA 取代现有 Jinja 页面,由 FastAPI 同源托管(同一容器、同一 origin)。一步合并 roadmap 的"前端重写"与"前端做厚":配置界面 + 数据可视化(热力图 / 地图,接管 Grafana)+ 记录的按需展示与小幅增删改。
元目标(agentic 实验):这是用 agent 写 React 的试水,全程尽量不读代码。因此本里程碑强约束 OpenAPI → 类型化 TS client 作为契约护栏:后端 API 先稳,前端永远对着强类型契约写,便宜模型不易跑偏,reviewer 也有客观依据。
2. 现状(M1 完成后)
- 页面仍是服务端 Jinja:
app/api/routes/pages.py(GET/POST /config、/、/admin、POST /config/smtp/test)+app/templates/(base/config/home/login.html、styles.css)。 - 鉴权:
get_current_auth_session(读auth_session_cookie_namecookie),server-side session + 每会话csrf_token内嵌在表单。 app/main.py已app.mount("/static", StaticFiles(...))。- 配置读写逻辑在
app/services/config_page.py(build_config_sections/save_config_updates/build_runtime_settings)。 - 业务数据:单库中的
location、poo_records、public_ip_state、public_ip_history。
3. 目标架构
3.1 后端:JSON API + SPA 托管
- 所有数据交互走 JSON API,统一前缀
/api(SPA 是客户端渲染,必须有 API——这与"同源/同容器"无关)。 - FastAPI 既挂
/api/*,又挂 SPA 静态产物,并对非/api、非静态资源的路径回退到index.html(支持前端路由 deep-link)。 - Jinja 页面在 SPA 达到功能对齐后移除。
3.2 鉴权:复用 session cookie + SPA 版 CSRF
- 继续用现有 HttpOnly session cookie(同源自动携带),M2 不引入 token(token 属 M3)。
- CSRF(已定·简化版):依赖
SameSite=Lax的 session cookie——跨站发起的写请求(POST/PUT/PATCH/DELETE)不会自动携带 cookie,经典 CSRF 主路已被堵;再要求所有写请求带一个自定义 header(跨站无 CORS 预检发不出,且本应用不对外站开放 CORS)作为纵深防御。不做 per-session token 比对(个人自用场景足够)。GET /api/session仍保留,用途是返回当前登录用户、引导 SPA(不再以下发/校验csrf_token为目的)。 - 浏览器面向的所有新端点一律 session 保护;裸 ingestion 端点(设备调用的
POST /location/record、POST /poo/record)维持现状到 M3。
3.3 前端工程
frontend/:Vite + React + TypeScript。- 组件库:Mantine(已定;批电池齐、TS 优先、视觉中性,最贴近此前 Vue 侧 Naive UI 的用法)。
- API client:由后端
openapi/openapi.json自动生成 TS 类型与请求函数(如openapi-typescript+ 轻量 fetch 封装)。生成物入库 +npm run codegen+ CI 校验"生成物与 openapi 同步"(已定)。fetch 封装统一带 cookie、写请求注入自定义 CSRF header、401 跳登录。 - 可视化:Leaflet(已定)——
react-leaflet+leaflet.heat(热力图,头号功能)+leaflet.markercluster(点多时聚合)+ OSM 栅格瓦片(零 key)。地图封在一个自包含组件后面(如<RecordsMap points mode onSelect>,全应用只此处 import leaflet),数据获取/时间窗 state 在外面;这样将来若要换 MapLibre GL 是被隔离的局部重写,不波及其它。 - 状态/数据请求:轻量即可(TanStack Query,已定),不引入重型框架。
3.4 构建与部署
- 多阶段
Dockerfile:node 阶段npm ci && npm run build→ 把frontend/dist拷进 python 镜像的静态目录;运行镜像不带 node。 - compose 仍是单 app 容器(同源)。
4. API 契约(M2 要落地的端点)
全部
/api前缀、session 保护、JSON 进出。具体 schema 在各任务里用 Pydantic 定义,并经export_openapi.py固化。
| 分组 | 端点 | 用途 |
|---|---|---|
| 会话 | GET /api/session |
返回当前用户 + csrf_token;未登录 401 |
| 会话 | POST /api/auth/login |
账号密码登录,下发 session cookie |
| 会话 | POST /api/auth/logout |
注销 |
| 会话 | POST /api/auth/password |
改密(沿用现有强制改密语义) |
| 配置 | GET /api/config |
返回配置 sections(secret 不回显) |
| 配置 | PUT /api/config |
保存配置(留空保留旧 secret 语义不变) |
| 配置 | POST /api/config/smtp/test |
触发测试发信 |
| 数据 | GET /api/locations |
location 记录查询(时间范围/分页,供地图/热力图) |
| 数据 | GET /api/poo |
poo 记录列表(分页) |
| 数据 | GET /api/public-ip |
当前状态 + 变化历史 |
| CRUD | PATCH /api/locations/{person}/{datetime} |
修正单条 location |
| CRUD | DELETE /api/locations/{person}/{datetime} |
删除单条 location |
| CRUD | PATCH /api/poo/{timestamp} |
修正单条 poo |
| CRUD | DELETE /api/poo/{timestamp} |
删除单条 poo |
记录 CRUD 依赖现有 PK 作行标识(location PK=
person+datetime,poo PK=timestamp)。路径参数需对datetime/timestamp做 URL 编码处理。
5. 已锁定决策(讨论后拍板)
以下为与项目所有者讨论后已定的选择。线框图本里程碑不画——按本节 + 各任务卡描述,由实现侧自行合理排版(含移动端布局)。
技术选型
- 组件库 = Mantine。批电池齐、TS 优先、视觉中性、文档好,最贴近此前 Naive UI 的用法,利于 agent 产出一致 UI。
- 地图库 = Leaflet(
react-leaflet+leaflet.heat+leaflet.markercluster,OSM 栅格、零 key)。封在自包含组件后,预留将来迁 MapLibre 的接缝(见 §3.3)。 - OpenAPI client = 生成物入库 +
npm run codegen+ CI 校验"与 openapi 同步"。 - CSRF = 简化版:
SameSite=Laxcookie + 写请求带自定义 header,不做 per-session token(见 §3.2)。 - 前端栈:Vite + React + TS + TanStack Query + Mantine。
- Jinja:SPA 功能对齐后全量移除
templates/与pages.py。
信息架构 / UX
7. 首页主视图 = 地图(热力图为主)+ 时间范围选择器。可视化优先级:热力图(最重要)> 时间选择器(必须)> 散点点位/列表(辅助)。
8. 列表 = 辅助页面,分页(默认页大小 ~100、有上限;前端换页取数,不拉全量)。
9. 记录编辑/删除:location 靠点地图上的点触发(不做 75k 行大列表);poo 靠列表 + 地图点位。
10. 配置入口:config 作为普通页之一,由界面上一个齿轮图标进入。/admin、/ 现状只是重定向到 /config,SPA 不需要单独 admin 页;/ 首页直接给地图主视图(概览 dashboard 列为可选/后续,非 M2 核心)。
11. 响应式 = 要(手机浏览器可用、合理移动端布局)。PWA 列为近期 backlog(见 docs/future-ideas.md),M2 设计即按移动端友好铺路。
范围边界
12. CRUD = 改非主键字段 + 删单行;主键(location=person+datetime、poo=timestamp)不可改;不提供 UI 新建(记录由设备 ingestion 产生)。
13. 裸 ingestion 端点(POST /location/record、POST /poo/record)维持现状到 M3,本里程碑不加保护、不改动。
14. trip / 轨迹连线为可选 / 后续(5 分钟一点 + 手机记录较糙,先不做核心)。
项目定位:个人自用、家庭特化、不开源——设计可按单用户场景简化,不为通用性过度抽象。
6. 任务依赖图
后端 API(可与前端 scaffold 并行)
M2-T01 config API
M2-T02 session/auth API ─┐
M2-T03 data read API ├─► 都产出 OpenAPI 契约
M2-T04 record CRUD API │
M2-T05 smtp/action API ─┘
│ (openapi 稳定后)
▼
M2-T06 前端 scaffold + codegen ──► M2-T07 auth UI
├─► M2-T08 config UI
├─► M2-T09 可视化 UI
└─► M2-T10 records 管理 UI
▼
M2-T11 FastAPI 托管 SPA + 移除 Jinja(依赖 T07–T10 达到对齐)
▼
M2-T12 多阶段 Dockerfile + CI/compose
▼
M2-T13 文档 + OpenAPI 收尾
7. 原子任务(任务卡)
后端任务沿用 M1 的校验闸门(
pytest/ruff/export_openapi)。前端任务的闸门见 §8。
M2-T01 — config JSON API
- Status:
done· Depends: none(M1 完成后) - Context: 把
config_page的读写能力暴露成 JSON,复用现有 service,不重写业务逻辑。 - Files:
create app/api/routes/api/config.py、create app/schemas/config.py;modify app/main.py(注册路由);create tests/test_api_config.py - Steps: 用
build_config_sections/save_config_updates包出GET/PUT /api/config;session 保护;secret 不回显、留空保留旧值语义照搬。 - Acceptance:
- 未登录访问
GET /api/config返回 401。 - 登录后
GET返回 sections,secret 字段被遮罩。 PUT留空 secret 时保留旧值;非法值返回 4xx 且不写库。- 校验闸门全绿(含
openapi/重导出入库)。
- 未登录访问
- Reviewer: 复用了 service 而非复制逻辑;CSRF 校验存在;secret 不泄漏到响应或 OpenAPI 示例。
M2-T02 — session / auth JSON API
- Status:
done· Depends: none - Context: 给 SPA 提供登录/注销/会话探测 + CSRF 下发。
- Files:
create app/api/routes/api/session.py、app/schemas/session.py;modify app/main.py;create tests/test_api_session.py - Steps:
GET /api/session(401 或 user+csrf)、POST /api/auth/login、POST /api/auth/logout、POST /api/auth/password,复用app/services/auth.py。 - Acceptance:
- 正确账号密码登录后置下 HttpOnly session cookie;
GET /api/session返回 user + csrf_token。 - 错误凭据 401,不下发 cookie。
- 写端点缺
X-CSRF-Token或不匹配 → 403。 - 强制改密语义与现有一致。
- 校验闸门全绿。
- 正确账号密码登录后置下 HttpOnly session cookie;
- Reviewer: cookie 仍 HttpOnly、
Secure跟随app_env、SameSite=Lax;密码仍 Argon2,不明文。
M2-T03 — 数据读取 API(locations / poo / public-ip)
- Status:
done· Depends: none - Files:
create app/api/routes/api/data.py、app/schemas/data.py;modify app/main.py;create tests/test_api_data.py - Steps:
GET /api/locations(时间范围 + 分页)、GET /api/poo(分页)、GET /api/public-ip(state + history);session 保护;查询参数有上限防全表导出。 - Acceptance:
- 分页/时间范围参数生效且有上限;越权未登录 401。
- 返回 schema 经 OpenAPI 固化。
- 校验闸门全绿。
- Reviewer: 查询走索引/PK,无 N+1;时间过滤边界正确。
M2-T04 — 记录 CRUD API(修正 / 删除)
- Status:
done· Depends: M2-T03 - Files:
modify app/api/routes/api/data.py、app/services/location.py、app/services/poo.py;create tests/test_api_record_crud.py - Steps:
PATCH/DELETElocation(PK person+datetime)与 poo(PK timestamp);session + CSRF 保护;PK 路径参数 URL 解码;删除是硬删单行(不是清表)。 - Acceptance:
- PATCH 改单行字段、DELETE 删单行,行数变化精确为 1。
- 不存在的 PK → 404。
- 缺 CSRF → 403。
- 没有任何"批量删/清表"路径。
- 校验闸门全绿。
- Reviewer: 删除限定单 PK;编辑校验输入;ingestion 裸端点未被顺手加保护或改动。
M2-T05 — SMTP 测试 / 动作类 JSON API
- Status:
done· Depends: M2-T01 - Files:
modify app/api/routes/api/config.py;modify tests/test_api_config.py - Steps:
POST /api/config/smtp/test复用send_smtp_test_email,返回结构化结果(success / config-error / failed)。 - Acceptance:
- 三种结果都有明确 JSON 状态码/字段;session + CSRF 保护。
- 校验闸门全绿。
M2-T06 — 前端 scaffold + OpenAPI codegen [structural]
- Status:
done· Depends: M2-T01..T05(OpenAPI 已稳定) - Context: 建
frontend/工程与类型化 client 流水线,这是后续所有前端任务的地基。 - Files:
create frontend/(Vite+React+TS 脚手架、package.json、tsconfig.json、eslint、vitest、.gitignore)、frontend/src/api/(codegen 产物 + fetch 封装,自动注入X-CSRF-Token)、frontend/README.md、npm run codegen脚本 - Steps: 初始化 Vite React-TS;接
openapi/openapi.json生成类型;写一个最小 App 壳 + 受保护路由骨架;fetch 封装统一带 cookie、写请求注入 CSRF header、401 跳登录。 - Acceptance:
npm ci && npm run build成功产出frontend/dist。npm run lint、npm run typecheck、npm run test全绿(哪怕只有 1 个 smoke 测试)。npm run codegen生成物与当前openapi/openapi.json一致(CI 可校验)。
- Reviewer: client 全部基于生成类型;CSRF/cookie/401 处理在统一封装层;无手写、与契约不符的请求类型。
M2-T07 — 鉴权 UI(登录 / 会话引导 / 改密)
- Status:
done· Depends: M2-T06 - Acceptance: 登录成功进受保护区;未登录访问受保护路由跳登录;强制改密流程可走完;
build/lint/typecheck/test全绿。
M2-T08 — 配置 UI(取代 Jinja config 页)
- Status:
done· Depends: M2-T06 - Acceptance: 能读/存所有现有配置 section;secret 不回显、留空保留;SMTP 测试按钮反映三态;前端闸门全绿。
M2-T09 — 数据可视化 UI(热力图为主的地图)
- Status:
done· Depends: M2-T06(数据来自 T03) - Context: 接管 Grafana 原职责,且首页主视图就是这张地图。优先级:① 热力图(最重要)② 时间范围选择器(必须)③ 散点点位(辅助,主要服务编辑/删除)。location:去过哪的密度;poo:狗最爱在哪拉。
- Acceptance: 首页渲染热力图(location / poo);时间范围选择器生效、只取窗口内数据(不拉全量);散点层可切换、点选某点可进入编辑/删除(接 T10/T04);location 点多时聚合;响应式(手机浏览器可用);前端闸门全绿。
M2-T10 — 记录管理 UI(按需展示 + 增删改)
- Status:
done· Depends: M2-T06(CRUD 来自 T04) - Acceptance: 列表分页展示 poo/location;可编辑、可删除单条并即时刷新;删除有二次确认;前端闸门全绿。
M2-T11 — FastAPI 托管 SPA + 移除 Jinja
- Status:
done· Depends: M2-T07, T08, T09, T10 - Files:
modify app/main.py(挂载 SPA 静态目录 + 非/api路径回退index.html);delete app/templates/、app/api/routes/pages.py(功能对齐后);modify tests(移除 Jinja 页面测试,新增 SPA fallback 测试) - Acceptance:
/config等路径返回 SPA(index.html),/api/*不被 fallback 吞掉,/static/资源正常。- 旧 Jinja 模板与 pages 路由移除后
pytest全绿。 - 校验闸门全绿(含 OpenAPI 重导出)。
- Reviewer: fallback 不拦截
/api、/docs、/openapi.json、静态资源;未登录访问 API 仍 401(不是被 SPA 壳吞掉)。
M2-T12 — 多阶段 Dockerfile + CI/compose
- Status:
done· Depends: M2-T11 - Files:
modify Dockerfile(node build 阶段 → 拷dist进 python 镜像);modify .github/workflows/*(加前端 build/lint/typecheck);modify tests/test_deployment.py(镜像断言更新) - Acceptance:
- 镜像构建成功且运行镜像不含 node 运行时。
- CI 跑前端闸门 + 后端
pytest。 - 校验闸门全绿。
M2-T13 — 文档 + OpenAPI 收尾
- Status:
done· Depends: M2-T12 - Acceptance: README 增"前端 v2"段(开发/构建说明);architecture 退役"不前后端分离"约束;roadmap 勾选 M2;
openapi/已同步入库。
8. 前端校验闸门(前端任务每次结束都要全绿)
在 frontend/ 下:
npm ci
npm run codegen # 生成类型化 client;产物须与 openapi/openapi.json 同步
npm run lint
npm run typecheck
npm run test
npm run build # 必须产出 dist
- 后端若同任务改了路由/schema,仍需根目录
python scripts/export_openapi.py并提交openapi/。 - "codegen 产物与 OpenAPI 同步"应在 CI 校验(生成后
git diff --exit-code)。
9. 里程碑完成定义(DoD)
- 访问应用得到 React SPA;配置、可视化、记录增删改都在 SPA 内完成。
- 所有浏览器交互走
/apiJSON 端点,session + CSRF 保护;ingestion 裸端点维持现状(留给 M3)。 - Jinja
templates/与 pages 路由移除;FastAPI 同源托管 SPA。 - 多阶段镜像构建通过;CI 含前端闸门。
- 后端
pytest/ruff/export_openapi+ 前端build/lint/typecheck/test全绿。