Files
ds-galo/openspec/changes/album-ui-covers/design.md
T
2026-10-10 14:21:43 +03:00

103 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Design
## Context
См. `proposal.md` для мотивации. Текущее состояние, важное для подхода:
- Авторизация — Bearer-токен (не cookies), поэтому `<img>` не может обращаться к
защищённым эндпоинтам напрямую. Все фото уже отдаются фронтенду как presigned
URL (`internal/photos/service.go`, `attachURLs`).
- `albums.cover_photo_id` — колонка `text` без внешнего ключа (миграция
`000001_init.up.sql:15`), поэтому удаление фото-обложки оставляет висячий id.
- `albums.Service` уже держит `*storage.Client` и умеет presign; отдельной
зависимости от пакета `photos` нет.
- Сборка фото идёт асинхронно (`processVariants`): `thumb_key` появляется не
сразу после создания записи фото.
- В `backend/static` закоммичен собранный `Icon.svelte` (inline-SVG спрайт
Material Symbols), при этом в `frontend/src` компонента нет — UI сейчас на
эмодзи. Берём inline-SVG как ориентир.
## Goals / Non-Goals
**Goals:**
- Список альбомов отдаёт `cover_url` (presigned миниатюра) и карточки его рисуют.
- Выбор и очистка обложки из фото альбома в настройках, применяется немедленно.
- Автоматическая обложка не затирает ручной выбор.
- Кнопка создания — иконка с хинтом, открывающая диалог ввода имени.
**Non-Goals:**
- Обложка в шапке страницы альбома и в публичных share-страницах.
- Смена обложки из лайтбокса/контекстного меню фото (может быть позже).
- Переработка всей иконочной системы проекта — вводим минимальный inline-SVG.
## Decisions
### 1. `cover_url` в ответе списка (вариант A)
`GET /albums` отдаёт каждому альбому `cover_url` — presigned URL миниатюры
(`thumb`) или `null`. Источник ключа: `albums.Store` расширяет выборку
подзапросом к `photos` за `thumb_key` по `cover_photo_id`; `albums.Service`
делает presign через уже имеющийся `storage.Client`.
**Альтернативы:** (B) фронт дёргает `GET /photos/:id` на каждую обложку — N+1 и
тянет лишние поля; (C) эндпоинт `GET /albums/:id/cover` с редиректом — не
работает, `<img>` не приложит Bearer-токен. Оба отпадают.
### 2. Presign в `albums`, а не в `photos`
Не вводим зависимость `albums -> photos`; достаточно подзапроса за ключом и
существующего `storage` в `albums.Service`. Дублирование кода presign
минимально и оправдано сохранением слоёв.
### 3. Авто-обложка только при пустой
`photos.Store.SetCover` меняет SQL на условный:
`UPDATE albums SET cover_photo_id = $2 WHERE id = $1 AND cover_photo_id IS NULL`.
Вызов остаётся в `processVariants` после `SetVariants`, чтобы ключ миниатюры уже
существовал. Это же сохраняет ручной выбор при последующих загрузках.
### 4. Валидация обложки при PATCH
`albums.Service.Update` при заданном `cover_photo_id` SHALL проверять, что фото
принадлежит этому альбому (иначе обложкой станет произвольный чужой id). Ошибка —
`BadRequest`/`NotFound`. `clear_cover` по-прежнему очищает.
### 5. Иконка — inline-SVG, диалог — нативный `<dialog>`
Минимальный компонент иконки (один SVG на кнопку создания) с `title` +
`aria-label` для хинта. Диалог имени — нативный `<dialog>`: ESC, backdrop и
focus-trap бесплатно, не тащим оверлей как в `Lightbox`. Поле — только
«Название»; описание остаётся в настройках.
### 6. Очистка висячей обложки при удалении фото
Так как FK нет, при удалении фото, которое является обложкой, бэкенд SHALL
очищать `cover_photo_id` (в `photos.Service.Delete` или через `SetCover`-подобный
метод). Иначе получим обложку на несуществующее фото (визуально — заглушка).
## Risks / Trade-offs
- [Presigned `cover_url` живёт ~1 час, а список может висеть открытым дольше] →
Приемлемо: список перезагружается при навигации; при желании позже добавим
refetch. Для MVP не блокирует.
- [Подзапрос за `thumb_key` на каждый альбом в списке] → Дёшево при индексе
`photos` по PK; presign — локальная подпись без сетевого вызова.
- [`cover_photo_id` задан, но варианты ещё не собраны] → `thumb_key` пуст →
`cover_url = null` → карточка показывает заглушку; авто-обложка ставится только
после сборки вариантов, ручной выбор сразу после загрузки может временно не
иметь миниатюры.
- [Пользователь уберёт обложку во время загрузки нового фото] → авто-обложка
снова назначит свежее фото, если обложка пуста — ожидаемо по спеке.
## Migration Plan
Миграции не нужны. Изменения аддитивны: новое поле `cover_url` в JSON; старые
клиенты его игнорируют. Порядок выката: бэкенд, затем фронтенд. Откат —
возврат предыдущего бинарника и статики; данные не меняются.
## Open Questions
Нет.