M2-T13: docs wrap-up + retire frontend constraints + dependency cleanup
- 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
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
当前系统已经包含:
|
当前系统已经包含:
|
||||||
|
|
||||||
- FastAPI Web 应用与服务端模板页面
|
- FastAPI Web 应用(React SPA 前端 + JSON API)
|
||||||
- SQLite + SQLAlchemy + Alembic 的单库结构
|
- SQLite + SQLAlchemy + Alembic 的单库结构
|
||||||
- username/password + server-side session 鉴权
|
- username/password + server-side session 鉴权
|
||||||
- runtime config 页面与 app DB 持久化
|
- runtime config 页面与 app DB 持久化
|
||||||
@@ -47,11 +47,13 @@ python -m scripts.run_migrations
|
|||||||
|
|
||||||
主要目录如下:
|
主要目录如下:
|
||||||
|
|
||||||
- `app/`: FastAPI 应用代码
|
- `app/`: FastAPI 应用代码(包含 JSON API、业务服务、数据模型)
|
||||||
|
- `frontend/`: React SPA 前端(Vite + React + TypeScript + Mantine)
|
||||||
- `alembic_app/`: App DB 的 Alembic migration 环境(同时管理 `location` / `poo_records` 表)
|
- `alembic_app/`: App DB 的 Alembic migration 环境(同时管理 `location` / `poo_records` 表)
|
||||||
- `tests/`: pytest 测试
|
- `tests/`: pytest 测试
|
||||||
- `docs/`: 当前系统说明文档
|
- `docs/`: 当前系统说明文档
|
||||||
- `scripts/`: 辅助脚本,例如 OpenAPI 导出
|
- `scripts/`: 辅助脚本,例如 OpenAPI 导出
|
||||||
|
- `openapi/`: OpenAPI schema 静态产物(`openapi.json` / `openapi.yaml`),纳入版本控制
|
||||||
|
|
||||||
## 依赖管理
|
## 依赖管理
|
||||||
|
|
||||||
@@ -112,11 +114,62 @@ uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
|||||||
|
|
||||||
启动后可访问:
|
启动后可访问:
|
||||||
|
|
||||||
- 应用首页:`http://localhost:8000/`
|
- 应用首页(React SPA):`http://localhost:8000/`
|
||||||
- 健康检查:`http://localhost:8000/status`
|
- 健康检查:`http://localhost:8000/status`
|
||||||
- Swagger UI:`http://localhost:8000/docs`
|
- Swagger UI:`http://localhost:8000/docs`
|
||||||
- ReDoc:`http://localhost:8000/redoc`
|
- ReDoc:`http://localhost:8000/redoc`
|
||||||
|
|
||||||
|
## 前端 v2(React SPA)
|
||||||
|
|
||||||
|
M2 用 React SPA 取代了原有 Jinja 服务端模板,由 FastAPI 同源托管(同一容器、同一 origin)。
|
||||||
|
|
||||||
|
### 技术栈
|
||||||
|
|
||||||
|
- **Vite + React + TypeScript + Mantine**(组件库)
|
||||||
|
- **TanStack Query**(数据请求/缓存)
|
||||||
|
- **Leaflet / react-leaflet**(地图与热力图)
|
||||||
|
- **openapi-typescript + openapi-fetch**(类型化 API client,由 `openapi/openapi.json` 生成)
|
||||||
|
|
||||||
|
### 本地开发(前端)
|
||||||
|
|
||||||
|
前端开发服务器会把 `/api`、`/location`、`/poo`、`/public-ip`、`/homeassistant`、`/ticktick`、`/status` 等路径代理到后端 FastAPI(`:8000`)。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run dev # 启动 Vite dev server(默认 :5173),代理后端
|
||||||
|
```
|
||||||
|
|
||||||
|
### 构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run build # 产出 frontend/dist
|
||||||
|
```
|
||||||
|
|
||||||
|
FastAPI 启动时若 `frontend/dist/index.html` 存在,则自动挂载该目录,并对非 `/api` 路径做 SPA fallback(返回 `index.html`)。该路径可通过环境变量 `SPA_DIST_DIR` 覆盖(默认值为 `frontend/dist`,与多阶段 Dockerfile 中 `COPY` 到 `/app/frontend/dist` 一致)。
|
||||||
|
|
||||||
|
### 类型化 API Client
|
||||||
|
|
||||||
|
前端 API client 由后端 OpenAPI schema 自动生成:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run codegen # 从 ../openapi/openapi.json 生成 src/api/schema.d.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
生成物(`src/api/schema.d.ts`)已提交入库,CI 会校验它与 `openapi/openapi.json` 保持同步。
|
||||||
|
|
||||||
|
### 前端校验闸门
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run lint # ESLint
|
||||||
|
npm run typecheck # TypeScript 类型检查
|
||||||
|
npm run test # Vitest 单元测试
|
||||||
|
npm run build # 构建,确认产出 dist
|
||||||
|
```
|
||||||
|
|
||||||
## 数据库与 Alembic
|
## 数据库与 Alembic
|
||||||
|
|
||||||
当前使用单一 SQLite 数据库文件:
|
当前使用单一 SQLite 数据库文件:
|
||||||
@@ -142,9 +195,9 @@ python -m scripts.migrate_legacy_data
|
|||||||
|
|
||||||
- 认证模型:`username/password`
|
- 认证模型:`username/password`
|
||||||
- 会话模型:server-side session + cookie
|
- 会话模型:server-side session + cookie
|
||||||
- 当前主要受保护页面:`/config`
|
- 当前受保护入口:React SPA(`/` 等客户端路由)调用 `/api/*` JSON 端点
|
||||||
- 当前公开页面:`/login`
|
- 当前公开页面:`/login`(SPA 登录页)
|
||||||
- 当前公开 API:现有业务 API 暂未在这一轮统一收口到 auth 下
|
- 当前公开 API:裸 ingestion 端点(`/location/record`、`/poo/record` 等设备调用端点)暂未收口到 session 保护(M3 再做)
|
||||||
|
|
||||||
安全实现的当前边界:
|
安全实现的当前边界:
|
||||||
|
|
||||||
@@ -152,7 +205,7 @@ python -m scripts.migrate_legacy_data
|
|||||||
- session cookie 使用 `HttpOnly`
|
- session cookie 使用 `HttpOnly`
|
||||||
- `Secure` 默认随 `APP_ENV` 切换:非 development 时默认开启
|
- `Secure` 默认随 `APP_ENV` 切换:非 development 时默认开启
|
||||||
- `SameSite=Lax`
|
- `SameSite=Lax`
|
||||||
- 登录表单和登出表单都有基础 CSRF 防护
|
- 写请求(POST/PUT/PATCH/DELETE)需携带 `X-CSRF-Token` header(SameSite=Lax + 自定义 header 纵深防御,无需 per-session token 值比对)
|
||||||
|
|
||||||
首次启动时,如果 `APP_DATABASE_URL` 对应的 auth DB 里还没有用户,应用会使用:
|
首次启动时,如果 `APP_DATABASE_URL` 对应的 auth DB 里还没有用户,应用会使用:
|
||||||
|
|
||||||
@@ -166,12 +219,14 @@ python -m scripts.migrate_legacy_data
|
|||||||
|
|
||||||
首次登录后会被要求立即修改密码。这个 bootstrap 只用于首个用户落库,不是后续的完整配置管理方案。
|
首次登录后会被要求立即修改密码。这个 bootstrap 只用于首个用户落库,不是后续的完整配置管理方案。
|
||||||
|
|
||||||
当前前端主要有两条页面路径:
|
React SPA 主要页面路由(客户端路由,均由 FastAPI fallback 到 `index.html`):
|
||||||
|
|
||||||
- `/login`
|
- `/login`:登录页
|
||||||
- `/config`
|
- `/`:首页(地图热力图主视图)
|
||||||
|
- `/config`:配置页(取代原 Jinja `/config`)
|
||||||
|
- `/records`:记录管理列表页
|
||||||
|
|
||||||
无论是本地 `host:port` 还是反向代理后的域名访问,登录成功后都使用相对路径跳转到 `/config`。
|
无论是本地 `host:port` 还是反向代理后的域名访问,登录成功后进入 SPA 首页(`/`)。
|
||||||
|
|
||||||
## Config 持久化
|
## Config 持久化
|
||||||
|
|
||||||
@@ -230,8 +285,8 @@ python -m scripts.migrate_legacy_data
|
|||||||
|
|
||||||
当前系统已经提供最小可用的 SMTP 能力:
|
当前系统已经提供最小可用的 SMTP 能力:
|
||||||
|
|
||||||
- SMTP 配置可在 `/config` 页面填写并保存到 `app_config`
|
- SMTP 配置可在 React SPA `/config` 页面填写并保存到 `app_config`(通过 `PUT /api/config`)
|
||||||
- 可通过 config 页面发送测试邮件
|
- 可通过 config 页面发送测试邮件(`POST /api/config/smtp/test`)
|
||||||
- 邮件 `From` 头支持显示名,例如 `Home Automation <sender@example.com>`
|
- 邮件 `From` 头支持显示名,例如 `Home Automation <sender@example.com>`
|
||||||
|
|
||||||
当前 SMTP 配置项包括:
|
当前 SMTP 配置项包括:
|
||||||
|
|||||||
@@ -53,14 +53,10 @@ idna==3.11
|
|||||||
# httpx
|
# httpx
|
||||||
iniconfig==2.3.0
|
iniconfig==2.3.0
|
||||||
# via pytest
|
# via pytest
|
||||||
jinja2==3.1.6
|
|
||||||
# via -r requirements.in
|
|
||||||
mako==1.3.11
|
mako==1.3.11
|
||||||
# via alembic
|
# via alembic
|
||||||
markupsafe==3.0.3
|
markupsafe==3.0.3
|
||||||
# via
|
# via mako
|
||||||
# jinja2
|
|
||||||
# mako
|
|
||||||
packaging==26.1
|
packaging==26.1
|
||||||
# via
|
# via
|
||||||
# build
|
# build
|
||||||
|
|||||||
@@ -29,10 +29,8 @@
|
|||||||
- 通用依赖注入
|
- 通用依赖注入
|
||||||
- `api/`
|
- `api/`
|
||||||
- HTTP routes
|
- HTTP routes
|
||||||
- 当前已迁入 `/login`、`/logout`、`/admin`
|
- `api/routes/api/`:JSON API(`/api/*` 前缀),供 React SPA 调用:会话/鉴权、配置读写、数据查询、记录 CRUD
|
||||||
- 当前已迁入 `GET /public-ip/check`
|
- 裸 ingestion 端点:`GET /public-ip/check`、`POST /homeassistant/publish`、`POST /poo/record`、`GET /poo/latest`、TickTick OAuth 等
|
||||||
- 当前已迁入 `POST /homeassistant/publish` 第一版入口
|
|
||||||
- 当前已迁入 `POST /poo/record` 与 `GET /poo/latest`
|
|
||||||
- `models/`
|
- `models/`
|
||||||
- SQLAlchemy models
|
- SQLAlchemy models
|
||||||
- 所有模型(auth / config / public_ip / location / poo)共用同一个 `Base`,均落在单一 `app.db` 中
|
- 所有模型(auth / config / public_ip / location / poo)共用同一个 `Base`,均落在单一 `app.db` 中
|
||||||
@@ -46,8 +44,6 @@
|
|||||||
- `integrations/`
|
- `integrations/`
|
||||||
- 外部系统适配层
|
- 外部系统适配层
|
||||||
- 当前已迁入 Home Assistant outbound adapter
|
- 当前已迁入 Home Assistant outbound adapter
|
||||||
- `templates/`
|
|
||||||
- Jinja2 模板
|
|
||||||
- `static/`
|
- `static/`
|
||||||
- 极简静态资源
|
- 极简静态资源
|
||||||
|
|
||||||
@@ -63,15 +59,26 @@ pytest 测试目录。后续可以在这里自然扩展:
|
|||||||
- mock tests
|
- mock tests
|
||||||
- integration tests
|
- integration tests
|
||||||
|
|
||||||
|
### `frontend/`
|
||||||
|
|
||||||
|
React SPA 前端(M2 引入)。Vite + React + TypeScript + Mantine,由 FastAPI 同源托管。
|
||||||
|
|
||||||
|
- `src/`:React 源码
|
||||||
|
- `src/api/`:由 `openapi/openapi.json` 生成的类型化 client(`schema.d.ts`)+ fetch 封装
|
||||||
|
- `dist/`:`npm run build` 产物,由 FastAPI 的 `SPA_DIST_DIR` 挂载并对非 `/api` 路径做 fallback
|
||||||
|
|
||||||
### `scripts/`
|
### `scripts/`
|
||||||
|
|
||||||
辅助脚本目录。当前包含 OpenAPI 导出脚本。
|
辅助脚本目录。当前包含 OpenAPI 导出脚本(`export_openapi.py`)与数据层辅助脚本。
|
||||||
|
|
||||||
|
### `openapi/`
|
||||||
|
|
||||||
|
OpenAPI schema 静态产物(`openapi.json` / `openapi.yaml`),由 `python scripts/export_openapi.py` 生成,纳入版本控制。前端 codegen 以此为契约源。
|
||||||
|
|
||||||
## 当前约束
|
## 当前约束
|
||||||
|
|
||||||
- 当前只搭骨架,不迁业务逻辑
|
|
||||||
- 当前数据库继续使用 SQLite
|
- 当前数据库继续使用 SQLite
|
||||||
- 当前不引入前后端分离
|
- ~~当前不引入前后端分离~~ **已退役(M2)**:现为 React SPA + JSON `/api` 层,由 FastAPI 同源托管
|
||||||
- 当前不设计 Notion 模块
|
- 当前不设计 Notion 模块
|
||||||
- 当前通知能力仍保持极小范围,不引入独立通知中心或多渠道抽象
|
- 当前通知能力仍保持极小范围,不引入独立通知中心或多渠道抽象
|
||||||
|
|
||||||
|
|||||||
@@ -222,7 +222,7 @@
|
|||||||
- [ ] 校验闸门全绿。
|
- [ ] 校验闸门全绿。
|
||||||
|
|
||||||
### M2-T13 — 文档 + OpenAPI 收尾
|
### M2-T13 — 文档 + OpenAPI 收尾
|
||||||
- **Status**: `todo` · **Depends**: M2-T12
|
- **Status**: `done` · **Depends**: M2-T12
|
||||||
- **Acceptance**: README 增"前端 v2"段(开发/构建说明);architecture 退役"不前后端分离"约束;roadmap 勾选 M2;`openapi/` 已同步入库。
|
- **Acceptance**: README 增"前端 v2"段(开发/构建说明);architecture 退役"不前后端分离"约束;roadmap 勾选 M2;`openapi/` 已同步入库。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+5
-3
@@ -35,7 +35,7 @@
|
|||||||
| 里程碑 | 主题 | 一句话 |
|
| 里程碑 | 主题 | 一句话 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| **M1** ✅ | 单库化地基 | 把三库合并成单一 `app.db`,清理散落数据层,删掉 Grafana |
|
| **M1** ✅ | 单库化地基 | 把三库合并成单一 `app.db`,清理散落数据层,删掉 Grafana |
|
||||||
| **M2** | 前端 v2 | React SPA 取代 Jinja,承载 config + 可视化 + 记录增删改 |
|
| **M2** ✅ | 前端 v2 | React SPA 取代 Jinja,承载 config + 可视化 + 记录增删改 |
|
||||||
| **M3** | 开放与移动端(远期试水) | token 鉴权 + React Native 移动端 |
|
| **M3** | 开放与移动端(远期试水) | token 鉴权 + React Native 移动端 |
|
||||||
|
|
||||||
排序原则:**先清地基,再在干净结构上盖楼。** M2 的新 API 和 React 必须建立在合并后的单库之上,否则就是在准备推倒的旧数据层上盖新楼、之后回头返工。
|
排序原则:**先清地基,再在干净结构上盖楼。** M2 的新 API 和 React 必须建立在合并后的单库之上,否则就是在准备推倒的旧数据层上盖新楼、之后回头返工。
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## M2 — 前端 v2(React SPA)
|
## M2 — 前端 v2(React SPA)✅ 已完成
|
||||||
|
|
||||||
### 目标
|
### 目标
|
||||||
|
|
||||||
@@ -125,9 +125,11 @@
|
|||||||
|
|
||||||
### 鉴权边界(与 M3 衔接)
|
### 鉴权边界(与 M3 衔接)
|
||||||
|
|
||||||
- 现在那个“裸 API 记小狗日志”的 ingestion 端点(设备 / 脚本调用,非浏览器)**维持现状到 M3**。
|
- 现在那个”裸 API 记小狗日志”的 ingestion 端点(设备 / 脚本调用,非浏览器)**维持现状到 M3**。
|
||||||
- M2 新增的、浏览器调用的 CRUD 端点,用 session 保护即可,本步不引入 token。
|
- M2 新增的、浏览器调用的 CRUD 端点,用 session 保护即可,本步不引入 token。
|
||||||
|
|
||||||
|
> **M2 已完成**(M2-T01 至 M2-T13 全部 done)。Jinja 模板已移除,React SPA 同源托管,多阶段 Docker 构建通过,所有校验闸门绿。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## M3 — 开放与移动端(远期试水)
|
## M3 — 开放与移动端(远期试水)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ apscheduler>=3.10,<4.0
|
|||||||
argon2-cffi>=25.1,<26.0
|
argon2-cffi>=25.1,<26.0
|
||||||
fastapi>=0.115,<0.116
|
fastapi>=0.115,<0.116
|
||||||
httpx>=0.28,<1.0
|
httpx>=0.28,<1.0
|
||||||
jinja2>=3.1,<4.0
|
|
||||||
pydantic-settings>=2.6,<3.0
|
pydantic-settings>=2.6,<3.0
|
||||||
python-multipart>=0.0.12,<1.0
|
python-multipart>=0.0.12,<1.0
|
||||||
pyyaml>=6.0,<7.0
|
pyyaml>=6.0,<7.0
|
||||||
|
|||||||
+1
-5
@@ -45,14 +45,10 @@ idna==3.11
|
|||||||
# via
|
# via
|
||||||
# anyio
|
# anyio
|
||||||
# httpx
|
# httpx
|
||||||
jinja2==3.1.6
|
|
||||||
# via -r requirements.in
|
|
||||||
mako==1.3.11
|
mako==1.3.11
|
||||||
# via alembic
|
# via alembic
|
||||||
markupsafe==3.0.3
|
markupsafe==3.0.3
|
||||||
# via
|
# via mako
|
||||||
# jinja2
|
|
||||||
# mako
|
|
||||||
pycparser==2.23
|
pycparser==2.23
|
||||||
# via cffi
|
# via cffi
|
||||||
pydantic==2.13.2
|
pydantic==2.13.2
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"""Tests for M2-T03: GET /api/locations, GET /api/poo, GET /api/public-ip."""
|
"""Tests for M2-T03: GET /api/locations, GET /api/poo, GET /api/public-ip."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
@@ -18,12 +17,6 @@ from app.models.public_ip import PublicIPHistory, PublicIPState
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def _extract_csrf_token(html: str) -> str:
|
|
||||||
match = re.search(r'name="csrf_token" value="([^"]+)"', html)
|
|
||||||
assert match is not None, "csrf_token not found in HTML"
|
|
||||||
return match.group(1)
|
|
||||||
|
|
||||||
|
|
||||||
def _api_login(client: TestClient) -> None:
|
def _api_login(client: TestClient) -> None:
|
||||||
"""Log in via POST /api/auth/login so the TestClient has a session cookie."""
|
"""Log in via POST /api/auth/login so the TestClient has a session cookie."""
|
||||||
resp = client.post(
|
resp = client.post(
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
"""Jinja-based auth tests removed in M2-T11 (Jinja routes deleted).
|
|
||||||
|
|
||||||
Equivalent JSON-API coverage lives in test_api_session.py and test_api_config.py.
|
|
||||||
This file is intentionally left with no test functions so pytest does not error.
|
|
||||||
"""
|
|
||||||
Reference in New Issue
Block a user