# 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-007 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-007). ### Обоснование (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-007 содержит полную спецификацию. Переход потребует ~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 работает.