From 09b0deab2a7d86aca88a7cbd83b891726285278f Mon Sep 17 00:00:00 2001 From: Gros Frumos Date: Fri, 20 Mar 2026 20:55:16 +0200 Subject: [PATCH] =?UTF-8?q?kin:=20BATON-ARCH-002=20=D0=9E=D1=82=D0=BA?= =?UTF-8?q?=D0=BB=D1=8E=D1=87=D0=B8=D1=82=D1=8C=20SignalAggregator=20?= =?UTF-8?q?=D0=B8=D0=B7=20v1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/adr/ADR-005-frontend-stack.md | 153 ++++++++++++++++ docs/adr/ADR-006-offline-ios-constraints.md | 191 ++++++++++++++++++++ 2 files changed, 344 insertions(+) create mode 100644 docs/adr/ADR-005-frontend-stack.md create mode 100644 docs/adr/ADR-006-offline-ios-constraints.md diff --git a/docs/adr/ADR-005-frontend-stack.md b/docs/adr/ADR-005-frontend-stack.md new file mode 100644 index 0000000..0734dd1 --- /dev/null +++ b/docs/adr/ADR-005-frontend-stack.md @@ -0,0 +1,153 @@ +# ADR-005: Выбор фронтенд-стека и i18n-стратегия + +**Дата:** 2026-03-20 +**Статус:** Accepted +**Автор:** Architect Agent (Kin pipeline, BATON-003) +**Решения:** #1026 + +--- + +## Контекст + +Фронтенд Baton — PWA с одной кнопкой SOS. Функционал: +- Показать кнопку по центру экрана +- При первом визите — форма ввода имени (регистрация) +- При нажатии — вызов `/api/signal` с UUID, timestamp, geo (опционально) +- Service Worker для cache-first +- Адаптация под iOS/Android/Desktop + +Пользователи из разных стран (300–400 человек) → вопрос о локализации. + +--- + +## Варианты фронтенд-стека + +### Вариант A: Vanilla JS (zero dependencies) + +**Плюсы:** +- Нет build step: файлы деплоятся как есть +- Минимальный размер: app.js ~2–3 KB, style.css ~1–2 KB +- Нет транспиляции, бандлинга, node_modules +- Время загрузки: < 50ms на 3G +- Полный контроль над SW lifecycle (нет абстракций) + +**Минусы:** +- Нет компонентной модели (не нужна для 1 экрана) +- Ручное управление DOM (getElementById, classList) +- Нет HMR при разработке + +### Вариант B: Preact (~3 KB gzip) + +**Плюсы:** +- React-совместимый API в 3 KB +- JSX + компоненты +- Hooks для state management + +**Минусы:** +- Требует build step (Vite/esbuild) +- node_modules для 1 кнопки и 1 формы — overkill +- Добавляет ~3 KB к bundle без пользы +- Усложняет SW интеграцию (build output vs source) + +### Вариант C: Vue 3 (используется в Kin) + +**Плюсы:** +- Знакомость команды +- Мощный template синтаксис +- Экосистема (router, Pinia) + +**Минусы:** +- ~33 KB gzip (runtime) +- Обязателен build step +- Vue Router, Pinia, SFC — всё это не нужно для 1 экрана +- Кратный overkill: framework для 1 кнопки + +--- + +## Решение (фронтенд) + +**Выбран Вариант A: Vanilla JS** + +--- + +## Обоснование (фронтенд) + +1. **Один экран, одна кнопка.** Компонентная модель не даёт преимуществ когда весь UI — это кнопка + форма имени + сообщение об ошибке. + +2. **Zero build step = zero complexity.** Файлы `index.html`, `app.js`, `style.css`, `sw.js`, `manifest.json` деплоятся напрямую. Нет Vite, нет esbuild, нет `npm run build`. + +3. **Минимальный bundle.** Для экстренного приложения скорость загрузки критична. Vanilla JS: ~5 KB total. Vue 3: ~40 KB минимум. На 3G разница: 50ms vs 400ms. + +4. **SW интеграция проще.** Service Worker precache список — конечный и известный заранее. С build step нужно интегрировать hashed filenames в SW. + +5. **Preact/Vue отклонены** — обоснованный overkill. Если в будущем UI усложнится (v2: история сигналов, настройки, чат) — миграция на Preact за 2 часа. + +--- + +## i18n-стратегия (#1026) + +### Решение: i18n НЕ НУЖНА в v1 + +### Обоснование + +**Анализ текстового контента UI:** + +| Элемент | Текст | Универсальность | +|---|---|---| +| Кнопка | "SOS" или "HELP" | SOS — международный сигнал, не требует перевода | +| Заголовок | "Baton" | Название продукта, не переводится | +| Форма регистрации | "Your name" + placeholder | 1 строка, английский понятен целевой аудитории | +| Ошибка сети | "No connection" | 1 строка | +| Ошибка сервера | "Try again" | 1 строка | + +**Итого: 4–5 строк текста**, из которых главная (SOS) универсальна. + +**Целевая аудитория:** 300–400 пользователей из разных стран, но это не массовый consumer-продукт. Пользователи знают что это за приложение и как им пользоваться (установлено по прямой рекомендации). + +### v2 (если потребуется) + +```javascript +// Минимальный i18n: JSON + navigator.language +const LANG = navigator.language.slice(0, 2); +const T = translations[LANG] || translations['en']; +``` + +Файл `translations.json`: +```json +{ + "en": { "name_placeholder": "Your name", "no_connection": "No connection", "try_again": "Try again" }, + "ru": { "name_placeholder": "Ваше имя", "no_connection": "Нет связи", "try_again": "Повторите" } +} +``` + +**Триггер перехода к i18n:** явный запрос от пользователей или расширение аудитории на non-English speaking массовый рынок. + +--- + +## Файловая структура фронтенда (v1) + +``` +frontend/ +├── index.html # App shell, meta tags, apple-touch-icon, manifest link +├── app.js # UUID auth, geolocation, fetch /api/signal, error handling +├── style.css # Центрированная кнопка, responsive, dark theme +├── sw.js # Cache-first precache, skipWaiting, clientsClaim +├── manifest.json # PWA metadata (name, icons, display:standalone) +├── icon-180.png # iOS apple-touch-icon +├── icon-192.png # Android manifest (required) +└── icon-512.png # Android splash + maskable +``` + +--- + +## Последствия + +1. **Нет package.json в проекте.** Фронтенд не зависит от npm. Backend уже использует pip (requirements.txt). + +2. **Деплой фронтенда = копирование файлов.** Nginx `root /path/to/frontend;` — готово. Нет CI/CD для сборки. + +3. **Стили — один CSS файл.** Нет SCSS, нет PostCSS, нет CSS-in-JS. Для 1 экрана это достаточно. + +4. **Тестирование фронтенда:** в v1 — ручное. Если потребуется автоматизация — Playwright (headless Chrome, без build step). + +5. **i18n решение задокументировано явно (#1026).** При запросе локализации — план миграции готов (JSON файл + 3 строки JS). diff --git a/docs/adr/ADR-006-offline-ios-constraints.md b/docs/adr/ADR-006-offline-ios-constraints.md new file mode 100644 index 0000000..62baed7 --- /dev/null +++ b/docs/adr/ADR-006-offline-ios-constraints.md @@ -0,0 +1,191 @@ +# ADR-006: Офлайн-устойчивость v1 и ограничения iOS + +**Дата:** 2026-03-20 +**Статус:** Accepted +**Автор:** Architect Agent (Kin pipeline, BATON-003) +**Решения:** #1016, #1019, #1024, #1025 + +--- + +## Контекст + +Два связанных вопроса: + +1. **Offline queue:** если пользователь нажал SOS без сети — что происходит? Нужна ли очередь с повторной отправкой? +2. **iOS ограничения:** PWA standalone недоступен в EU (iOS 17.4 DMA)? 7-дневная очистка кеша? Приватный режим? + +--- + +## Часть 1: Offline queue + +### Варианты + +#### Вариант A: Показать ошибку, нет retry (v1) + +``` +Нажатие → navigator.onLine === false → "Нет подключения. Проверьте сеть." +``` + +**Плюсы:** +- Ноль дополнительного кода +- Предсказуемое поведение: нет сети = нет сигнала +- Пользователь знает, что сигнал НЕ отправлен (нет ложной надежды) + +**Минусы:** +- Сигнал потерян, если пользователь не нажмёт повторно +- navigator.onLine ненадёжен (может вернуть true при captive portal) + +#### Вариант B: localStorage queue + online event + +``` +Нажатие → try fetch → fail → JSON.stringify в localStorage → online event → flush +``` + +**Плюсы:** +- Простая реализация (~20 строк) +- Сигнал не теряется + +**Минусы:** +- iOS Safari private mode: localStorage недоступен (#1015) +- localStorage недоступен в Service Worker контексте +- При закрытии вкладки до online event — сигнал не отправлен (но сохранён) + +#### Вариант C: IndexedDB + BackgroundSync (ADR-002 plan) + +**Плюсы:** +- Самое надёжное: IndexedDB доступен из SW, BackgroundSync работает даже при закрытой вкладке (Chromium) +- Idempotency key защищает от дубликатов + +**Минусы:** +- IndexedDB API громоздкий +- BackgroundSync: 78.75% покрытие (Safari/Firefox не поддерживают) +- Сложность x10 по сравнению с Вариантом A + +### Решение (offline) + +**Выбран Вариант A для v1 (#1019): показать ошибку, нет retry.** + +Переход на Вариант C (IndexedDB + BackgroundSync) запланирован для v2 (полная спека в ADR-002). + +### Обоснование (offline) + +1. **#1019: cache-first SW без offline queue достаточен для low-load PWA v1.** Приложение открывается мгновенно (кешированные статические файлы). Если сети нет — пользователь видит ошибку и повторяет попытку когда сеть вернётся. + +2. **Частота использования: ~1 раз/неделю.** Вероятность "нажал SOS без сети и забыл повторить" — крайне низкая. Экстренная ситуация мотивирует повторную попытку. + +3. **navigator.onLine проверка + UX:** + ```javascript + button.addEventListener('click', async () => { + if (!navigator.onLine) { + showError('Нет подключения. Проверьте сеть и попробуйте снова.'); + return; + } + try { + await sendSignal(); + showSuccess(); + } catch (e) { + showError('Ошибка отправки. Попробуйте снова.'); + } + }); + ``` + Даже если `navigator.onLine` ненадёжен — try/catch ловит ошибку fetch. + +4. **Вариант C (IndexedDB + BackgroundSync) — зарезервирован для v2.** ADR-002 содержит полную спецификацию. Переход потребует ~4 часа dev-работы. + +--- + +## Часть 2: iOS 17.4 EU DMA (#1016) + +### Хронология + +| Дата | Событие | +|---|---| +| Январь 2024 | Apple анонсировала удаление PWA standalone в EU (iOS 17.4 beta) | +| 1 марта 2024 | Apple отменила решение после массовой критики и EC inquiry | +| iOS 17.4 release | PWA standalone работает в EU без ограничений | +| Март 2026 (сейчас) | PWA standalone работает в EU на iOS 17.x, 18.x | + +### Текущий статус + +**PWA standalone режим РАБОТАЕТ в EU на всех актуальных версиях iOS.** + +### Что НЕ работает в EU на iOS + +| Функция | Статус в EU | Влияние на Baton | +|---|---|---| +| PWA standalone | ✅ Работает | Нет влияния | +| Push notifications | ❌ Не работает | Нет влияния (Baton не использует push в v1) | +| Background Sync | ❌ Не поддерживается Safari вообще | Нет влияния (v1 без offline queue) | + +### Workaround (на случай повторного удаления) + +Если Apple снова уберёт standalone в EU: + +1. **manifest.json:** `"display": "standalone"` → `"display": "browser"` + - Приложение откроется в Safari tab вместо fullscreen + - Функциональность сохраняется полностью + - UX деградирует: видна адресная строка + +2. **Telegram Web App (альтернатива):** + - Baton как Telegram Mini App + - Не зависит от PWA standalone + - Ограничение: работает только внутри Telegram + +3. **Нативная обёртка (крайний случай):** + - Capacitor/Cordova → IPA → TestFlight + - Для 300-400 пользователей — Enterprise distribution или Ad Hoc provisioning + - Overkill, только если Apple полностью заблокирует PWA + +**Рекомендация:** не предпринимать действий сейчас. Мониторить iOS release notes. + +--- + +## Часть 3: iOS 7-дневная очистка кеша + +### Проблема + +WebKit (Safari) очищает все website data (включая Service Worker registration, Cache API, localStorage, IndexedDB) после **7 дней непрерывного неиспользования** домена. + +### Влияние на Baton + +| Что теряется | Последствие | Серьёзность | +|---|---|---| +| SW registration | При следующем открытии — SW re-registers (нужна сеть) | Низкая | +| Cache API (precached assets) | Первое открытие после 7 дней — загрузка из сети | Низкая | +| localStorage (UUID) | Пользователь получает новый UUID → повторная регистрация | Средняя | + +### Митигация + +1. **Подсказка при установке:** "Открывайте приложение хотя бы раз в неделю для надёжной работы" +2. **UUID на сервере:** при потере localStorage UUID — пользователь регистрируется заново. Это неидеально, но приемлемо (знаем имя, не знаем что это тот же человек). +3. **v2 mitigation:** привязка Telegram user ID (через /start в боте) как persistent identifier, не зависящий от browser storage. + +--- + +## Часть 4: iOS Safari Private Mode (#1015) + +### Проблема + +`localStorage.setItem()` бросает `SecurityError` в приватном режиме iOS Safari. + +### Решение (реализовано в ADR-003) + +Цепочка fallback (#1025): `localStorage → sessionStorage → in-memory` + +Проверка доступности через реальную запись (#1024), не `typeof`. + +### Поведение в private mode + +- UUID генерируется в `sessionStorage` или `in-memory` +- UUID живёт до закрытия вкладки (sessionStorage) или до перезагрузки (in-memory) +- При следующем открытии — новый UUID, новая регистрация +- **Это ожидаемое поведение.** Пользователь в приватном режиме осознанно выбирает непостоянство данных. + +--- + +## Последствия + +1. **v1 offline = show error.** Минимальная сложность, максимальная прозрачность для пользователя. +2. **iOS DMA = no action needed.** Standalone работает. Workaround задокументирован. +3. **7-day cache = acceptable risk.** Для экстренного приложения пользователь скорее всего открывает его чаще 1 раза в неделю (хотя бы для проверки). +4. **Private mode = degraded but functional.** UUID временный, но SOS работает.