kin: BATON-002 [Research] UX Designer
This commit is contained in:
commit
057e500d5f
29 changed files with 3530 additions and 0 deletions
97
docs/adr/ADR-001-backend-stack.md
Normal file
97
docs/adr/ADR-001-backend-stack.md
Normal 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`
|
||||
123
docs/adr/ADR-002-offline-pattern.md
Normal file
123
docs/adr/ADR-002-offline-pattern.md
Normal 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 — не «опциональный», а обязательный элемент архитектуры.
|
||||
35
docs/adr/ADR-003-auth-pattern.md
Normal file
35
docs/adr/ADR-003-auth-pattern.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# ADR-003: Паттерн аутентификации пользователей
|
||||
|
||||
**Дата:** 2026-03-20
|
||||
**Статус:** Stub (подлежит заполнению)
|
||||
**Автор:** —
|
||||
|
||||
---
|
||||
|
||||
## Контекст
|
||||
|
||||
_Описание контекста — предстоит заполнить._
|
||||
|
||||
---
|
||||
|
||||
## Варианты
|
||||
|
||||
_Описание вариантов — предстоит заполнить._
|
||||
|
||||
---
|
||||
|
||||
## Решение
|
||||
|
||||
_Решение — предстоит заполнить._
|
||||
|
||||
---
|
||||
|
||||
## Обоснование
|
||||
|
||||
_Обоснование — предстоит заполнить._
|
||||
|
||||
---
|
||||
|
||||
## Последствия
|
||||
|
||||
_Последствия — предстоит заполнить._
|
||||
35
docs/adr/ADR-004-telegram-strategy.md
Normal file
35
docs/adr/ADR-004-telegram-strategy.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# ADR-004: Стратегия отправки в Telegram (прямой vs агрегатор)
|
||||
|
||||
**Дата:** 2026-03-20
|
||||
**Статус:** Stub (подлежит заполнению)
|
||||
**Автор:** —
|
||||
|
||||
---
|
||||
|
||||
## Контекст
|
||||
|
||||
_Описание контекста — предстоит заполнить._
|
||||
|
||||
---
|
||||
|
||||
## Варианты
|
||||
|
||||
_Описание вариантов — предстоит заполнить._
|
||||
|
||||
---
|
||||
|
||||
## Решение
|
||||
|
||||
_Решение — предстоит заполнить._
|
||||
|
||||
---
|
||||
|
||||
## Обоснование
|
||||
|
||||
_Обоснование — предстоит заполнить._
|
||||
|
||||
---
|
||||
|
||||
## Последствия
|
||||
|
||||
_Последствия — предстоит заполнить._
|
||||
Loading…
Add table
Add a link
Reference in a new issue