251 lines
17 KiB
Markdown
251 lines
17 KiB
Markdown
# M2 — 前端 v2(React SPA)
|
||
|
||
> 阅读前提:先读 [`README.md`](./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_name` cookie),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. 已锁定决策(讨论后拍板)
|
||
|
||
> 以下为与项目所有者讨论后**已定**的选择。**线框图本里程碑不画**——按本节 + 各任务卡描述,由实现侧自行合理排版(含移动端布局)。
|
||
|
||
**技术选型**
|
||
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. 任务依赖图
|
||
|
||
```
|
||
后端 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。
|
||
- [ ] 强制改密语义与现有一致。
|
||
- [ ] 校验闸门全绿。
|
||
- **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`/`DELETE` location(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**: `todo` · **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**: `todo` · **Depends**: M2-T12
|
||
- **Acceptance**: README 增"前端 v2"段(开发/构建说明);architecture 退役"不前后端分离"约束;roadmap 勾选 M2;`openapi/` 已同步入库。
|
||
|
||
---
|
||
|
||
## 8. 前端校验闸门(前端任务每次结束都要全绿)
|
||
|
||
在 `frontend/` 下:
|
||
```bash
|
||
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` 全绿。
|