- fix-large-album-load -> photos, realtime, media-objects - fix-album-cover -> photos, albums
6.7 KiB
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 (меню/панель).
- Откат — возврат предыдущего бинаря/фронта; данные не деградируют.