Files
oikos/containers/120-mule-images.md
Claudio 79d769a8a6 docs(mule-image): editable/readonly split in right sidebar
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 21:50:47 +02:00

11 KiB
Raw Blame History

120 — mule-images

Hosts mule-image / "mulita" — the photos app at photos.hubris.network. Auto-deploys from dtoro/mule-image on git push origin main.

At a glance

  • Hostname: mule-images
  • IP: 192.168.8.136
  • Privilege: privileged
  • Resources: 4 cores / 8 GiB RAM / 60 GiB rootfs
  • Mounts: /mnt/library/mnt/library
  • Public hostname: photos.hubris.networkcaddy:3000 (frontend)

Stack (/opt/mule-image)

/opt/mule-image IS the working tree of dtoro/mule-image. Compose at /opt/mule-image/docker-compose.yml. Services:

Service Port Notes
frontend 3000 Reverse-proxied by Caddy
backend 8001 FastAPI
worker-vision ML scan worker
worker-light Lightweight worker
worker-watcher FS watcher
db (pg) pgvector
redis (rd) queue

.env is untrackedgit checkout .env will wipe it. Holds:

  • PHOTO_DIRS=/mnt/library/images/
  • NEXTCLOUD_USERS_HOST_PATH=/mnt/library/homecloud
  • NEXTCLOUD_BASE_URL=https://cloud.hubris.network
  • OIDC client secret + scopes
  • SECRET_KEY (generated)

Nextcloud-rooted libraries (since 2026-04-26)

Photo libraries live under each user's Nextcloud files/ tree, NOT in /mnt/library/images/*.

  • /mnt/library/homecloud is bind-mounted into backend, worker-light, worker-watcher, worker-vision as /nextcloud-users. Each NC user is /nextcloud-users/<nc_user>/files/.
  • Reads use that bind directly.
  • Mutations (upload, delete, rename, move) dispatch through services/nextcloud_dav.py (HTTP Basic auth, per-user app password Fernet-encrypted in users.nextcloud_app_password_enc) so Nextcloud's oc_filecache, trashbin, comments, and desktop-sync clients stay coherent.
  • Photo copy + cross-system moves return 501 with a "use Nextcloud's web UI" hint — defer until needed.
  • users.nextcloud_username overrides the default OIDC preferred_username. dtoro (mule-image) maps to admin (Nextcloud) — don't assume username equality.
  • Surviving SourceRoots in DB: Photos/nextcloud-users/admin/files/Photos; Memories/nextcloud-users/admin/files/Memories (both owned by dtoro). User muli has nextcloud_username=muli backfilled but no SourceRoot yet.
  • Pre-migration DB dump: /root/snapshots/mulita-pre-nc-migration-20260426-075132.dump (11 MB) on the host.

Authentication (since 2026-04-22)

Native OIDC via Authentik. Code in backend/app/auth_oidc.py, routes /api/v1/auth/oidc/{login,callback}. Authentik side:

  • OAuth2/OIDC Provider, client ID fCuHew48ONTskDjUKnMTZjFbVXuHwvQqTScQRNQ1
  • App slug mule-image
  • Redirect URI: https://photos.hubris.network/api/v1/auth/oidc/callback

Backend container needs extra_hosts: auth.hubris.network:192.168.8.175 via docker-compose.override.yml (gitignored). Otherwise Authlib's metadata fetch fails with SSL: CERTIFICATE_VERIFY_FAILED: self-signed certificate (it ends up at a random public host because LXC DNS resolves the public IONOS A record).

Caddyfile stays plain reverse_proxy 192.168.8.136:3000 — no forward-auth, no /api/* bypass needed.

Auto-deploy

Push to dtoro/mule-image main → gitea webhook → http://192.168.8.136:9797/deploymule-deploy-webhook.service:

  • Validates HMAC against /etc/mule-deploy/secret
  • Filters to refs/heads/main
  • Runs /opt/mule-deploy/deploy.sh in a daemon thread (returns 202 immediately — docker builds exceed gitea's request timeout)
  • git pull --ff-only + docker compose up -d --build + docker image prune -f

Deploy tooling is outside the app repo: /opt/mule-deploy/{deploy.sh,webhook.py}, secret at /etc/mule-deploy/secret, unit at /etc/systemd/system/mule-deploy-webhook.service. Same shape as the Caddy + Artifacto pipelines. Gitea webhook id 6.

app.ini ALLOWED_HOST_LIST on gitea includes 192.168.8.136.

Logs: pct exec 120 -- journalctl -u mule-deploy-webhook -f. Manual deploy: pct exec 120 -- /opt/mule-deploy/deploy.sh.

For pushes from inside the LXC, gitea creds at /etc/mule-deploy/git-credentials (mode 600) — same token as /etc/caddy-deploy/git-credentials on caddy.

Changelog

2026-05-10 — right sidebar restructure (heap pinned, single scroll, collapsible Metadata)

The right sidepanel had three stacked flex regions: <ActiveHeapCard />, <Header />, and <PhotoInfoPanel /> — with PhotoInfoPanel carrying its own internal scroll. That left the editable fields (filename, title, notes, rating, color, flag) stuck above the readonly metadata scroll, effectively two scroll boundaries on one sidebar.

  • Moved the scroll boundary up to RightSidebar.tsx: only ActiveHeapCard and Header stay pinned now. Editable + readonly content scroll together in one flex-1 overflow-y-auto region beneath them.
  • PhotoInfoPanel.tsx dropped its h-full / inner flex-1 overflow-y-auto.
  • The four readonly sections (Tags / Basic Info / Camera / Location) are now wrapped in a single outer "Metadata" Collapsible. Default expanded, one click hides the whole block. Sub-sections stay individually collapsible.
  • Second pass: the editable form (filename / title / notes / rating / color / flag) got the same treatment under an outer "Edit" Collapsible so the panel is now two equal collapsible groups below the title strip. Dropped the X (clear-selection) button from the Header; Esc and grid-empty-area-click still clear.
  • Third pass: split editable vs read-only between the two groups consistently. TagsEditor and TakenAtEditor had been buried inside the readonly Metadata sub-sections — Tags as its own Section, taken-at wedged into Basic Info between size/dims and the filepath. Moved both into the Edit collapsible, ordered identification → description → categorization: Filename · Title · Date Taken · Notes · Tags · Rating · Color · Flag. Metadata now holds only readonly: Basic Info (size, dims, path), Camera, Location.

PreviewView reuses RightSidebar under an <aside class="overflow-hidden">, so the change applies in both the grid sidebar and the preview overlay.

2026-05-10 — photos.hubris.network perf sweep

User reported slow load. Five fixes shipped, in order:

  1. Backend out of dev mode. docker-compose.yml command: was running uvicorn … --reload (single worker, file-watcher). Page loads fan out ~15 parallel API calls; they all serialized on one event loop. → --workers 2 --proxy-headers. Two uvicorn worker procs now.
  2. iPhone Apple ProRAW / Linear DNG decode fixed. rawpy 0.26.1 + LibRaw 0.22 rejects Apple Linear DNG (Photometric Interpretation: Linear Raw, 8-bit) as "Unsupported file format or not RAW file". Those files are TIFF containers with developed RGB inside and have no embedded preview to fall back to. Added PIL Image.open(src_path) as the last fallback in both _generate_proxy_webp (routers/photos.py) and process_raw_image (tasks/thumbs.py). ~1,300 iPhone DNGs were 415-ing on every detail view; now decode in <1s via PIL.
  3. Reconcile DB with disk (renamed-folder case). prune_missing_photos was skipping all 4,154 orphaned photo rows under SourceRoot Taco and Muli - 2024 onward (renamed in Nextcloud to Photo Archive 2004-2024) because the leaf path didn't resolve and the code assumed "drive unmounted → must skip". Added _sr_state() to classify as present / renamed (parent mount fine, leaf gone) / unmounted (parent inaccessible). Only unmounted still skips. Two stale source roots logged with a clearer hint pointing at POST /api/v1/library/maintenance/prune-missing. User has not yet been asked to run that — endpoint is ready when they are.
  4. Frontend page size + idle polling. usePhotosQuery.ts was first-fetching per_page=500 (slow paint + 500 thumb requests at once). Split into PER_PAGE_INITIAL=100 for first paint, PER_PAGE_BACKGROUND=500 for the cursor-chain prefetch. Idle polling for scan-status and worker-status (useScanActivity.ts, ScanProgress.tsx) bumped from 10s/15s to 30s/30s while idle; active cadence (2s/3s) unchanged.
  5. Partial index on photos. Default list query WHERE NOT is_trashed AND NOT is_hidden ORDER BY taken_at DESC NULLS LAST, id DESC LIMIT N was doing a seq-scan + top-N heapsort (~25ms standalone, worse under concurrency). Added migration 0017_photos_list_index creating ix_photos_list_visible (partial index on the sort key, restricted to visible rows). EXPLAIN now shows an Index Only Scan → 24.7ms → 0.097ms, ~250× speedup.

Deploy gotcha (repeated). The docker compose up -d --build in deploy.sh does not recreate the backend when only runtime config (command: or env-only changes) or migration files change without changing the Dockerfile-bake outcome. Forced recreation with docker compose up -d --no-deps --force-recreate backend twice this session. Action item: add --force-recreate (or at least --force-recreate backend when backend files changed) to /opt/mule-deploy/deploy.sh so future deploys are reliable.

Data drift still outstanding. 4,154 photo rows + 1 unregistered folder (Photo Archive 2004-2024) on disk that's not a SourceRoot. The reconcile endpoints now work — user decides when to call them. The new folder needs to be added as a SourceRoot via the Settings UI before its files will be indexed.

Proxy cache still empty (mule-image_proxies_data volume is 4 KB). Pre-generating ~5001,500 WebP proxies for non-web-safe formats would make first-open of every RAW/HEIC photo instant. Deferred — needs a one-shot script and the disk-space tradeoff isn't worth it until the data-drift reconcile happens first.

2026-04-28 — wiki entry created

Initial documentation.

2026-04-26 — Nextcloud-rooted libraries shipped

Bind /mnt/library/homecloud into the workers, reads via filesystem, writes via WebDAV. users.nextcloud_username override field added; dtoro → admin mapping. Surviving SourceRoots cleaned up to NC paths.

2026-04-22 — native OIDC via Authentik

Authlib-based code in backend/app/auth_oidc.py. extra_hosts override for auth.hubris.network in compose override (gitignored).

2026-04-21 — auto-deploy pipeline shipped

Webhook receiver at :9797, async deploy returning 202. Mirrors caddy-conf / gitea-customizations.