baton/ARCHITECTURE.md

270 lines
15 KiB
Markdown
Raw Permalink Normal View History

2026-03-20 20:59:17 +02:00
# Baton — Архитектура системы
**Версия:** 1.0
**Дата:** 2026-03-20
**Статус:** Approved (Architect, BATON-003)
---
## Обзор
Baton — PWA экстренного сигнала. Одна кнопка → HTTPS POST → FastAPI → Telegram-группа.
**Аудитория:** 300400 пользователей из разных стран.
**Нагрузка:** ~1 сигнал/неделю (реалистичный пик: 510 одновременных).
---
## Компоненты
```
┌─────────────────────────────────────────────────────────────────┐
│ 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 |
2026-03-20 21:12:43 +02:00
| Service Worker | Cache-first precache | ADR-007, ADR-006 |
2026-03-20 20:59:17 +02:00
| 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 |
2026-03-20 21:12:43 +02:00
| Offline | Show error v1, IndexedDB v2 | ADR-006, ADR-007 |
2026-03-20 20:59:17 +02:00
---
## База данных
### Схема (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-файлы хранятся в `docs/adr/`.
2026-03-20 20:59:17 +02:00
| 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 (v1): IndexedDB+BackgroundSync | Accepted |
| [ADR-007](docs/adr/ADR-007-offline-queue-v2.md) | Offline pattern: IndexedDB+BackgroundSync (v2) | Accepted |
2026-03-20 20:59:17 +02:00
| [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 |