kin: BATON-003 [Research] Architect
This commit is contained in:
parent
09b0deab2a
commit
8ecaeeafc6
1 changed files with 266 additions and 0 deletions
266
ARCHITECTURE.md
Normal file
266
ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,266 @@
|
||||||
|
# Baton — Архитектура системы
|
||||||
|
|
||||||
|
**Версия:** 1.0
|
||||||
|
**Дата:** 2026-03-20
|
||||||
|
**Статус:** Approved (Architect, BATON-003)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Обзор
|
||||||
|
|
||||||
|
Baton — PWA экстренного сигнала. Одна кнопка → HTTPS POST → FastAPI → Telegram-группа.
|
||||||
|
|
||||||
|
**Аудитория:** 300–400 пользователей из разных стран.
|
||||||
|
**Нагрузка:** ~1 сигнал/неделю (реалистичный пик: 5–10 одновременных).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Компоненты
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ PWA (Браузер) │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │
|
||||||
|
│ │index.html│ │ app.js │ │ style.css│ │ manifest.json │ │
|
||||||
|
│ │app shell │ │ логика │ │ стили │ │ PWA metadata │ │
|
||||||
|
│ └──────────┘ └────┬─────┘ └──────────┘ └───────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌──────────┐ │ UUID auth (localStorage fallback chain) │
|
||||||
|
│ │ sw.js │ │ Geolocation (optional) │
|
||||||
|
│ │cache-1st │ │ fetch('/api/signal') │
|
||||||
|
│ │ precache │ │ │
|
||||||
|
│ └──────────┘ │ │
|
||||||
|
└─────────────────────┼────────────────────────────────────────────┘
|
||||||
|
│ HTTPS POST
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Nginx (TLS termination) │
|
||||||
|
│ │
|
||||||
|
│ - Reverse proxy → uvicorn :8000 │
|
||||||
|
│ - Serves frontend/ static files │
|
||||||
|
│ - Let's Encrypt TLS certificate │
|
||||||
|
│ - HTTPS ports: 443 (обязателен для PWA + Telegram webhook) │
|
||||||
|
└─────────────────────┬───────────────────────────────────────────┘
|
||||||
|
│ HTTP (internal)
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Backend (FastAPI / uvicorn) │
|
||||||
|
│ │
|
||||||
|
│ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │
|
||||||
|
│ │ main.py │ │ db.py │ │ telegram.py │ │
|
||||||
|
│ │ 3 routes │ │ SQLite WAL │ │ sendMessage │ │
|
||||||
|
│ │ lifespan │ │ CRUD ops │ │ setWebhook │ │
|
||||||
|
│ │ CORS │ │ 3 tables │ │ (aggregator) │ │
|
||||||
|
│ └────────────┘ └────────────┘ └──────┬───────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌────────────┐ ┌──────────────┐ │ │
|
||||||
|
│ │ models.py │ │middleware.py │ │ │
|
||||||
|
│ │ Pydantic │ │ webhook sec │ │ │
|
||||||
|
│ │ schemas │ │ validation │ │ │
|
||||||
|
│ └────────────┘ └──────────────┘ │ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌────────────┐ │ │
|
||||||
|
│ │ config.py │ │ │
|
||||||
|
│ │ env vars │ │ │
|
||||||
|
│ └────────────┘ │ │
|
||||||
|
└─────────────────────────────────────────┼────────────────────────┘
|
||||||
|
│ │
|
||||||
|
│ SQLite │ HTTPS POST
|
||||||
|
▼ ▼
|
||||||
|
┌────────────┐ ┌──────────────────────┐
|
||||||
|
│ baton.db │ │ Telegram Bot API │
|
||||||
|
│ SQLite │ │ api.telegram.org │
|
||||||
|
│ WAL mode │ │ │
|
||||||
|
└────────────┘ │ → Группа оповещения │
|
||||||
|
└──────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Потоки данных
|
||||||
|
|
||||||
|
### 1. Первый визит (регистрация)
|
||||||
|
|
||||||
|
```
|
||||||
|
Браузер Backend SQLite
|
||||||
|
│ │ │
|
||||||
|
│ открыл PWA │ │
|
||||||
|
│ ├── SW registers │ │
|
||||||
|
│ ├── precache assets │ │
|
||||||
|
│ ├── crypto.randomUUID() │ │
|
||||||
|
│ ├── localStorage.setItem() │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ показать форму "Your name" │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ POST /api/register ──────────>│ │
|
||||||
|
│ {uuid, name} │ INSERT OR IGNORE ───────>│
|
||||||
|
│ │ users (uuid, name) │
|
||||||
|
│ <──────── 200 {user_id, uuid} │ │
|
||||||
|
│ │ │
|
||||||
|
│ сохранить registered=true │ │
|
||||||
|
│ в localStorage │ │
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Экстренный сигнал (основной поток)
|
||||||
|
|
||||||
|
```
|
||||||
|
Браузер Backend Telegram
|
||||||
|
│ │ │
|
||||||
|
│ нажатие SOS │ │
|
||||||
|
│ ├── navigator.onLine? │ │
|
||||||
|
│ │ нет → showError() │ │
|
||||||
|
│ │ да ↓ │ │
|
||||||
|
│ ├── getCurrentPosition() │ │
|
||||||
|
│ │ (async, optional) │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ POST /api/signal ────────────>│ │
|
||||||
|
│ {user_id, timestamp, geo?} │ │
|
||||||
|
│ │ Pydantic validate │
|
||||||
|
│ │ save_signal() → SQLite│
|
||||||
|
│ │ get_user_name() │
|
||||||
|
│ │ │
|
||||||
|
│ │ POST sendMessage ────>│
|
||||||
|
│ │ "🚨 SOS от Алиса │
|
||||||
|
│ │ 📍 55.75, 37.61" │
|
||||||
|
│ │ │
|
||||||
|
│ <──────── 200 {ok, signal_id} │ <──── 200 OK │
|
||||||
|
│ │ │
|
||||||
|
│ showSuccess() │ │
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Telegram webhook (входящий, для /start)
|
||||||
|
|
||||||
|
```
|
||||||
|
Telegram Backend SQLite
|
||||||
|
│ │ │
|
||||||
|
│ /start command ──────────────>│ │
|
||||||
|
│ X-Telegram-Bot-Api-Secret: │ │
|
||||||
|
│ WEBHOOK_SECRET │ │
|
||||||
|
│ │ verify_webhook_secret() │
|
||||||
|
│ │ ├── 403 if mismatch │
|
||||||
|
│ │ ├── parse /start │
|
||||||
|
│ │ └── register_user() ────>│
|
||||||
|
│ │ │
|
||||||
|
│ <──────── 200 {"ok": true} │ │
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Стек технологий
|
||||||
|
|
||||||
|
| Слой | Технология | ADR |
|
||||||
|
|------|-----------|-----|
|
||||||
|
| Frontend | Vanilla JS (zero deps) | ADR-005 |
|
||||||
|
| Service Worker | Cache-first precache | ADR-002, ADR-006 |
|
||||||
|
| Auth | UUID v4 + localStorage fallback | ADR-003 |
|
||||||
|
| Backend | FastAPI (Python 3.11+) | ADR-001 |
|
||||||
|
| Database | SQLite WAL + aiosqlite | ADR-001 |
|
||||||
|
| Telegram | Direct sendMessage (v1) | ADR-004 |
|
||||||
|
| TLS | Nginx + Let's Encrypt | — |
|
||||||
|
| i18n | English-only v1, deferred | ADR-005 |
|
||||||
|
| Offline | Show error v1, IndexedDB v2 | ADR-006, ADR-002 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## База данных
|
||||||
|
|
||||||
|
### Схема (3 таблицы)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
PRAGMA journal_mode=WAL;
|
||||||
|
PRAGMA busy_timeout=5000;
|
||||||
|
PRAGMA synchronous=NORMAL;
|
||||||
|
|
||||||
|
users (id PK, uuid UNIQUE, name, created_at)
|
||||||
|
signals (id PK, user_uuid FK→users.uuid, timestamp, lat, lon, accuracy, created_at, telegram_batch_id)
|
||||||
|
telegram_batches (id PK, message_text, sent_at, signals_count, status)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Индексы
|
||||||
|
|
||||||
|
- `idx_users_uuid` (UNIQUE) — поиск по UUID при каждом сигнале
|
||||||
|
- `idx_signals_user_uuid` — история сигналов пользователя
|
||||||
|
- `idx_signals_created_at` — хронологическая выборка
|
||||||
|
- `idx_batches_status` — поиск неотправленных batch (v2)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
| Метод | Путь | Назначение | Auth |
|
||||||
|
|-------|------|------------|------|
|
||||||
|
| POST | /api/register | Регистрация UUID→name | Нет (idempotent) |
|
||||||
|
| POST | /api/signal | Экстренный сигнал | UUID в body |
|
||||||
|
| POST | /api/webhook/telegram | Входящие от Telegram | Secret token header |
|
||||||
|
|
||||||
|
**CORS:** только POST с `FRONTEND_ORIGIN`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Безопасность
|
||||||
|
|
||||||
|
| Аспект | Реализация | Решение |
|
||||||
|
|--------|-----------|---------|
|
||||||
|
| HTTPS | Nginx + Let's Encrypt, обязателен для PWA + Telegram | #1011 |
|
||||||
|
| Webhook validation | X-Telegram-Bot-Api-Secret-Token → 403 | #1010 |
|
||||||
|
| CORS | Strict: 1 origin, POST only | — |
|
||||||
|
| Input validation | Pydantic v2, auto 422 | — |
|
||||||
|
| Secrets | .env + python-dotenv, не логируются | — |
|
||||||
|
| Storage probe | Реальная запись, не typeof | #1024 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Деплой
|
||||||
|
|
||||||
|
```
|
||||||
|
VPS (один сервер)
|
||||||
|
├── Nginx (port 443)
|
||||||
|
│ ├── TLS termination (Let's Encrypt)
|
||||||
|
│ ├── location / → frontend/ static files
|
||||||
|
│ └── location /api/ → proxy_pass http://127.0.0.1:8000
|
||||||
|
├── uvicorn (port 8000, 1 worker)
|
||||||
|
│ └── FastAPI app
|
||||||
|
├── baton.db (SQLite, local disk)
|
||||||
|
└── .env (secrets)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Один uvicorn worker** — достаточен для нагрузки и упрощает in-memory состояние (если агрегатор включён в v2).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Граница v1 / v2
|
||||||
|
|
||||||
|
| Фича | v1 | v2 |
|
||||||
|
|------|----|----|
|
||||||
|
| Cache-first SW | ✅ | — |
|
||||||
|
| Offline queue (IndexedDB + BackgroundSync) | ❌ | ✅ |
|
||||||
|
| Прямой sendMessage | ✅ | — |
|
||||||
|
| Агрегатор сигналов | ❌ (код готов) | ✅ (активация) |
|
||||||
|
| i18n | ❌ (English-only) | ✅ (JSON + navigator.language) |
|
||||||
|
| Push notifications | ❌ | ✅ (Android only, iOS EU ❌) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Известные ограничения
|
||||||
|
|
||||||
|
| Ограничение | Влияние | Митигация |
|
||||||
|
|---|---|---|
|
||||||
|
| iOS 7-day cache expiry | SW + localStorage очищены → повторная регистрация | Открывать приложение хотя бы раз в неделю |
|
||||||
|
| iOS Safari private mode | localStorage недоступен → временный UUID | Fallback chain: sessionStorage → memory (#1025) |
|
||||||
|
| navigator.onLine ненадёжен | Может вернуть true при captive portal | try/catch fetch с UI ошибкой |
|
||||||
|
| Один uvicorn worker | Не масштабируется горизонтально | Достаточно для 400 пользователей; v2: gunicorn + Redis |
|
||||||
|
| Telegram 20 msg/min в группу | При массовом событии — throttling | v2: агрегатор (код готов в telegram.py) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ссылки на ADR
|
||||||
|
|
||||||
|
| ADR | Тема | Статус |
|
||||||
|
|-----|------|--------|
|
||||||
|
| [ADR-001](docs/adr/ADR-001-backend-stack.md) | Backend stack: FastAPI | Accepted |
|
||||||
|
| [ADR-002](docs/adr/ADR-002-offline-pattern.md) | Offline pattern: IndexedDB+BackgroundSync (v2) | Accepted |
|
||||||
|
| [ADR-003](docs/adr/ADR-003-auth-pattern.md) | Auth: UUID v4 + localStorage fallback | Accepted |
|
||||||
|
| [ADR-004](docs/adr/ADR-004-telegram-strategy.md) | Telegram: direct sendMessage (v1) | Accepted |
|
||||||
|
| [ADR-005](docs/adr/ADR-005-frontend-stack.md) | Frontend: Vanilla JS + i18n strategy | Accepted |
|
||||||
|
| [ADR-006](docs/adr/ADR-006-offline-ios-constraints.md) | Offline resilience + iOS DMA/constraints | Accepted |
|
||||||
Loading…
Add table
Add a link
Reference in a new issue