191 lines
9.3 KiB
Markdown
191 lines
9.3 KiB
Markdown
# 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 работает.
|