dtoro 29f7ad7073 web: deep-link from RightSidebar to folder/map + anchor-mode timeline + fix root count
RightSidebar:
- Folder + Location rows gain a small ArrowUpRight icon button that
  deep-links into the timeline / map view focused on the photo. OSM
  external link removed; the in-app map nav covers the same job.

Folder navigation:
- New navigateToFolder(path, { focusUid, focusTakenAt }) helper in the
  filters store; LeftSidebar's pickFolder collapses to a one-liner that
  reuses it.
- One-shot pending-focus stash carries both UID and TakenAt across the
  goto. URL-watch effect on the timeline consumes the stash so even
  same-folder navigations (where the filter doesn't change) get
  picked up.

Anchor-mode timeline query:
- listPhotosAround(q, takenAt, after, before) issues two parallel
  PhotoPrism calls (`after:<day-1>` oldest-first + `before:<day+1>`
  newest-first), merges + dedupes newest-first. Uses PhotoPrism's
  existing date-only DSL clauses — no server changes.
- When a deep-link stashes a TakenAt, page 0 of the photosQuery uses
  the merged window so the target photo is loaded even for photos
  buried past the standard newest-first cursor. Pages 1+ are disabled
  in anchor mode (PhotoPrism's day-precision cursor would infinite-loop
  on dense days; users see 120 around the target, refresh to drop the
  anchor).
- After page 0 lands, the existing scrollToIndex(targetIdx) expands the
  windowed render set + scrolls the tile into view.

Map view:
- /map honors `?lat=&lng=&zoom=&focus=` URL params, jumping to the
  photo's coordinates at zoom 17 instead of fitBounds-ing the full
  library. Params are stripped after first apply so a manual zoom-out
  + reload doesn't snap back.

LeftSidebar root count badge:
- Now matches what Cmd+A selects in the timeline. Old code used
  /config.count.all (library aggregate, includes archived/hidden/
  review). Switched to countPhotos('', { merged: true }) which counts
  the actual photo entries the timeline lists.
- countPhotos gains a `merged` option; with merged=true it returns the
  response body length instead of the X-Count header — PhotoPrism's
  X-Count is always the file-row count regardless of merged, so a
  HEIC + JPG companion pair inflated the badge to 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:04:23 +02:00

mule-image

Self-hosted photo management built on top of PhotoPrism. A SvelteKit frontend (web/) plus a small Go service (sidecar/) fill in the keyboard-driven UI and the file/folder/mark endpoints PhotoPrism's REST API does not expose. PhotoPrism itself handles indexing, originals, thumbnails, and the database; we never re-implement those.

Architecture

┌──────────────────┐    /api/v1/*      ┌──────────────┐
│  SvelteKit web/  │ ───────────────▶  │  photoprism  │ ──▶ mariadb
│  (Vite : 5173)   │    /api/sidecar/* │   :2342      │
│                  │ ─────────┐        └──────────────┘
└──────────────────┘          ▼
                      ┌──────────────┐
                      │  sidecar     │ ──▶ mariadb (mule_sidecar.*)
                      │  :8000       │ ──▶ originals FS (rename / folders / dups)
                      └──────────────┘

Three compose services — mariadb, photoprism, sidecar — plus the SvelteKit web/ app served separately. PhotoPrism's port 2342 is bound to 127.0.0.1 only; it isn't a user-facing surface. The SvelteKit app is.

What the sidecar adds on top of PhotoPrism (full list in sidecar/README.md):

  • Per-photo marks (rating + color) persisted to mule_sidecar.marks
  • File rename + folder create/rename/delete with PhotoPrism reindex
  • Heap (album) → folder conversion
  • Perceptual-hash duplicate scan + archive

Quick start

cp .env.example .env
# edit .env: set PHOTO_DIRS to the host path holding your library
# rotate PP_ADMIN_PASSWORD, PP_DB_PASSWORD, PP_DB_ROOT_PASSWORD
# before any non-local deployment.

podman-compose --env-file .env \
  -f docker-compose.yml \
  -f docker-compose.podman.yml \
  up -d

Then serve the frontend. For local use the simplest path is the Vite dev server:

cd web
npm install
npm run dev
# open http://localhost:5173

For a static deployment, npm run build produces a bundle under web/build/ that any static file host (nginx, Caddy, GitHub Pages-style) can serve. Reverse-proxy /api/v1/* to http://127.0.0.1:2342 and /api/sidecar/* to http://127.0.0.1:8000.

PhotoPrism's own UI is still reachable from the host at http://127.0.0.1:2342 if you need admin features (user management, settings) — set up an SSH tunnel from your laptop if the server is remote.

Configuration

All knobs live in .env.example. The required ones:

Variable Notes
PHOTO_DIRS Host path mounted at /photoprism/originals. The library.
PP_ADMIN_PASSWORD First-boot admin password. Rotate.
PP_DB_PASSWORD MariaDB password for the photoprism user. Rotate.
PP_DB_ROOT_PASSWORD MariaDB root password. Rotate.
PP_UID / PP_GID Host UID/GID that owns PHOTO_DIRS. PhotoPrism + sidecar drop to this user inside.
PP_PORT Loopback host port for PhotoPrism (default 2342).
PP_ORIGINALS_MODE rw (default) or ro — see Read-only libraries.
SIDECAR_PORT Loopback host port for the sidecar (default 8000).

Sidecar-specific env (DB DSN, USER_BASEPATHS, etc.) is documented in sidecar/README.md.

Read-only libraries

The default originals mount is :rw because file operations (rename, folder mutations, duplicate archive, heap convert) need to mutate the filesystem. To run against a read-only archive, set PP_ORIGINALS_MODE=ro in .env. Browsing, marks, ratings, and color labels still work; the following sidecar endpoints return an OS error:

  • POST /api/sidecar/files/:uid/rename
  • POST /api/sidecar/folders / :rel/rename / DELETE /:rel
  • POST /api/sidecar/albums/:uid/convert
  • POST /api/sidecar/duplicates/archive

PhotoPrism's PHOTOPRISM_READONLY is controlled separately by PP_READONLY and gates its own backwrite / import paths.

Dev iteration loop

For fast iteration on the sidecar without rebuilding its image on every change, run it as a host process — bring up just mariadb and photoprism from compose, then build and run the Go binary locally. Full instructions in sidecar/README.md.

Layout

.
├── docker-compose.yml         base stack: mariadb + photoprism + sidecar
├── docker-compose.podman.yml  rootless-podman overlay (keep-id mapping)
├── .env.example               required env vars (copy to .env)
├── mariadb/init/              first-boot SQL: creates mule_sidecar DB + user
├── pp/                        PhotoPrism bind-mounted state (storage, import)
├── sidecar/                   Go service — see sidecar/README.md
└── web/                       SvelteKit frontend
Description
No description provided
Readme 13 MiB
Languages
Svelte 54.6%
TypeScript 26.5%
Go 18.2%
CSS 0.4%
Dockerfile 0.1%
Other 0.1%