baton/ARCHITECTURE.md
Gros Frumos 2ee953866b kin: BATON-ARCH-014 Доработать ADR-002 и ADR-004 по замечаниям ревью
- Создан docs/adr/ADR-002-offline-pattern.md (Accepted, дата 2026-03-20)
  с секцией Open Questions: #1001, охват 78.75%, ACTION:/конвенция #1049
- ADR-004: добавлен "exponential backoff согласно решению #1046" к строке 429/retry_after
- ARCHITECTURE.md: добавлена вводная фраза "ADR-файлы хранятся в docs/adr/"
  и строка таблицы для ADR-002 (Accepted)
- tests/test_arch_004.py: удалены 4 теста на отсутствие ADR-002,
  устаревшие после создания нового ADR-002 (BATON-ARCH-014 supersedes)
- tests/test_arch_014.py: 14 новых тестов для критериев приёмки
- Все 216 тестов: passed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 22:05:04 +02:00

269 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 |
| Service Worker | Cache-first precache | ADR-007, 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-007 |
---
## База данных
### Схема (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 FKusers.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/`.
| 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 |
| [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 |