Folder create/rename/delete/move, photo move, heap convert, and file
rename now reject paths outside the caller's BasePath (403). Sources
resolved via PhotoPrism UIDs are re-checked in movePhotoFiles. The
USER_BASEPATHS reconciler also sets upload_path so client-app uploads
land inside the user's subtree.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two distinct bugs were causing left-sidebar badges to under-report:
1. sidecar/folders/counts hard-capped each PP /photos call at count=1000
and deduped UIDs from that single page. Any folder with >1000 file
rows under it (typical for a multi-year root scan with HEIC sidecars)
silently lost everything past row 1000. On this library the root
badge reported 912 while the year subfolders summed to 1175. Loop
offsets instead, breaking when PP returns a short page.
2. The Labels-badge query passed all:true label:* to PP, which 400s with
"Unable to do that" - none of the other bucket queries prefix
all:true. Drop it; the scoped() helper already injects the user's
path clause when applicable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two paired fixes for the folder tree on the timeline:
* +page.svelte: drop the `Path === ''` post-filter for the root entry.
PhotoPrism's indexer always nests photos under YYYY/MM, so "photos
whose Path is empty" is always the empty set in practice — the root
entry looked broken instead of "whole library". Treat `/` as the
unscoped view and rely on subfolder selections (now wildcarded via
filters.svelte.ts) for narrowing.
* sidecar/handlers_folders.go: the per-folder count fan-out used
`q=path:<x>`, the same exact-match operator that just got fixed in
the web filter. Result: every year-level folder reported count=0
in the sidebar. Switch to `q=path:"<x>*"` so the count reflects
the whole subtree (dedupe by UID still in place).
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>
Replace the Node prototype (server.mjs) with the stack the merge plan
calls for: Go 1.25, Gin for routing, GORM + MariaDB for persistence.
Same wire contract on /api/sidecar/* so the SvelteKit client doesn't
change.
- Marks move from a JSON file on disk to mule_sidecar.marks (auto-
migrated by GORM on first boot). The Node prototype's marks.json
was dev-only; not migrated.
- Folder/rename/heap-convert/duplicates handlers reproduce the
prototype's behaviour, including the path-traversal defence
(resolveUnderRoot + EvalSymlinks), the size-bucket prefilter for
the duplicate hasher, and the background reindex fire-and-forget
pattern.
- Auth model unchanged: requireSession middleware proxies the
caller's X-Auth-Token to PhotoPrism's /api/v1/photos?count=1
before any destructive op.
- Expose pp-mariadb on 127.0.0.1:3306 in docker-compose so the
host Go process can reach mule_sidecar.* without joining the
container network.
- Archive the Node prototype under sidecar/legacy/server.mjs for
one cycle as reference.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>