15 KiB
15 KiB
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 таблицы)
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 | Backend stack: FastAPI | Accepted |
| ADR-002 | Offline pattern: IndexedDB+BackgroundSync (v2) | Accepted |
| ADR-003 | Auth: UUID v4 + localStorage fallback | Accepted |
| ADR-004 | Telegram: direct sendMessage (v1) | Accepted |
| ADR-005 | Frontend: Vanilla JS + i18n strategy | Accepted |
| ADR-006 | Offline resilience + iOS DMA/constraints | Accepted |