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

15 KiB
Raw Blame History

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 таблицы)

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 Backend stack: FastAPI Accepted
ADR-002 Offline pattern (v1): IndexedDB+BackgroundSync Accepted
ADR-007 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