# Design ## Context См. `proposal.md` для мотивации. Текущее состояние, важное для подхода: - Авторизация — Bearer-токен (не cookies), поэтому `` не может обращаться к защищённым эндпоинтам напрямую. Все фото уже отдаются фронтенду как 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` с редиректом — не работает, `` не приложит 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, диалог — нативный `` Минимальный компонент иконки (один SVG на кнопку создания) с `title` + `aria-label` для хинта. Диалог имени — нативный ``: 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 Нет.