chore(openspec): archive completed changes and sync main specs
- fix-large-album-load -> photos, realtime, media-objects - fix-album-cover -> photos, albums
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
# 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 (меню/панель).
|
||||
- Откат — возврат предыдущего бинаря/фронта; данные не деградируют.
|
||||
Reference in New Issue
Block a user