- Gate the sidebar's "Suggested from path" row on /review?tab=stripped_exif
instead of a per-photo TakenSrc heuristic — PhotoPrism stores a guessed
TakenAt for stripped-EXIF photos too, so the heuristic was hiding the
row even when a path-derived date was available.
- Same gate on the BulkActionBar's "Accept date & Keep" button.
- Extract acceptDateAndKeep() + cachedPhoto() into photoActions so the
bar button and a new bare-'a' shortcut in gridKeyNav share one path.
- Show an 'A' kbd hint on the bar button.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- CauseGroupCard now wraps PhotoGrid so selection, keyboard nav, and
preview flow through the standard timeline plumbing. Per-tile hover
Approve/Archive and the group-wide Approve all are gone — the bottom
BulkActionBar's review-section Keep/Archive handle single + bulk.
- Low Resolution tab opts into a new PhotoTile dimensionBadge prop so
WxH stays visible on each tile.
- New suggestDateFromPath util parses YYYY-MM-DD from filename or
folder path. RightSidebar surfaces it as an amber Apply row above
the Taken-at input whenever the photo lacks a trusted TakenAt.
- BulkActionBar gains a "Accept date & Keep" button (review section
only) that patches each selected photo's TakenAt from its path
suggestion when available, then approves.
- Drop the Same folder / Same camera / Same year strips and the
RelatedStrip component from the metadata sidebar.
Also bundles in-progress Notes route + tile components and small
tweaks to LeftSidebar, DuplicatesView, CrossFolderGroupCard, and
photoprism.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Account tab in General settings — self-service password change.
- UsersDialog (admin-only footer entry) — full /api/v1/users CRUD with
admin-issued password reset.
- People as a fifth tag category alongside Labels/Keywords/Colors/Ratings,
backed by /api/v1/subjects and the `person:` DSL clause.
- About tab in Library settings — version, library counts, feature chips,
and a collapsible env-config help panel for the bits PP has no runtime
API for (OIDC, TF, WebDAV).
- Library tab expanded with Indexer-advanced, extra Downloads checksums,
and a Features grid that only renders keys PhotoPrism actually returns.
- Fix the SettingsDialog null-draft race the same way GeneralSettingsDialog
already had: normalize on open, never null on close.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces "PhotoPrism" in UI strings (empty states, tooltips, toasts,
log header, login screen) with neutral terms like "the indexer", "the
library", "the server" — accurate regardless of backend. The login
header becomes "Mulimage" and drops the explicit PhotoPrism mention.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PhotoPrism's /api/v1/config.count is library-wide and the same value
for every authenticated session. That made non-admins (and admins
with a non-empty BasePath) see badges that didn't match what the
timeline actually showed them.
Replaces the direct `configQuery.data?.count?.<bucket>` reads in
LeftSidebar with per-bucket queries against PhotoPrism's /photos
endpoint. The new `countPhotos(q)` helper sets `count=10000` and
reads the X-Count response header to get the true total in one round
-trip (PhotoPrism's ACL filter is what scopes the result, so the
header reflects "what this session can see").
Each bucket query appends `path:"<BasePath>*"` so admins-with-a-
BasePath stay scoped too; non-admins without a BasePath short-circuit
to `uid:none` (their effective visibility is zero, no point
querying). Admins without a BasePath skip the scoped queries
entirely and keep using the precomputed /config totals — same
network footprint as before for the common case.
Affected badges: Favorites, Hidden, Archive, Review, Tags (labels
component). Map already used `geoQuery` whose result is ACL-filtered
server-side, so its badge is per-user-correct without changes. The
`favorites` field was missing from PpClientConfig.count's TypeScript
type; added it.
Resolves the `test`-user complaint: sidebar showed the admin
library's totals next to Review / Hidden / Archive / Favorites
because those numbers came from /config, not from a user-scoped
query.
- /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>
The /review cards now share the timeline's PhotoGrid, gridKeyNav,
BulkActionBar, and global selection store instead of carrying parallel
implementations. The card is reduced to a header (title + count +
Dismiss / Archive all), an advisory caption, and a PhotoGrid; keyboard
nav, hover affordances, multi-select, and bulk verbs come from the
shared machinery.
- New web/src/lib/services/photoActions.ts holds the canonical
dismissPhotos / archivePhotos helpers (toast wording, focus advance,
undo push, ['photos'] + ['review-groups'] cache invalidation).
BulkActionBar.onApprove / onArchive and gridKeyNav.approveCullTargets
/ toggleArchive('archive') route through it. CauseGroupCard's
Dismiss / Archive-all buttons call the same helpers - one code path
from any surface.
- Approve verb renamed to "Dismiss" across BulkActionBar, gridKeyNav
toasts ("Kept N" -> "Dismissed N"), and the new review card. The
BulkActionBar Clear/Dismiss clear button is just "Clear" now so the
verb only means the action.
- /review sets filters.section='review' on mount and restores on
unmount, which is what swings the shared action surface into review
semantics; an effect clears the selection on tab change so a
previously-selected photo from another cause can't be hit by a new
tab's bulk verb.
- The route mounts BulkActionBar at the bottom and swaps the right
aside to BulkMetadataSidebar when selection.ids.size >= 2 - same as
the timeline; gives the user a one-shot "apply this Date / Caption /
Keyword to all selected" affordance for EXIF-stripped batches.
- CauseGroupCard drops its bespoke keyboard handler, ResizeObserver,
focusedIdx state, per-tile hover Approve/Archive buttons, confirm()
dialogs, and toast.loading worker loop. The unused CauseBadges
component is removed.
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.
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>
PhotoPrism's /api/v1/oidc/redirect handler doesn't actually set
auth_token/auth_session cookies — it returns an HTML page that does:
setItem("pp:<storageNamespace>:session.id", <session uid>)
setItem("pp:<storageNamespace>:session.token", <X-Auth-Token value>)
setItem("pp:<storageNamespace>:session.user", <user JSON>)
setItem("pp:<storageNamespace>:session.provider", "oidc")
window.location.href = "/library/login";
The deployment's reverse proxy is expected to bounce /library/login
(and /library/*) back to `/`; the SPA then reads PhotoPrism's
storageNamespace from /api/v1/config, looks up session.id and
session.token under that prefix, and adopts the session.
Confirmed via the M0 test instance: prior to this change, server-side
sessions were created on every OIDC return (DB row present) but the
browser had no way to claim them, so the user bounced back to /login.
The SvelteKit /login was username/password only; the legacy comment
even called out 'OIDC SSO ships in M4 when the IdP is wired up'.
Authentik is wired up now, so:
- /api/v1/config exposes ext.oidc when the IdP is configured. Fetch
it on the login page and conditionally render "Sign in with
{provider}", which kicks off /api/v1/oidc/login.
- After PhotoPrism completes the auth code exchange, it sets
`auth_token` + `auth_session` cookies and redirects to siteUrl
(/library/browse by default; the deployment's reverse proxy is
expected to bounce that to /). bootstrapSessionFromCookies()
reads those cookies, calls GET /api/v1/session/<id> with the
cookie's token, and adopts the resulting session into the SPA
store on mount.
- Root layout's auth guard now waits for the bootstrap pass before
punting to /login, so a fresh OIDC return doesn't get redirected
away before the session is read.
- New selection.focusAfter(excluded) walks selection.order forward past
the archived/restored set so X-ing through the timeline keeps the
cursor on the next live photo instead of falling back to photo[0]
via the auto-anchor effect. Wired into gridKeyNav.toggleArchive (X
key) and BulkActionBar.onArchive.
- Auto-focus effect on the timeline always re-anchors to photos[0] on
view load (pageCount → 1), instead of preserving a stale uid from
the previous filter.
- PhotoGrid re-anchors focus when the previously focused uid isn't in
the new photo set, so drilling into a /tags category drops the
cursor on its first tile instead of carrying a stale selection from
whatever view the user came from.
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>