Files
home-automation/docs/design/m2-frontend-v2.md
T
tliu93 66ec9979cc
pytest / test (push) Failing after 11m46s
docs(m2): lock M2 frontend design decisions
Record the decisions reached in planning into docs/design/m2-frontend-v2.md:
component library = Mantine; map = Leaflet (react-leaflet + leaflet.heat +
markercluster, isolated behind a component seam for a future MapLibre swap);
OpenAPI typed client committed + CI-checked; CSRF simplified to SameSite=Lax +
a custom write header (no per-session token); heatmap-first map as the home
view with a required time-range picker and an auxiliary paginated list;
record CRUD edits non-PK fields and deletes single rows (no UI create); bare
ingestion endpoints stay until M3; trips optional. Wireframes intentionally
skipped for this milestone.
2026-06-12 22:40:57 +02:00

17 KiB
Raw Blame History

M2 — 前端 v2React 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 完成后)

  • 页面仍是服务端 Jinjaapp/api/routes/pages.pyGET/POST /config//adminPOST /config/smtp/test+ app/templates/base/config/home/login.htmlstyles.css)。
  • 鉴权:get_current_auth_session(读 auth_session_cookie_name cookie),server-side session + 每会话 csrf_token 内嵌在表单。
  • app/main.pyapp.mount("/static", StaticFiles(...))
  • 配置读写逻辑在 app/services/config_page.pybuild_config_sections / save_config_updates / build_runtime_settings)。
  • 业务数据:单库中的 locationpoo_recordspublic_ip_statepublic_ip_history

3. 目标架构

3.1 后端:JSON API + SPA 托管

  • 所有数据交互走 JSON API,统一前缀 /api(SPA 是客户端渲染,必须有 API——这与"同源/同容器"无关)。
  • FastAPI 既挂 /api/*,又挂 SPA 静态产物,并对非 /api、非静态资源的路径回退到 index.html(支持前端路由 deep-link)。
  • Jinja 页面在 SPA 达到功能对齐后移除。
  • 继续用现有 HttpOnly session cookie(同源自动携带),M2 不引入 tokentoken 属 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/recordPOST /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 构建与部署

  • 多阶段 Dockerfilenode 阶段 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 返回配置 sectionssecret 不回显)
配置 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+datetimepoo PK=timestamp)。路径参数需对 datetime/timestamp 做 URL 编码处理。

5. 已锁定决策(讨论后拍板)

以下为与项目所有者讨论后已定的选择。线框图本里程碑不画——按本节 + 各任务卡描述,由实现侧自行合理排版(含移动端布局)。

技术选型

  1. 组件库 = Mantine。批电池齐、TS 优先、视觉中性、文档好,最贴近此前 Naive UI 的用法,利于 agent 产出一致 UI。
  2. 地图库 = Leafletreact-leaflet + leaflet.heat + leaflet.markerclusterOSM 栅格、零 key)。封在自包含组件后,预留将来迁 MapLibre 的接缝(见 §3.3)。
  3. OpenAPI client = 生成物入库 + npm run codegen + CI 校验"与 openapi 同步"。
  4. CSRF = 简化版SameSite=Lax cookie + 写请求带自定义 header不做 per-session token(见 §3.2)。
  5. 前端栈Vite + React + TS + TanStack Query + Mantine。
  6. JinjaSPA 功能对齐后全量移除 templates/pages.py

信息架构 / UX 7. 首页主视图 = 地图(热力图为主)+ 时间范围选择器。可视化优先级:热力图(最重要)> 时间选择器(必须)> 散点点位/列表(辅助)。 8. 列表 = 辅助页面,分页(默认页大小 ~100、有上限;前端换页取数,不拉全量)。 9. 记录编辑/删除location 靠点地图上的点触发(不做 75k 行大列表);poo 靠列表 + 地图点位。 10. 配置入口:config 作为普通页之一,由界面上一个齿轮图标进入。/admin/ 现状只是重定向到 /configSPA 不需要单独 admin 页/ 首页直接给地图主视图(概览 dashboard 列为可选/后续,非 M2 核心)。 11. 响应式 = 要(手机浏览器可用、合理移动端布局)。PWA 列为近期 backlog(见 docs/future-ideas.md),M2 设计即按移动端友好铺路。

范围边界 12. CRUD = 改非主键字段 + 删单行;主键(location=person+datetime、poo=timestamp不可改不提供 UI 新建(记录由设备 ingestion 产生)。 13. 裸 ingestion 端点POST /location/recordPOST /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(依赖 T07T10 达到对齐)
        ▼
  M2-T12 多阶段 Dockerfile + CI/compose
        ▼
  M2-T13 文档 + OpenAPI 收尾

7. 原子任务(任务卡)

后端任务沿用 M1 的校验闸门(pytest / ruff / export_openapi)。前端任务的闸门见 §8。

M2-T01 — config JSON API

  • Status: todo · Depends: noneM1 完成后)
  • Context: 把 config_page 的读写能力暴露成 JSON,复用现有 service,不重写业务逻辑。
  • Files: create app/api/routes/api/config.pycreate app/schemas/config.pymodify app/main.py(注册路由);create tests/test_api_config.py
  • Steps: 用 build_config_sections/save_config_updates 包出 GET/PUT /api/configsession 保护;secret 不回显、留空保留旧值语义照搬。
  • Acceptance:
    • 未登录访问 GET /api/config 返回 401。
    • 登录后 GET 返回 sectionssecret 字段被遮罩。
    • PUT 留空 secret 时保留旧值;非法值返回 4xx 且不写库。
    • 校验闸门全绿(含 openapi/ 重导出入库)。
  • Reviewer: 复用了 service 而非复制逻辑;CSRF 校验存在;secret 不泄漏到响应或 OpenAPI 示例。

M2-T02 — session / auth JSON API

  • Status: todo · Depends: none
  • Context: 给 SPA 提供登录/注销/会话探测 + CSRF 下发。
  • Files: create app/api/routes/api/session.pyapp/schemas/session.pymodify app/main.pycreate tests/test_api_session.py
  • Steps: GET /api/session401 或 user+csrf)、POST /api/auth/loginPOST /api/auth/logoutPOST /api/auth/password,复用 app/services/auth.py
  • Acceptance:
    • 正确账号密码登录后置下 HttpOnly session cookieGET /api/session 返回 user + csrf_token。
    • 错误凭据 401,不下发 cookie。
    • 写端点缺 X-CSRF-Token 或不匹配 → 403。
    • 强制改密语义与现有一致。
    • 校验闸门全绿。
  • Reviewer: cookie 仍 HttpOnly、Secure 跟随 app_envSameSite=Lax;密码仍 Argon2,不明文。

M2-T03 — 数据读取 APIlocations / poo / public-ip

  • Status: todo · Depends: none
  • Files: create app/api/routes/api/data.pyapp/schemas/data.pymodify app/main.pycreate tests/test_api_data.py
  • Steps: GET /api/locations(时间范围 + 分页)、GET /api/poo(分页)、GET /api/public-ipstate + history);session 保护;查询参数有上限防全表导出。
  • Acceptance:
    • 分页/时间范围参数生效且有上限;越权未登录 401。
    • 返回 schema 经 OpenAPI 固化。
    • 校验闸门全绿。
  • Reviewer: 查询走索引/PK,无 N+1;时间过滤边界正确。

M2-T04 — 记录 CRUD API(修正 / 删除)

  • Status: todo · Depends: M2-T03
  • Files: modify app/api/routes/api/data.pyapp/services/location.pyapp/services/poo.pycreate tests/test_api_record_crud.py
  • Steps: PATCH/DELETE locationPK person+datetime)与 pooPK timestamp);session + CSRF 保护;PK 路径参数 URL 解码;删除是硬删单行(不是清表)。
  • Acceptance:
    • PATCH 改单行字段、DELETE 删单行,行数变化精确为 1。
    • 不存在的 PK → 404。
    • 缺 CSRF → 403。
    • 没有任何"批量删/清表"路径。
    • 校验闸门全绿。
  • Reviewer: 删除限定单 PK;编辑校验输入;ingestion 裸端点未被顺手加保护或改动。

M2-T05 — SMTP 测试 / 动作类 JSON API

  • Status: todo · Depends: M2-T01
  • Files: modify app/api/routes/api/config.pymodify 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: todo · Depends: M2-T01..T05OpenAPI 已稳定)
  • Context: 建 frontend/ 工程与类型化 client 流水线,这是后续所有前端任务的地基。
  • Files: create frontend/Vite+React+TS 脚手架、package.jsontsconfig.json、eslint、vitest、.gitignore)、frontend/src/api/codegen 产物 + fetch 封装,自动注入 X-CSRF-Token)、frontend/README.mdnpm 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 lintnpm run typechecknpm run test 全绿(哪怕只有 1 个 smoke 测试)。
    • npm run codegen 生成物与当前 openapi/openapi.json 一致(CI 可校验)。
  • Reviewer: client 全部基于生成类型;CSRF/cookie/401 处理在统一封装层;无手写、与契约不符的请求类型。

M2-T07 — 鉴权 UI(登录 / 会话引导 / 改密)

  • Status: todo · Depends: M2-T06
  • Acceptance: 登录成功进受保护区;未登录访问受保护路由跳登录;强制改密流程可走完;build/lint/typecheck/test 全绿。

M2-T08 — 配置 UI(取代 Jinja config 页)

  • Status: todo · Depends: M2-T06
  • Acceptance: 能读/存所有现有配置 section;secret 不回显、留空保留;SMTP 测试按钮反映三态;前端闸门全绿。

M2-T09 — 数据可视化 UI(热力图为主的地图)

  • Status: todo · Depends: M2-T06(数据来自 T03
  • Context: 接管 Grafana 原职责,且首页主视图就是这张地图。优先级:① 热力图(最重要)② 时间范围选择器(必须)③ 散点点位(辅助,主要服务编辑/删除)。location:去过哪的密度;poo:狗最爱在哪拉。
  • Acceptance: 首页渲染热力图(location / poo);时间范围选择器生效、只取窗口内数据(不拉全量);散点层可切换、点选某点可进入编辑/删除(接 T10/T04);location 点多时聚合;响应式(手机浏览器可用);前端闸门全绿。

M2-T10 — 记录管理 UI(按需展示 + 增删改)

  • Status: todo · Depends: M2-T06CRUD 来自 T04
  • Acceptance: 列表分页展示 poo/location;可编辑、可删除单条并即时刷新;删除有二次确认;前端闸门全绿。

M2-T11 — FastAPI 托管 SPA + 移除 Jinja

  • Status: todo · 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 等路径返回 SPAindex.html),/api/* 不被 fallback 吞掉,/static/资源正常。
    • 旧 Jinja 模板与 pages 路由移除后 pytest 全绿。
    • 校验闸门全绿(含 OpenAPI 重导出)。
  • Reviewer: fallback 不拦截 /api/docs/openapi.json、静态资源;未登录访问 API 仍 401(不是被 SPA 壳吞掉)。

M2-T12 — 多阶段 Dockerfile + CI/compose

  • Status: todo · Depends: M2-T11
  • Files: modify Dockerfilenode build 阶段 → 拷 dist 进 python 镜像);modify .github/workflows/*(加前端 build/lint/typecheck);modify tests/test_deployment.py(镜像断言更新)
  • Acceptance:
    • 镜像构建成功且运行镜像不含 node 运行时。
    • CI 跑前端闸门 + 后端 pytest
    • 校验闸门全绿。

M2-T13 — 文档 + OpenAPI 收尾

  • Status: todo · Depends: M2-T12
  • Acceptance: README 增"前端 v2"段(开发/构建说明);architecture 退役"不前后端分离"约束;roadmap 勾选 M2openapi/ 已同步入库。

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 内完成。
  • 所有浏览器交互走 /api JSON 端点,session + CSRF 保护;ingestion 裸端点维持现状(留给 M3)。
  • Jinja templates/ 与 pages 路由移除;FastAPI 同源托管 SPA。
  • 多阶段镜像构建通过;CI 含前端闸门。
  • 后端 pytest/ruff/export_openapi + 前端 build/lint/typecheck/test 全绿。