chore(openspec): archive thumbnail-variants and sync main specs
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-11
|
||||
@@ -0,0 +1,73 @@
|
||||
# Design
|
||||
|
||||
## Context
|
||||
|
||||
См. `proposal.md` для мотивации. Технические факты, формирующие подход:
|
||||
|
||||
- Генерация вариантов — `photos/pipeline.go:buildVariants`: `thumb = Fit(200,200)`,
|
||||
`preview = Fit(1200,1200)`, WebP q82. Оба кропа вписывают по длинной стороне.
|
||||
- Метаданные имеют две реализации (Postgres и SQLite) с общей контрактной проверкой
|
||||
`internal/repo/contract`; SQL дублируется (`photos/store.go`, `photos/sqlite_store.go`).
|
||||
- Роли заданы жёстко: `photo_objects.role CHECK (role IN ('original','thumb','preview'))`
|
||||
(`migrations/000008`, `migrations/sqlite/000001`).
|
||||
- Варианты генерирует фоновый воркер из очереди `variant_jobs`; лишние объекты удаляются
|
||||
сборщиком мусора по отсутствию ссылок (`photos/worker.go`).
|
||||
- Фронтенд-сетка берёт только `thumb_url` (`PhotoGrid.svelte:78`); обложки — только
|
||||
`cover_thumb_url` (`albums/store.go` джойнит `role='thumb'`).
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Сетка фото и обзор альбомов показывают изображение без апскейла при любом выбранном размере.
|
||||
- Единое поведение на Postgres и SQLite; контрактные тесты покрывают новую роль.
|
||||
- Бэкфилл существующих фото без ручных операций.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Динамоческое ресайзирование на лету / image-proxy.
|
||||
- Иные форматы (AVIF) или согласование форматов.
|
||||
- Изменение `preview` (просмотрщик) и оригинала.
|
||||
- Замер точной плотности DPR-пикселей и «идеальный» выбор варианта — допускается запас.
|
||||
|
||||
## Decisions
|
||||
|
||||
- **Набор вариантов.** `thumb = Fill(200,200)` (квадрат), `medium = Fill(800,800)` (квадрат),
|
||||
`preview = Fit(1200,1200)`. Причина: плитка сетки квадратная (`aspect-square object-cover`),
|
||||
квадратный кроп исключает лишний апскейл короткой стороны; просмотрщик не квадратный, поэтому
|
||||
`preview` остаётся вписанным. Альтернатива — оставить `thumb` вписанным: отклонена, короткая
|
||||
сторона выходит < 200 и мылит уже мелкие плитки.
|
||||
- **Роль `medium` в `photo_objects`.** Роли маппятся 1:1 на поля API и на привязки; расширяем
|
||||
ограничение значением `medium`. Альтернатива — отдельная таблица вариантов: избыточна для
|
||||
одного размера.
|
||||
- **`srcset` + `sizes`.** Ширины `200w/800w/1200w`; `sizes` берётся из фактической ширины сетки
|
||||
(`bind:clientWidth` на `<ul>`, делённой на число колонок). Альтернатива — оценка по `100vw`:
|
||||
неточна из-за рейла и правой панели, ведёт к перекачке 1200.
|
||||
- **Бэкфилл через очередь.** Миграция делает `INSERT ... SELECT id FROM photos` в
|
||||
`variant_jobs`; воркер пересоздаёт роли и перепривязывает, старые объекты уходят в GC по
|
||||
refcount. Альтернатива — воркер, пропускающий уже готовые роли: требует версионирования
|
||||
вариантов; альтернатива — отдельный `cmd/backfill`: лишний код. Выбран самый простой путь.
|
||||
- **Обложка.** Джойн medium-варианта обложки с откатом на `thumb`, если medium ещё не готов;
|
||||
отдельного поля «preview обложки» не вводим.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Перегенерация `preview` впустую при бэкфилле] → воркер работает батчами и параллельно
|
||||
(настраивается); лишние объекты удаляются сборщиком; разовая стоимость.
|
||||
- [Рост хранилища на один объект 800px на фото] → WebP q82 скромного размера; приемлемо.
|
||||
- [Неточный `sizes`] → запас в пользу резкости; при отсутствии `medium_url` вариант просто не
|
||||
попадает в `srcset`, сетка падает на доступные (thumb/preview).
|
||||
- [SQLite не умеет `ALTER CHECK`] → миграция пересоздаёт `photo_objects`; на больших таблицах
|
||||
копирование строк. Приемлемо для текущего масштаба, выполняется в окне обслуживания.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Применить миграцию: расширить `CHECK` роли, налить `variant_jobs` для существующих фото.
|
||||
2. Выкатить код: воркер генерирует `medium` и квадратный `thumb`; API отдаёт новые поля.
|
||||
3. Воркер разбирает очередь; сетка показывает `srcset` с откатом на доступные варианты.
|
||||
4. Откат: сначала код (API перестаёт отдавать medium, воркер не генерирует); затем down-миграция
|
||||
восстанавливает `CHECK`, предварительно удалив привязки `medium`.
|
||||
|
||||
## Open Questions
|
||||
|
||||
Нет.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Proposal
|
||||
|
||||
## Why
|
||||
|
||||
Сетка миниатюр часто отображает изображение крупнее, чем единственный вариант `thumb` (200px),
|
||||
потому что между миниатюрой 200px и превью 1200px нет промежуточного размера. При увеличении
|
||||
плиток (малое число колонок) и на экранах с высокой плотностью пикселей фото расплывается;
|
||||
обложки альбомов в обзоре и вовсе отдают только 200px.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Добавить четвёртый вариант `medium` — квадратный кроп 800×800, WebP — и роль `medium` в
|
||||
журнале объектов. `thumb` перевести на квадратный кроп 200×200; `preview` остаётся вписанным
|
||||
1200×1200 для просмотрщика.
|
||||
- API фото SHALL отдавать `medium_url` рядом с `thumb_url`/`preview_url`; данные обложки
|
||||
альбома SHALL отдавать URL более крупного варианта обложки.
|
||||
- Сетка фото и обзор альбомов SHALL выбирать вариант под фактический размер плитки через
|
||||
`srcset` (200/800/1200) и `sizes`, а не всегда тянуть 200px.
|
||||
- Миграция расширяет ограничение роли `photo_objects` значением `medium` (Postgres и SQLite) и
|
||||
ставит в очередь перегенерацию вариантов для уже загруженных фото.
|
||||
- Из-за смены кропа у `thumb` существующие миниатюры перегенерируются (квадратными) вместе с
|
||||
добавлением `medium`.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
Нет.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `photos`: требование «Варианты изображений» — четыре варианта, квадратный кроп grid-вариантов.
|
||||
- `media-objects`: требование «Журнал объектов» — роль `medium` в наборе ролей.
|
||||
- `albums`: требование «Отображение обложки альбома» — URL обложки крупнее миниатюры.
|
||||
- `ui-workspace`: новое требование — сетка и обзор альбомов подбирают вариант изображения под
|
||||
размер плитки.
|
||||
|
||||
## Impact
|
||||
|
||||
- Backend: `photos/{pipeline,worker,objects,models,service,store,sqlite_store}.go`,
|
||||
`albums/{models,store,sqlite_store,service}.go`, миграции Postgres и SQLite, бэкфилл
|
||||
`variant_jobs`, контрактные тесты `repo/contract`.
|
||||
- Frontend: `PhotoGrid.svelte` и обзор альбомов (`(app)/+page.svelte`).
|
||||
- API: новые поля `medium_url` (фото) и URL medium-обложки (альбом); существующие клиенты
|
||||
продолжают работать (аддитивно).
|
||||
- Хранилище: +1 производный объект (800px WebP) на фото; preview у существующих фото
|
||||
перегенерируется впустую и удаляется сборщиком мусора по ссылкам.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Spec Delta
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Отображение обложки альбома
|
||||
|
||||
Данные альбома SHALL включать URL обложки, когда обложка задана, чтобы клиенты могли
|
||||
показать её в списке, рейле и карточке; URL SHALL указывать на вариант, достаточный для
|
||||
крупного отображения, с откатом на миниатюру, пока крупный вариант не готов.
|
||||
|
||||
#### Scenario: Обложка задана
|
||||
|
||||
- **WHEN** клиент запрашивает список альбомов или альбом по id, и у альбома есть обложка
|
||||
- **THEN** ответ содержит URL обложки
|
||||
|
||||
#### Scenario: Обложка отсутствует
|
||||
|
||||
- **WHEN** у альбома нет обложки или фото обложки удалено
|
||||
- **THEN** ответ не содержит URL обложки, и клиент показывает плейсхолдер
|
||||
|
||||
#### Scenario: Крупный вариант ещё не готов
|
||||
|
||||
- **WHEN** у обложки готов только вариант-миниатюра
|
||||
- **THEN** ответ содержит URL миниатюры обложки
|
||||
@@ -0,0 +1,19 @@
|
||||
# Spec Delta
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Журнал объектов
|
||||
|
||||
Каждый физический объект SHALL иметь ровно одну запись в слое метаданных с ключом,
|
||||
MIME-типом, размером, габаритами и состоянием; фото SHALL ссылаться на объекты по
|
||||
ролям original, thumb, medium и preview.
|
||||
|
||||
#### Scenario: Привязка ролей
|
||||
|
||||
- **WHEN** фото загружено и обработано
|
||||
- **THEN** фото ссылается на объект-оригинал и, когда они готовы, на объекты thumb, medium и preview
|
||||
|
||||
#### Scenario: Оригинал без вариантов
|
||||
|
||||
- **WHEN** варианты ещё не сгенерированы
|
||||
- **THEN** фото доступно с оригиналом, а thumb, medium и preview отсутствуют
|
||||
@@ -0,0 +1,19 @@
|
||||
# Spec Delta
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Варианты изображений
|
||||
|
||||
Система SHALL генерировать четыре варианта: thumb (200×200, квадратный кроп), medium
|
||||
(800×800, квадратный кроп), preview (1200×1200, вписанный) и оригинал, в WebP, и извлекать
|
||||
EXIF (дата, координаты, размеры).
|
||||
|
||||
#### Scenario: Генерация вариантов
|
||||
|
||||
- **WHEN** фото загружено
|
||||
- **THEN** система асинхронно генерирует thumb, medium и preview и заполняет EXIF-поля
|
||||
|
||||
#### Scenario: Форма вариантов сетки
|
||||
|
||||
- **WHEN** генерируются thumb и medium
|
||||
- **THEN** они обрезаны до квадрата, а preview вписан с сохранением пропорций
|
||||
@@ -0,0 +1,23 @@
|
||||
# Spec Delta
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Резкость миниатюр
|
||||
|
||||
Сетка фото и обзор альбомов SHALL подбирать вариант изображения под фактический размер
|
||||
плитки, чтобы миниатюры не отображались крупнее доступного разрешения.
|
||||
|
||||
#### Scenario: Крупные плитки
|
||||
|
||||
- **WHEN** пользователь увеличивает миниатюры до крупных плиток
|
||||
- **THEN** показывается вариант изображения, достаточный для их размера, без расплывания
|
||||
|
||||
#### Scenario: Мелкие плитки
|
||||
|
||||
- **WHEN** миниатюры показаны мелкими плитками
|
||||
- **THEN** показывается компактный вариант, чтобы не загружать крупное изображение
|
||||
|
||||
#### Scenario: Вариант ещё не готов
|
||||
|
||||
- **WHEN** для фото доступен только оригинал или только миниатюра
|
||||
- **THEN** показывается доступный вариант, и сетка отображается корректно
|
||||
@@ -0,0 +1,37 @@
|
||||
# Tasks
|
||||
|
||||
## 1. Варианты и роли
|
||||
|
||||
- [x] 1.1 `backend/internal/photos/pipeline.go`: `thumb` → `imaging.Fill(200,200)`, добавить `medium = imaging.Fill(800,800)`; `buildVariants` возвращает thumb, medium, preview — проверка: обновлённый тест генерации подтверждает квадратную форму thumb/medium и наличие preview.
|
||||
- [x] 1.2 `backend/internal/photos/objects.go`: `RoleMedium = "medium"`; `backend/internal/models/models.go`: `MediumKey`, `MediumURL` — проверка: `cd backend && go build ./...`.
|
||||
|
||||
## 2. Миграции и бэкфилл
|
||||
|
||||
- [x] 2.1 Postgres `backend/migrations/000011_*`: расширить `CHECK` роли `photo_objects` значением `medium`; налить `variant_jobs` для всех фото — проверка: `make migrate` применяется без ошибок на dev-БД.
|
||||
- [x] 2.2 SQLite `backend/migrations/sqlite/000003_*`: пересоздать `photo_objects` с новым `CHECK` (перенос строк) + бэкфилл `variant_jobs` — проверка: `cd backend && go test ./internal/db/...`.
|
||||
|
||||
## 3. Метаданные (Postgres и SQLite)
|
||||
|
||||
- [x] 3.1 Postgres `photos/store.go` + `photos/service.go`: JOIN/scan роли `medium`, `attachURLs` заполняет `MediumURL` — проверка: `make test` (с `TEST_DATABASE_URL`).
|
||||
- [x] 3.2 SQLite `photos/sqlite_store.go`: JOIN/scan роли `medium` — проверка: `make test`.
|
||||
- [x] 3.3 `albums` (models, `store.go`, `sqlite_store.go`, `service.go`): URL medium-обложки с откатом на `thumb` — проверка: `make test`.
|
||||
- [x] 3.4 `backend/internal/repo/contract`: тесты привязки роли `medium` и URL обложки для обоих бэкендов — проверка: `make test` (и с `TEST_DATABASE_URL`).
|
||||
|
||||
## 4. Генерация вариантов
|
||||
|
||||
- [x] 4.1 `backend/internal/photos/worker.go`: создавать и привязывать `medium` (порядок thumb → medium → preview); обновить `worker_test.go` (4 привязанных объекта, `MediumURL`) — проверка: `make test`.
|
||||
|
||||
## 5. Фронтенд
|
||||
|
||||
- [x] 5.1 `frontend/src/lib/types.ts`: добавить `medium_url`, `cover_medium_url` — проверка: `cd frontend && npm run check`.
|
||||
- [x] 5.2 `frontend/src/lib/components/PhotoGrid.svelte`: `srcset`/`sizes` по фактической ширине сетки (`bind:clientWidth`), откат на доступные варианты — проверка: `npm run dev` — крупные плитки резкие, мелкие тянут компактный вариант.
|
||||
- [x] 5.3 Обзор альбомов `frontend/src/routes/(app)/+page.svelte`: `srcset` обложки с откатом на миниатюру — проверка: `npm run dev` — обложки не мылят при крупном размере.
|
||||
|
||||
## 6. Интеграционная проверка
|
||||
|
||||
- [x] 6.1 `make test` и `cd frontend && npm run check && npm run build` проходят; вручную: резкость на всех размерах, обложки в обзоре, бэкфилл существующих фото не ломает сетку.
|
||||
|
||||
## Workflow follow-up
|
||||
|
||||
- Заархивировать change после выполнения требований ревью проекта (`/opsx-archive`).
|
||||
- Проверить результат после архивации.
|
||||
Reference in New Issue
Block a user