baton/docs/adr/ADR-006-offline-ios-constraints.md
2026-03-20 21:12:43 +02:00

191 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 работает.