From 8ecaeeafc613950c2f03445d7c124108259c478c Mon Sep 17 00:00:00 2001 From: Gros Frumos Date: Fri, 20 Mar 2026 20:59:17 +0200 Subject: [PATCH] kin: BATON-003 [Research] Architect --- ARCHITECTURE.md | 266 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 ARCHITECTURE.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..bb6a34e --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,266 @@ +# 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 таблицы) + +```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 | Тема | Статус | +|-----|------|--------| +| [ADR-001](docs/adr/ADR-001-backend-stack.md) | Backend stack: FastAPI | Accepted | +| [ADR-002](docs/adr/ADR-002-offline-pattern.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 |