Files
2026-10-10 14:21:43 +03:00

6.7 KiB
Raw Permalink Blame History

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

Нет.