kin: BATON-002 [Research] UX Designer

This commit is contained in:
Gros Frumos 2026-03-20 20:44:00 +02:00
commit 057e500d5f
29 changed files with 3530 additions and 0 deletions

View file

@ -0,0 +1,97 @@
# ADR-001: Выбор бэкенд-стека
**Дата:** 2026-03-20
**Статус:** Accepted
**Автор:** Architect Agent (Kin pipeline, BATON-001)
---
## Контекст
Baton — минималистичное PWA приложение экстренного сигнала. Бэкенд выполняет три задачи:
1. Принять POST /signal от 300-400 пользователей (возможны одновременные запросы)
2. Сохранить сигнал в SQLite
3. Отправить уведомление в Telegram-группу через Bot API
Проект требует простого деплоя на один VPS, без kubernetes, без сложной инфраструктуры.
Команда имеет опыт работы с Python/FastAPI (используется в проекте Kin).
Рассматривались три стека: FastAPI (Python), Express/Fastify (Node.js), Go (net/http).
---
## Варианты
### Вариант A: FastAPI (Python 3.11+)
**Плюсы:**
- Знакомость команды (используется в Kin)
- asyncio нативно
- Pydantic — автоматическая валидация входных данных
- `aiosqlite` или `sqlite3` через `run_in_executor`
- Быстрый старт разработки
**Минусы:**
- В 2-3x медленнее Go по RPS
- Docker образ ~200-400 MB (Python runtime + зависимости)
- bcrypt блокирует event loop — нужен `run_in_executor` (решение #1004)
### Вариант B: Express/Fastify (Node.js 20+)
**Плюсы:**
- Единый язык с фронтендом (vanilla JS)
- `better-sqlite3` — синхронный, самый быстрый SQLite биндинг для Node.js
- Fastify ~24% быстрее FastAPI по RPS в независимых тестах
- Docker образ ~200-300 MB
**Минусы:**
- Нет опыта работы в команде
- `better-sqlite3` синхронный — блокирует event loop при долгих запросах (на практике приемлемо для simple INSERT)
- Дополнительное переключение контекста (JS для фронта, JS для бека)
### Вариант C: Go (net/http)
**Плюсы:**
- Компилируется в единый статический бинарь ~8-15 MB (простейший деплой)
- В 2-3x быстрее Python, ~2x быстрее Node.js
- Горутины — нативный concurrency без event loop ограничений
- Нет проблем с bcrypt (не блокирует горутины)
- Cross-compile: `GOARCH=amd64 GOOS=linux go build`
**Минусы:**
- Нет опыта работы в команде
- Более длительный онбординг
- `modernc.org/sqlite` (CGO-free): 10-100% медленнее нативного SQLite — компромисс для кросс-компиляции
---
## Решение
**Выбран Вариант A: FastAPI (Python 3.11+)**
---
## Обоснование
1. **Знакомость команды — главный фактор для минимального проекта.** FastAPI используется в Kin. Нет времени на онбординг в Go или Node.js для задачи, которая требует ~200 строк бэкенд-кода.
2. **Производительности FastAPI достаточно.** При нагрузке 300-400 одновременных запросов бутылочное горлышко — Telegram rate limit (20 сообщений/минуту в группу), а не скорость Python. SQLite WAL + `busy_timeout=5000` справится с 400 одновременными INSERT за ~400 мс (решения #1002, #1005).
3. **Pydantic даёт бесплатную валидацию** входных данных (user_id, timestamp, geo) без дополнительного кода.
4. **Размер деплоя приемлем.** ~300 MB Docker образ — не проблема для одного VPS сервиса.
5. **Вариант B отклонён:** нет опыта у команды, преимущество в скорости (+24%) несущественно при текущей нагрузке.
6. **Вариант C отклонён:** несмотря на превосходную производительность и минимальный деплой, отсутствие опыта в команде создаёт риск для проекта без аргументированной причины переходить на Go.
---
## Последствия
- Использовать `aiosqlite` для async SQLite операций (или `sqlite3` через `run_in_executor`)
- bcrypt (если понадобится в будущем) — только через `run_in_executor` (решение #1004)
- SQLite WAL обязателен: `busy_timeout=5000`, `synchronous=NORMAL` — вместе (решение #1005)
- Агрегатор Telegram: реализовать в Python как background task (asyncio) или через простой in-memory буфер с `asyncio.sleep`
- requirements.txt: `fastapi`, `uvicorn[standard]`, `aiosqlite`, `httpx` (для Telegram API)
- Переменные окружения: `BOT_TOKEN`, `CHAT_ID`, `DB_PATH` — читать из `.env` через `python-dotenv`

View file

@ -0,0 +1,123 @@
# ADR-002: Паттерн офлайн-очереди
**Дата:** 2026-03-20
**Статус:** Accepted
**Автор:** Architect Agent (Kin pipeline, BATON-001)
---
## Контекст
Baton — приложение экстренного сигнала. Критичное требование: сигнал **не должен быть потерян**, если пользователь нажал кнопку в момент отсутствия сети (тоннель, слабый сигнал, офлайн).
Сигнал должен быть сохранён локально и доставлен на сервер, как только соединение восстановится.
Аудитория: 300-400 пользователей, разные браузеры и платформы (Android Chrome, iOS Safari, Desktop Firefox/Chrome).
---
## Варианты
### Вариант A: IndexedDB outbox + BackgroundSync + online event fallback
**Схема:**
1. Кнопка нажата → немедленная попытка `fetch('/signal')`
2. Ошибка или offline → запись в IndexedDB outbox
3. Trigger 1: `window.addEventListener('online', flushOutbox)` — main thread, все браузеры
4. Trigger 2: SW регистрирует `registration.sync.register('flush-outbox')` — Chromium только
5. SW обрабатывает `sync` event → читает IndexedDB → flush
**Плюсы:**
- IndexedDB персистентна: не очищается при закрытии вкладки (в отличие от memory)
- IndexedDB доступна как из main thread, так и из Service Worker → общее хранилище
- BackgroundSync: браузер сам управляет повтором (Chrome может сработать даже при закрытой вкладке)
- Dual trigger страхует: если BackgroundSync не сработал → online event
- Квота IndexedDB: обычно GB (не 5 MB как localStorage)
- Соответствует принятому решению #1006
**Минусы:**
- IndexedDB API громоздкий → нужна обёртка (`idb` библиотека, ~1.9 KB gzip) или написать самому
- BackgroundSync поддерживается только 78.75% браузеров (caniuse, март 2026) — Safari и Firefox не поддерживают
- Сложнее отлаживать в DevTools, чем localStorage
### Вариант B: localStorage queue + online event listener
**Схема:**
1. Кнопка нажата → попытка отправки
2. Ошибка → `JSON.stringify` очереди в `localStorage`
3. `window.addEventListener('online', flush)` → отправить всё из очереди
**Плюсы:**
- Самый простой вариант (~20 строк)
- Нет зависимостей
- Синхронный API — легко читать и писать
**Минусы:**
- iOS Safari приватный режим: `localStorage.setItem()` бросает `SecurityError` → нужен try/catch → если упал, сигнал теряется
- localStorage недоступна в Service Worker контексте → нельзя flush из SW
- Лимит: 5 MB (достаточно, но IndexedDB надёжнее)
- Нет BackgroundSync — только один trigger (online event)
### Вариант C: Cache API + Request replay в Service Worker
**Схема:**
- SW перехватывает failed POST запросы → сохраняет в Cache API
- При появлении сети → повторяет запросы из кэша
**Плюсы:**
- Нативная интеграция с SW fetch event
- Нет отдельного хранилища
**Минусы:**
- Cache API спроектирован для Response (кэш ответов), не для Request replay
- Нет гарантий персистентности очереди (Cache может быть очищен браузером без предупреждения)
- Workbox Background Sync внутри использует IndexedDB, не Cache API (косвенное свидетельство)
- Нестандартное использование API → неожиданное поведение в edge cases
---
## Решение
**Выбран Вариант A: IndexedDB outbox + BackgroundSync + online event fallback**
Соответствует зафиксированному решению #1006.
---
## Обоснование
1. **IndexedDB — единственный вариант, доступный и в main thread, и в Service Worker.** Это критично: flush может произойти как из app.js (online event), так и из sw.js (BackgroundSync event). Общее хранилище исключает дублирование кода.
2. **Dual trigger — страховочная сеть.** BackgroundSync не обязателен для работы (Safari/Firefox — 21% юзеров обойдутся без него), но является бонусом для Chrome пользователей: flush случится даже при закрытой вкладке.
3. **Вариант B отклонён** из-за проблемы iOS Safari приватного режима (решение #1003: не понижать явные требования) и недоступности в SW контексте. При том что приложение экстренного сигнала должно работать без потерь на iOS.
4. **Вариант C отклонён** как злоупотребление API не по назначению. Cache API не гарантирует персистентность POST запросов.
5. **Размер зависимости `idb`:** ~1.9 KB gzip — приемлемо. Альтернатива: написать минимальную обёртку (~30 строк) для трёх операций (add, getAll, delete).
---
## Последствия
**При реализации учесть:**
1. **iOS Safari приватный режим:** `localStorage` недоступен → переход на IndexedDB не помогает (IndexedDB тоже может быть ограничен). Нужен graceful degradation: попытка записи в IndexedDB → при ошибке сигнал отправляется только online или теряется с явным UI-предупреждением.
2. **Idempotency ключ:** `id: "${Date.now()}-${Math.random().toString(36).slice(2)}"` — уникальный ключ каждой записи в outbox. Защита от дубликатов при повторных попытках. Бэкенд должен игнорировать дубликаты (INSERT OR IGNORE по `client_id`).
3. **Лимит попыток:** `attempts` в outbox entry. После 3-5 неудачных попыток — показать пользователю UI-предупреждение. Не flush бесконечно.
4. **SW lifecycle:** при обновлении SW (новая версия) старый SW активен до закрытия всех вкладок. Flush в процессе обновления → запрос может быть потерян. Idempotency ключ и `INSERT OR IGNORE` на бэкенде защищают от дубликатов.
5. **Background Sync — проверка перед регистрацией:**
```javascript
if ('serviceWorker' in navigator && 'SyncManager' in window) {
const reg = await navigator.serviceWorker.ready;
await reg.sync.register('flush-outbox');
} else {
if (navigator.onLine) flushOutbox(); // немедленный fallback
}
```
6. **Решение #1001 требует обновления:** фактический охват Background Sync — 78.75% (21% без поддержки), не 85% как было зафиксировано. Ручной fallback — не «опциональный», а обязательный элемент архитектуры.

View file

@ -0,0 +1,35 @@
# ADR-003: Паттерн аутентификации пользователей
**Дата:** 2026-03-20
**Статус:** Stub (подлежит заполнению)
**Автор:** —
---
## Контекст
_Описание контекста — предстоит заполнить._
---
## Варианты
_Описание вариантов — предстоит заполнить._
---
## Решение
_Решение — предстоит заполнить._
---
## Обоснование
_Обоснование — предстоит заполнить._
---
## Последствия
оследствия — предстоит заполнить._

View file

@ -0,0 +1,35 @@
# ADR-004: Стратегия отправки в Telegram (прямой vs агрегатор)
**Дата:** 2026-03-20
**Статус:** Stub (подлежит заполнению)
**Автор:** —
---
## Контекст
_Описание контекста — предстоит заполнить._
---
## Варианты
_Описание вариантов — предстоит заполнить._
---
## Решение
_Решение — предстоит заполнить._
---
## Обоснование
_Обоснование — предстоит заполнить._
---
## Последствия
оследствия — предстоит заполнить._