# 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-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 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/`. | 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 |