# Design ## Context Требование «Обложка по умолчанию» существовало и работало в старом инлайн-пути генерации вариантов (`processVariants`), который вызывал `SetCoverIfEmpty`. Рефакторинг media-objects (коммит `96268ee`) перенёс генерацию вариантов в фоновый worker и потерял этот вызов; сейчас `SetCoverIfEmpty` вызывается только из `CopyBatch`. Дополнительно обложка не снимается при уходе фото-обложки. См. `proposal.md — Why`. Опорные точки: - `internal/photos/service.go`: `Upload`, `Delete`, `MoveBatch`; `SetCoverIfEmpty` — метод репозитория фото (`store.go`, `repository.go`, `sqlite_store.go`). - `internal/photos/worker.go`: с недавних пор обрабатывает новые фото первыми (`created_at DESC`) — отсюда решение не ставить обложку в worker'е. - `albums.Update` уже поддерживает `cover_photo_id` и `clear_cover`. - Frontend: `albums.setCover()` уже существует; контекстное меню — `openMenuForPhoto` в `albums/[id]/+page.svelte`; секция «Действия» — `PhotoPanel.svelte` (props: `photo`, `role`). ## Goals / Non-Goals **Goals:** - Обложкой нового альбома становится первое загруженное фото. - Обложка снимается, когда фото-обложка покидает альбом (удаление/перенос). - Пункт «Установить как обложку альбома» доступен из сетки и правой панели. **Non-Goals:** - Backfill существующих альбомов. - Автоперевыбор новой обложки после снятия. - Изменение поведения Viewer и share-страниц. ## Decisions ### 1. Обложка ставится при загрузке, а не в worker'е **Выбор:** в `Service.Upload`, после успешного сохранения фото, вызвать `store.SetCoverIfEmpty(ctx, in.AlbumID, p.ID)`. **Почему:** формулировка «первое загруженное фото». Worker обрабатывает новейшие фото первыми, поэтому установка «при обработке» дала бы обложку = последнее загруженное. Загрузка идёт последовательно по файлам запроса, поэтому первое фото детерминированно выигрывает (`UPDATE ... WHERE cover_photo_id IS NULL`). **Альтернативы:** - *В worker'е по готовности вариантов* — thumb уже есть, но порядок обработки ломает «первое загруженное». - *В worker'е с выбором MIN(created_at)* — сложнее и не гарантирует готовность thumb выбранного фото. **Следствие:** `cover_thumb_url` пуст до генерации thumb; список альбомов подхватит его при следующем запросе (JOIN по `cover_photo_id`). Реальное время смены обложки в рейле не обновляется по SSE — вне области. ### 2. Снятие обложки — атомарный метод репозитория **Выбор:** метод `ClearCoverIfMatches(ctx, albumID, photoID)`: `UPDATE albums SET cover_photo_id = NULL WHERE id = $1 AND cover_photo_id = $2`. Вызывается в `Delete` (корзина) и в `MoveBatch` для исходного альбома. **Почему:** условный UPDATE защищает от гонки (обложку могли сменить) и не требует чтения. `PermanentDelete` уже покрыт FK `ON DELETE SET NULL`. **Альтернативы:** - *Снять безусловно* — риск снести чужой выбор обложки. - *FK при soft-delete* — FK не срабатывает при мягком удалении. ### 3. `clear_cover` → NULL **Выбор:** явный сброс обложки оставляет `cover_photo_id = NULL` (пересчёта по умолчанию нет). Соответствует решению «очищать, без backfill». **Альтернативы:** - *Пересчитать «по умолчанию» (первое фото)* — тогда сброс бессмыслен (обложка мгновенно вернётся), плюс скрытый backfill. ### 4. Frontend: та же защита прав, что и у редактирования альбома **Выбор:** пункт показывается при `role === 'owner' || role === 'admin'`; действие вызывает `albums.setCover(albumId, photo.id)` (существующий `PATCH /albums/:id`). Добавляется в `openMenuForPhoto` и в секцию «Действия» `PhotoPanel`; иконка `wallpaper`. **Почему:** переиспользуется существующий API и гейт прав; `PhotoPanel` уже получает `role`. `albumId` берётся из `photo.album_id`/контекста страницы. ## Risks / Trade-offs - [Пункт меню/панели меняет обложку, но рейл не обновляется мгновенно, если thumb ещё не готов] → допустимо; при следующем запросе обложка появится. - [Гонка параллельных загрузок в пустой альбом] → условный UPDATE выбирает одного победителя; «первое» при строго параллельных запросах незначительно недетерминировано. - [Снятие обложки при переносе может неожиданно обнулить обложку] → это и есть желаемое поведение по спеке; пользователь может назначить заново. - [Существующие альбомы без обложки остаются пустыми] → осознанно (no backfill). ## Migration Plan - Миграций нет. Порядок: backend (загрузка/снятие обложки) → frontend (меню/панель). - Откат — возврат предыдущего бинаря/фронта; данные не деградируют.