feat(android): статус синхронизации на карточке и безопасная разгрузка

Карточка альбома устройства получает третью строку: метку режима («Копии»/
«Перенос») и ход загрузки. Процент считается как (uploaded + running) / total
для обоих синхронизируемых режимов, пульсирует только при активной загрузке,
показывает очередь, ожидание Wi-Fi и ошибку. В режиме переноса карточка
показывает и карту памяти, и облако — режимы различает метка, а не значки.

Разгрузка оригиналов теперь требует подтверждённой серверной копии в текущем
целевом альбоме: устаревший serverPhotoId перепроверяется по альбому, смена
цели переносит уже загруженные фото и отклоняется, если перенести нельзя.
Потеря серверной копии видна на карточке и на экране альбома, автозатирания
превью нет.

Изменения OpenSpec: android-album-card-sync-status, safe-album-offload.
This commit is contained in:
iu
2026-10-16 18:36:51 +03:00
parent 98aaa3156a
commit 1eb1177273
36 changed files with 1855 additions and 81 deletions
@@ -0,0 +1,61 @@
# Proposal
## Why
Реальный инцидент: альбом устройства U2 синхронизировался в режиме переноса в серверный
альбом A (фото загружены, локальные оригиналы заменены превью), затем альбом A удалили на
сервере, а альбом устройства перепривязали к новому серверному альбому U2. В U2 уехало всего
1 фото: остальные на телефоне уже были превью, и загрузить их заново было нечем. Оригиналы
остались только в корзине сервера, а приложение молча оставило телефон с превью.
Три причины:
- решение о разгрузке принимается по `serverPhotoId` в локальном индексе, без проверки, что
это фото вообще есть в **текущем** целевом альбоме;
- смена целевого альбома только перезаписывает связь: уже загруженные фото никто не переносит
на сервере, а из превью повторно не загрузить;
- потеря серверной копии — тихий тупик: `ORPHANED` виден, но нигде не сказано, что оригинал
утрачен, и нет явного предупреждения.
## What Changes
- **Доверие только к живой копии.** Записанный `serverPhotoId` считается действительным,
только если это фото присутствует в целевом альбоме. Если копии нет: при локальном
оригинале ассет загружается заново, при одном превью элемент помечается потерявшим копию.
- **Разгрузка только под защитой.** Замена оригинала превью выполняется, только если копия
подтверждена в целевом альбоме; разгрузка «по памяти индекса» запрещена. Автоматическая
разгрузка в режиме переноса сохраняется.
- **Смена цели переносит копии.** При смене целевого альбома приложение сначала переносит на
сервере уже загруженные этим альбомом устройства фото в новый альбом и переключает связь
только если перенос удался; иначе связь не меняется и пользователь получает отказ.
- **Потеря копии видна.** Экран альбома устройства и карточка альбома в списке показывают,
сколько фото осталось без серверной копии; никакого автоматического удаления превью.
- **BREAKING**: нет. Меняются только защитные условия; успешные сценарии (загрузка,
разгрузка, повторная синхронизация) сохраняют поведение.
## Capabilities
### New Capabilities
_Нет — поведение относится к существующим способностям._
### Modified Capabilities
- `phone-album-sync`: разгрузка требует живой копии в текущем целевом альбоме; записанный
`serverPhotoId` перепроверяется по целевому альбому; смена целевого альбома переносит уже
загруженные фото и отклоняется, если перенести нельзя.
- `android-client`: потеря серверной копии показывается на экране альбома устройства и на
карточке в списке; автоматического удаления превью нет.
## Impact
- **Код (Android):** `data/sync/SyncPlan` (доверие к живой копии), `data/sync/SyncCoordinator`
(защищённая разгрузка, перенос при смене цели), `data/sync/OffloadManager` (условие разгрузки),
`data/upload/UploadWorker` (передача живой копии после загрузки), `data/db/dao/SyncDao` и
`data/repo/SyncRepository` (счёт потерянных копий), `ui/devicealbum/DeviceAlbumViewModel`
(смена цели, разгрузка, предупреждение), `ui/devicealbum/DeviceAlbumScreen` (баннер),
`domain/sync/AlbumSyncProgress` и `ui/albums/*` (сигнал на карточке).
- **Схема Room:** без изменений; добавляются только запросы.
- **API и бэкенд:** не затрагиваются (используется существующий `/api/photo-batch/move`).
- **Веб-клиент:** не затрагивается.
- **Не затрагивается:** ручные загрузки в серверный альбом; восстановление уже потерянных
копий из корзины сервера (по решению пользователя это отдельная задача).