PhotoPrism's /api/v1/config.count returns library-wide aggregates to
any authenticated session, with no per-user scoping. The timeline
itself IS scoped (a guest sees zero photos), but the sidebar was
rendering admin-side totals next to Review / Hidden / Archive / Tags /
Map / root for non-admins — including a freshly-registered "test"
user with role=guest and BasePath="".
Until PhotoPrism gains per-user counters, the SPA now derives an
`isAdminUser` flag and gates every count that's drawn from
configQuery on it. Non-admin users see the labels without badges;
counts re-appear automatically when promoted. Per-folder counts from
the sidecar (which DO scope to BasePath) are unaffected.
- /tags: each tab pill shows its own count (labels/keywords =
distinct tags, ratings/colors = photos covered). Labels and marks
queries become always-enabled on the route so every pill resolves
immediately; keywords stays lazy.
- LeftSidebar: Map badge now reads from the shared `['geo']` cache so
it matches /map's "N geotagged" footer instead of count.places
(distinct locations). Tags badge sums the four inner counts;
keywords contributes lazily once /tags?tab=keywords is visited.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- /duplicates and /inbox routes removed and folded into /review as
additional tabs alongside cause tabs; /duplicates keeps a redirect
for bookmarks.
- LeftSidebar: drop import/inbox tile and favorites; show per-user
BasePath label at the folder root.
- RightSidebar: split file header into read-only path over editable
basename (matches sidecar rename contract); date field switches to
plain-text ISO YYYY-MM-DD (no native datetime picker) with strict
validation and revert-on-invalid-blur; preserves original hour.
- BulkMetadataSidebar: same ISO-only date input with invalid-state
styling and apply-button gating.
- BulkActionBar: drop redundant Restore and Undo buttons; ⌘Z still
reachable via gridKeyNav.
- gridKeyNav: remove favorite toggle (F) alongside the favorites view
retirement.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sidebar Duplicates badge now sums stacks + cross-folder groups, with
cross-folder observed from cache (no eager disk scan from the sidebar).
Cross-folder tab auto-fires the scan on access; button becomes Rescan.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PhotoPrism's user entity carries a per-user BasePath; the web app now
mirrors that scope client-side so each user sees only their own subtree
in the sidebar, timeline, folder counts, and heap-convert target picker.
Admin without a BasePath is unchanged. Also removes the redundant
"✕ <folder>" pill below the folder tree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Builds a dedicated /review route that mirrors /duplicates' chrome:
- /review/+page.svelte mounts Toolbar + ReviewView + RightSidebar
- CauseGroupCard.svelte renders one card per cause with Approve all
and Archive all bulk actions plus a per-cause suggestion line
- CauseBadges.svelte shows every matching cause as chips on each tile
- services/adapters/review.ts fetches review:true and groups photos
by primary cause; current taxonomy is low_resolution >
stripped_exif > implausible_year > non_image_type > quality_other
(low_resolution ranks first because it's the most actionable signal)
Sidebar gains an opt-in showRelated prop that adds three
RelatedStrip panels (same folder / camera / year) for the
'decide these together' workflow.
LeftSidebar's Review entry switches from a section filter to a
route link so /review picks up the click.
PpPhoto gains the missing Resolution field PhotoPrism actually
returns on list responses.
Earlier we (a) wildcarded the per-folder count fan-out in the sidecar
so internal tree nodes (year folders, etc.) recurse, and (b) flipped
the timeline root view to mean "the whole library" instead of
"photos with no path component". The remaining piece — the badge on
the root row — still computed `total - Σ(folderCounts)`, which used
to give the count of root-direct photos. With recursive folder
counts that subtraction double-counts every nested photo (year +
month + …) and clamps the badge to 0.
Use PhotoPrism's authoritative `count.all` directly. That now matches
what the timeline shows under `/` (everything indexed) without an
extra round-trip.
Six-item frontend performance pass on the SvelteKit app.
P1 — Move per-folder photo counts to a new sidecar endpoint and defer
the fetch to requestIdleCallback. The old client-side path fired one
/photos?count=1000 per folder from the browser (≈1 MB JSON × N folders)
on every cold sidebar mount; the new POST /api/sidecar/folders/counts
fans out over loopback with bounded concurrency and returns a single
{path: count} payload of a few KB.
P2 — Bound the visibleRange scroll-scan around the previous visible
band instead of sweeping every shell from index 0 on each scroll-rAF.
Falls back to a full sweep on cache miss (filter reset, programmatic
jump) so behaviour is unchanged at the edges.
P3 — Adaptive thumbnail size + srcset. PhotoTile now picks the smallest
PhotoPrism tile_* variant (100/224/500) that covers the user's grid
preset at the current DPR. Adds decoding="async".
P4 — Lift the selection check above the {#each} loop. Mostly readability
— SvelteSet.has() is already per-key reactive — but keeps the hot loop
body terse.
P5 — Split dedupedAll / photos derivations so filter-store mutations
(search-as-you-type, section toggles) don't re-walk every loaded page;
only the cheap folder-scope filter re-runs.
P6 — Dynamic-import PreviewOverlay on first preview.uid !== null and
cache the loaded module; closing the overlay leaves the component
mounted with its internal {#if} collapsing the DOM.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- visibleRange action rewritten to scan [data-uid-shell] divs on each
rAF-throttled scroll instead of attaching an IntersectionObserver to
sample tiles. The observer approach broke on return from /inbox: with
cached photo data, shells mounted in the same Svelte pass as the
scroll root and tileRegister fired before any __visibleRange stash
was in place, so registrations dropped silently. Fast scrolling could
also strand the observer in a dead zone when every sample tile left
the viewport before the next was mounted. Shells are always rendered,
so a DOM scan always finds a true first/last.
- Extract PhotoTile + SkeletonGrid so the timeline and the drill-in
PhotoGrid share one tile chrome (selection animation, badges,
hover-only "open preview" affordance).
- FolderTree count badge moves inside the row's button so the badge
area becomes part of the click target instead of a dead zone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- /tags hosts four tabs (Labels (auto) / Keywords / Ratings / Colors),
URL-driven with pagination on the label + keyword grids; ratings and
colors stay as fixed buckets.
- /duplicates tabs (Stacks / Cross-folder) restyled to pill row in the
Toolbar to match /tags; tab state moved into the route and bound to
?tab=...
- New aggregateKeywords() service fans out per-photo getPhoto calls so
user-typed Details.Keywords surface on /tags (PhotoPrism's /labels
only returns classifier output).
- RightSidebar renders photo.Labels[] as dashed-border chips after the
Keywords section, each linking to /?q=label:slug.
- /colors and /ratings routes redirect to /tags?tab=colors|ratings so
old bookmarks still land somewhere useful; LeftSidebar drops their
entries and the Tags badge now sums labels + ratings + colors.
- listFolderCounts dedupes by UID (merged=false returns one row per
FILE, so HEIC+JPG / Live Photo / RAW+JPG pairs were inflating folder
counts ~2x).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sidebar
- New `/` root-folder entry at the top of the Folders group. Active
when the timeline is scoped to root; the photo grid post-filters to
`Path === ''` because PhotoPrism's `path:` operator can't express an
exact-root match. Collapsible chevron, persisted to its own
localStorage key, and a kebab carrying just "New subfolder".
- Per-folder count badges. `/api/v1/photos?q=path:X&count=1000` per
folder in parallel via `listFolderCounts`; root count derived from
`config.count.all − Σ subfolder counts`.
- Folder tree starts at depth=1 under the root so nested rows indent
visually relative to `/`.
- Footer matches the Toolbar / action-bar h-9 height.
Timeline interaction
- Single click on a tile selects only that tile (clears others); the
preview now lives on dblclick. Modifier clicks still go through
`gridKeyNav`'s document handler (shift = range, cmd/ctrl = toggle).
- `x` (archive) now actually archives — PhotoPrism's photo PUT
silently drops the Archived field, so we route through
/batch/photos/{archive,restore} the same way the BulkActionBar
already did. Mirror for `u`.
- Preview close restores the timeline focus + scrolls the last-shown
photo into view via `forcedExpand`+`scrollTileIntoView` so it
actually mounts (selection ring would otherwise stay invisible when
the user navigated far in preview).
- `applyFolderScope` only narrows the timeline to root when the active
view is a folder view (no heap / search / non-default section), so
label clicks / heap views / favorites no longer drop subfolder
photos.
Action bar
- Inline `h-9` row at the bottom of the main column (not `fixed`),
matching the Toolbar's visual language. Right sidebar stays full
height — the bar only spans the timeline width.
- Approve action wired for the review pile.
Colors / Tags / Ratings drill-ins
- New shared `PhotoGrid` component owning tile rendering, selection
styling, single-click-selects + dblclick-previews, and `setOrder`
for arrow-key nav.
- Each route's drill-in `<main>` carries `use:gridKeyNav` and a
trailing `<BulkActionBar />` so shift/cmd/ctrl click, arrow keys,
and the keyboard shortcuts work the same as the timeline.
- Tags switches from `goto('/?q=label:…')` to an in-place drill-in
with a back button, mirroring `/colors`'s flow.
- Category cards + drill-in photo cards honour the global
`view.thumbnailSize` (XS–XL) so the timeline's size selector now
reaches into all four grids.
Settings
- General-settings dialog merges Appearance into UI and switches free
text inputs to selects for the PhotoPrism theme / language / start
page / map style (the value-from-server prepends if it's outside
the curated list so we never silently rewrite a custom value). Time
zone uses `<datalist>` with `Intl.supportedValuesOf('timeZone')`.
Sidecar
- Heap convert runs reindex synchronously per source path so the
client's invalidate-and-refetch sees the moved files.
Inbox
- New /inbox route stub for the upcoming import workflow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add a Settings cog to the Folders header that opens a tabbed library
admin dialog (Library / Index / Import / Logs) wrapping PhotoPrism's
/api/v1 settings, index, import and errors endpoints.
- Add a sticky footer to the left sidebar with the signed-in user's
display name plus quick-toggle theme, general-settings cog (separate
dialog for app prefs), and sign-out. Pull these out of the top
Toolbar trailing slot.
- Align depth-0 folder rows with the rest of the sidebar entries (drop
the leading chevron column when no children) and bring heap rows in
line with folder rows so the kebab is part of the row's hover
background instead of a detached chip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the legacy mule-image backend with PhotoPrism plus a thin
SvelteKit client and a Node sidecar for endpoints PhotoPrism doesn't
expose (file rename), and add a two-phase migrator (metadata via PUT,
heaps → albums) for the existing Postgres library.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>