fix(ui): propagate photo tag edits to grid and viewer

Add a shared photo-updates signal the panel publishes on rename or tag
add/remove; the album grid and viewer subscribe and patch the changed
photo into their lists, so the 360 badge and panorama mode update without
a reload.
This commit is contained in:
iu
2026-10-12 12:14:48 +03:00
parent eef7c6c5d0
commit e4975aafb5
9 changed files with 190 additions and 1 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-11
@@ -0,0 +1,73 @@
# Design
## Context
See `proposal.md` - Why. Relevant current state:
- The photo panel lives in the app layout (`AlbumPanel.svelte`) and keeps its own copy of
the photo; `PhotoPanel` reports edits through `onRenamed(updated)`.
- Each route keeps its own list: the grid page and the viewer page both fetch
`photos`, and `Viewer` additionally fetches a single `fresh` photo.
- Renames reach the grid through the album SSE `photo.updated` event; `AddTag`/`RemoveTag`
publish nothing and the panel's callback only touches panel-local state, so tags never
reach the route lists.
## Goals / Non-Goals
**Goals:**
- A tag change in the panel is reflected in the grid (360 badge) and the viewer
(panorama mode) with no reload.
- Keep the change local to the acting client so user-scoped tags are not mixed between
users.
**Non-Goals:**
- Replacing the SSE `photo.updated` path used for renames and variant readiness.
- Reworking the photo list into a single shared per-album store.
- Propagating tag changes across devices/users (tags remain user-scoped).
## Decisions
### Frontend-local signal instead of an SSE broadcast
Add a small `photoUpdates` store that exposes the latest refreshed `Photo`. The panel
publishes; the grid and viewer pages subscribe and patch their own lists by id.
- Alternative considered: publish `photo.updated` from `AddTag`/`RemoveTag` like
`Rename`. Rejected - the SSE payload is built for the acting viewer and would carry
that user's tags to every album member, overwriting their own tags in their grid.
Local propagation keeps each client on its own tag set.
### One shared store, consumed by id patch
`photoUpdates.latest` holds the most recent published photo; an `$effect` in each
consumer maps over its `photos` and replaces the matching id. Only photos already loaded
are affected (the panel only edits photos the user has open/selected), so no
insert/remove logic is needed.
- Alternative considered: promote the album photo list into a shared store. Rejected -
it touches loading, pagination and SSE handling for no behavior gain here.
### Publish on the existing `onRenamed` path
`AlbumPanel.onRenamed` already receives the server-refreshed photo for tags and rename.
It publishes that photo in addition to its local state and `selection.replace`. Renames
also flow through SSE, so the extra patch is idempotent.
### Cover the viewer by patching its page list
The viewer page passes its `photos` to `Viewer`, and `Viewer` derives `current` from it,
so patching the page list makes the panorama mode switch live. `Viewer`'s internal
`fresh` fetch is left as-is.
## Risks / Trade-offs
- [Two photos updated within one tick could drop one] -> User edits are sequential; keep
the signal as "latest" and patching by id. Revisit with a map if batch edits appear.
- [Grid list reassignment on patch] -> Only the changed id is replaced; Svelte keyed each
keeps DOM stable.
- [Panel state and route list diverge again] -> Single publish point in `onRenamed`
covers tags and rename.
## Migration Plan
No data or API change. Deploy frontend only. Rollback is reverting the deploy.
## Open Questions
None blocking.
@@ -0,0 +1,38 @@
# Proposal
## Why
After adding or removing a tag in the photo panel, the grid and viewer keep showing
the stale photo: the `pano360` badge and panorama mode only appear after reloading the
page. Tag edits are the one panel change that updates nothing outside the panel —
`AddTag`/`RemoveTag` publish no event, and the panel's `onRenamed` only refreshes the
panel's own copy, not the page-level photo lists.
## What Changes
- Introduce a shared photo-updates signal that the panel publishes to and the grid and
viewer pages subscribe to, so a changed photo (tags included) is patched into their
local lists immediately.
- Wire the photo panel to publish the refreshed photo on tag add/remove (and rename is
covered by the same path, harmlessly).
- Add a `ui-workspace` scenario: changing a photo's tags in the panel updates the grid
badge and the panorama mode without a page reload.
## Capabilities
### New Capabilities
<!-- none -->
### Modified Capabilities
- `ui-workspace`: the «Метки в панели фото» requirement gains a no-reload propagation
scenario.
## Impact
- Frontend: new `frontend/src/lib/stores/photo-updates.svelte.ts`;
`frontend/src/lib/components/AlbumPanel.svelte` (publish);
`frontend/src/routes/(app)/albums/[id]/+page.svelte` and
`frontend/src/routes/(app)/albums/[id]/photos/[photoId]/+page.svelte` (subscribe+patch).
- Spec: `openspec/specs/ui-workspace/spec.md` («Метки в панели фото»).
@@ -0,0 +1,16 @@
# Spec Delta
## MODIFIED Requirements
### Requirement: Метки в панели фото
Панель фото SHALL содержать раздел «Метки» с чипами меток фото и полем добавления, дающим
поиск по меткам пользователя и создание новых. Изменение меток в панели SHALL сразу отражаться
в сетке и в режиме просмотра без перезагрузки страницы.
#### Scenario: Раздел «Метки»
- **WHEN** открыт просмотр или выбран один фото в сетке
- **THEN** в панели есть раздел «Метки» с текущими метками и полем добавления с поиском
#### Scenario: Обновление без перезагрузки
- **WHEN** пользователь добавляет или снимает метку в панели фото
- **THEN** сетка и режим просмотра сразу отражают изменение (например, бейдж и режим 360-панорамы), без перезагрузки страницы
@@ -0,0 +1,20 @@
# Tasks
## 1. Shared photo-updates store
- [x] 1.1 Add `frontend/src/lib/stores/photo-updates.svelte.ts` exposing `latest: Photo | null` and `publish(photo: Photo)`; verify `npm run check` passes.
- [x] 1.2 In `frontend/src/lib/components/AlbumPanel.svelte`, call `photoUpdates.publish(updated)` from `onRenamed` (keeping the local `photo` assignment and `selection.replace`); verify the tag chips in the panel still update as before.
## 2. Route lists reflect the update
- [x] 2.1 In `frontend/src/routes/(app)/albums/[id]/+page.svelte`, add an effect that patches `photos` by id from `photoUpdates.latest`; verify that adding a `pano360` tag shows the 360 badge in the grid without reloading.
- [x] 2.2 In `frontend/src/routes/(app)/albums/[id]/photos/[photoId]/+page.svelte`, add the same patch to its `photos`; verify the panorama mode switches on/off live after tagging while in the viewer.
## 3. Integration verification
- [x] 3.1 Run `npm run check` and `npm run build` (frontend); verify both pass with no regressions.
## Workflow follow-up
- Run `/opsx-apply` to implement this change.
- Archive the change after review and verification (sync the delta spec into `ui-workspace`).