chore(openspec): архивировать album-live-sync и синхронизировать основные спеки

- realtime: личный поток несёт события жизненного цикла альбома и изменения
  счётчика фото; добавлены требования о жизненном цикле, получателях, форме
  payload'а, догоне и потере доступа
- albums: photos_count считает только фото вне корзины
This commit is contained in:
iu
2026-10-15 23:25:07 +03:00
parent e82f45647c
commit 6e68242ec2
8 changed files with 556 additions and 2 deletions
@@ -0,0 +1,85 @@
# Proposal
## Why
Real-time сейчас работает только для открытого альбома: события изменений альбома публикуются
в канал конкретного альбома, поэтому у пользователя, который не смотрит этот альбом, список
альбомов (рейл, главная) остаётся устаревшим до перезагрузки страницы. Персональный поток
`GET /api/sse/me` существует, но не несёт событий жизненного цикла альбома, и ни один клиент
в него не подписан — то есть создание, переименование, смена обложки, изменение состава
участников и удаление альбома не доходят до тех, кого они касаются.
Отдельно устаревает состав фото: загрузка, перемещение, копирование, восстановление и удаление
фото не публикуют событие альбома, поэтому счётчик `photos_count` и обложка на карточках не
обновляются, а открытая страница альбома у других клиентов не показывает загруженное фото
(`Upload` не публикует `photo.created`, хотя способность `realtime` это требует).
## What Changes
- Персональный поток `GET /api/sse/me` начинает нести события жизненного цикла альбома:
`album.created` и `album.updated` с полным объектом альбома в той же форме, что отдаёт
`GET /api/albums`, и `album.deleted` с `{id}`.
- Получатели события — пользователи, для которых альбом входит в список доступных: владелец
(`albums.owner_id`) и участники (`album_members`); событие доставляется веером по личным
каналам.
- Публикации добавляются в: создание альбома, принятие инвайта, изменение названия/описания/
обложки, смену роли, исключение участника и выход, удаление альбома.
- Список альбомов обновляется и при изменении состава фото: загрузка, перемещение,
копирование, восстановление, перевод в корзину и окончательное удаление публикуют
`album.updated` в личные потоки владельца и участников, поэтому `photos_count` и обложка
на карточках становятся актуальными без перезагрузки.
- Загрузка фото публикует `photo.created` в канал альбома, поэтому открытая страница альбома
у других клиентов добавляет новое фото; готовность вариантов по-прежнему приходит как
`photo.updated` и обновляет фото на месте.
- Web-клиент подписывается на `/api/sse/me` на уровне layout'а приложения и применяет
события к стору списка альбомов (`upsert` / `remove`) без повторного запроса; при
переподключении догоняет состояние через `GET /api/albums`.
- Открытая страница альбома начинает обрабатывать `album.deleted` (сейчас обрабатывается
только `album.updated`, поэтому удалённый альбом продолжает отображаться у участников).
- Контракт существующих пер-альбомных событий (`photo.*`, `like.*`, `comment.created`,
`member.*`, `album.updated`) и REST не меняется: `Upload` начинает публиковать уже
предусмотренный контрактом `photo.created`, остальное — аддитивно.
**Не входит в этап** (осознанно отложено): персистентный центр уведомлений (колокольчик,
список непрочитанных, новые типы в таблице `notifications`); Android-клиент (личный поток
остаётся совместимым, подключение мобильного клиента — отдельный шаг); принудительный обрыв
пер-альбомного SSE у исключённого участника.
## Capabilities
### New Capabilities
_Нет — поведение описывает расширение существующей способности `realtime`, отдельная
способность не вводится._
### Modified Capabilities
- `realtime`: персональный поток обязан нести события жизненного цикла альбома с полным
payload'ом; определяются получатели события, поведение клиента при получении (обновление
или удаление альбома в списке без запроса) и догон состояния при переподключении. Дополнительно
определяются события при изменении состава фото: список альбомов обновляется в реальном
времени, а загрузка фото публикует `photo.created`.
- `albums`: уточняется счётчик `photos_count` — он считает только доступные в альбоме фото
(вне корзины), чтобы совпадать с сеткой и корректно меняться при переводе в корзину и
восстановлении.
## Impact
- **Код (backend):** `internal/sse` (веер по личным каналам), `internal/albums/service.go`
(Create/Update/Delete, рассылка `album.updated` получателям), `internal/members/service.go`
(Accept/ChangeRole/Remove), `internal/photos/service.go` (загрузка/удаление/перемещение/
копирование/восстановление фото публикуют `photo.created` и `album.updated`), точка
сборки `internal/app/app.go` и `cmd/server/main.go` — если для веера потребуется новый
интерфейс-резолвер аудитории альбома.
- **Код (frontend):** `routes/(app)/+layout.svelte` (единая подписка на `/api/sse/me`),
`lib/stores/albums.svelte.ts` (upsert/remove/load как обработчики событий),
`routes/(app)/albums/[id]/+page.svelte` (обработка `album.deleted`), `lib/sse.ts` —
список известных событий.
- **API:** новых HTTP-эндпоинтов нет; существующий `GET /api/sse/me` начинает отдавать
дополнительные типы событий. Для старых клиентов изменение аддитивное.
- **Не затрагивается:** `notifications` (персистентные уведомления остаются как есть),
`android-client` (требование live-обновлений там и так ограничено активным экраном
альбома), схема БД, миграции, зависимости.
- **Известное ограничение, фиксируемое отдельной задачей:** доступ к пер-альбомному SSE
проверяется только при подключении, поэтому исключённый участник продолжает получать
события альбома до разрыва соединения.