docs(m2): lock M2 frontend design decisions
pytest / test (push) Failing after 11m46s

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.
This commit is contained in:
2026-06-12 22:40:57 +02:00
parent c1a5d7a425
commit 66ec9979cc
+31 -13
View File
@@ -27,15 +27,16 @@
### 3.2 鉴权:复用 session cookie + SPA 版 CSRF ### 3.2 鉴权:复用 session cookie + SPA 版 CSRF
- 继续用现有 **HttpOnly session cookie**(同源自动携带),M2 **不引入 token**token 属 M3)。 - 继续用现有 **HttpOnly session cookie**(同源自动携带),M2 **不引入 token**token 属 M3)。
- CSRF:新增 `GET /api/session` 返回当前用户 + 该会话的 `csrf_token`SPA 在所有写请求(POST/PUT/PATCH/DELETE)放 `X-CSRF-Token` header,后端校验其与 session 内 `csrf_token` 一致。等价于把现有表单 CSRF 平移到 header - 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**。 - 浏览器面向的所有新端点一律 session 保护;**裸 ingestion 端点(设备调用的 `POST /location/record``POST /poo/record`)维持现状到 M3**。
### 3.3 前端工程 ### 3.3 前端工程
- `frontend/`**Vite + React + TypeScript**。 - `frontend/`**Vite + React + TypeScript**。
- API client:由后端 `openapi/openapi.json` **自动生成** TS 类型与请求函数(如 `openapi-typescript` + 轻量 fetch 封装,或同类工具)。生成物入库或在 build 时生成(见 T06 决策)。 - 组件库:**Mantine**(已定;批电池齐、TS 优先、视觉中性,最贴近此前 Vue 侧 Naive UI 的用法)。
- 可视化:地图 + 热力图(location 轨迹 / poo 点位)。建议 **MapLibre GL 或 Leaflet + heatmap 插件**(最终选型见 §5 决策) - API client:由后端 `openapi/openapi.json` **自动生成** TS 类型与请求函数(如 `openapi-typescript` + 轻量 fetch 封装)。**生成物入库** + `npm run codegen` + CI 校验"生成物与 openapi 同步"(已定)。fetch 封装统一带 cookie、写请求注入自定义 CSRF header、401 跳登录
- 状态/数据请求:轻量即可(如 TanStack Query),不引入重型框架 - 可视化:**Leaflet**(已定)—— `react-leaflet` + `leaflet.heat`(热力图,**头号功能**+ `leaflet.markercluster`(点多时聚合)+ OSM 栅格瓦片(零 key)。**地图封在一个自包含组件后面**(如 `<RecordsMap points mode onSelect>`,全应用只此处 import leaflet),数据获取/时间窗 state 在外面;这样将来若要换 **MapLibre GL** 是被隔离的局部重写,不波及其它
- 状态/数据请求:轻量即可(**TanStack Query**,已定),不引入重型框架。
### 3.4 构建与部署 ### 3.4 构建与部署
@@ -65,14 +66,31 @@
> 记录 CRUD 依赖现有 PK 作行标识(location PK=`person+datetime`poo PK=`timestamp`)。路径参数需对 `datetime`/`timestamp` 做 URL 编码处理。 > 记录 CRUD 依赖现有 PK 作行标识(location PK=`person+datetime`poo PK=`timestamp`)。路径参数需对 `datetime`/`timestamp` 做 URL 编码处理。
## 5. 需先拍板的决策(Orchestrator 在派 T06 前确认 ## 5. 已锁定决策(讨论后拍板
1. **地图/热力图库**MapLibre GL(矢量、现代)vs Leaflet(简单、生态大)。推荐 Leaflet + `leaflet.heat`(试水门槛低)。 > 以下为与项目所有者讨论后**已定**的选择。**线框图本里程碑不画**——按本节 + 各任务卡描述,由实现侧自行合理排版(含移动端布局)。
2. **OpenAPI client 生成物**:入库(确定性、便于 review)vs build 时生成(仓库干净)。推荐**入库**,并加一个 `npm run codegen` + CI 校验"生成物与 openapi 同步"。
3. **CSRF 落地**header `X-CSRF-Token` + `GET /api/session` 下发(推荐)vs 双提交 cookie。
4. **是否保留少量 Jinja**:建议 SPA 对齐后**全量移除** `templates/`,只留 SPA。
> 这些可用 1 个轻量"决策任务"或直接由 Orchestrator 在本节记录选择,再开 T06。 **技术选型**
1. **组件库 = Mantine**。批电池齐、TS 优先、视觉中性、文档好,最贴近此前 Naive UI 的用法,利于 agent 产出一致 UI。
2. **地图库 = Leaflet**`react-leaflet` + `leaflet.heat` + `leaflet.markercluster`,OSM 栅格、零 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. **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. 任务依赖图 ## 6. 任务依赖图
@@ -177,10 +195,10 @@
- **Status**: `todo` · **Depends**: M2-T06 - **Status**: `todo` · **Depends**: M2-T06
- **Acceptance**: 能读/存所有现有配置 section;secret 不回显、留空保留;SMTP 测试按钮反映三态;前端闸门全绿。 - **Acceptance**: 能读/存所有现有配置 section;secret 不回显、留空保留;SMTP 测试按钮反映三态;前端闸门全绿。
### M2-T09 — 数据可视化 UI地图 + 热力图) ### M2-T09 — 数据可视化 UI(热力图为主的地图)
- **Status**: `todo` · **Depends**: M2-T06(数据来自 T03 - **Status**: `todo` · **Depends**: M2-T06(数据来自 T03
- **Context**: 接管 Grafana 原职责location 轨迹/热力图、poo 点位 - **Context**: 接管 Grafana 原职责,且**首页主视图就是这张地图**。优先级:**① 热力图(最重要)② 时间范围选择器(必须)③ 散点点位(辅助,主要服务编辑/删除)**。location:去过哪的密度;poo:狗最爱在哪拉
- **Acceptance**: 地图渲染 location/poo 点;热力图层可切换;时间范围筛选生效;前端闸门全绿。 - **Acceptance**: 首页渲染热力图(location / poo);**时间范围选择器生效、只取窗口内数据**(不拉全量);散点层可切换、点选某点可进入编辑/删除(接 T10/T04);location 点多时聚合;响应式(手机浏览器可用);前端闸门全绿。
### M2-T10 — 记录管理 UI(按需展示 + 增删改) ### M2-T10 — 记录管理 UI(按需展示 + 增删改)
- **Status**: `todo` · **Depends**: M2-T06CRUD 来自 T04 - **Status**: `todo` · **Depends**: M2-T06CRUD 来自 T04