120 mule-images: Phase 1 + 2 NC integration (thumbnail proxy + webhook receiver)
- Worker-watcher retired; file events come from NC webhook_listeners.
- Thumbnails proxy /index.php/core/preview keyed by photos.nextcloud_fileid.
- backfill_gps auto-trigger killed (was queueing ~60k tasks per deploy).
- Range support added to /api/v1/photos/{id}/original so .mov plays.
- NC cron tightened to */1 for ~60s webhook latency.
This commit is contained in:
@@ -19,17 +19,27 @@ Hosts `mule-image` / "mulita" — the photos app at `photos.hubris.network`. Aut
|
||||
| frontend | 3000 | Reverse-proxied by Caddy |
|
||||
| backend | 8001 | FastAPI |
|
||||
| worker-vision | — | ML scan worker |
|
||||
| worker-light | — | Lightweight worker |
|
||||
| worker-watcher | — | FS watcher |
|
||||
| worker-light | — | Lightweight worker (+ celery beat scheduler since 2026-05-11) |
|
||||
| db | (pg) | pgvector |
|
||||
| redis | (rd) | queue |
|
||||
|
||||
`worker-watcher` was retired 2026-05-11 — file events come from NC webhook_listeners instead of watchfiles inotify. See "Nextcloud integration" below.
|
||||
|
||||
`.env` is **untracked** — `git 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_WEBHOOK_SECRET` (since 2026-05-11; bearer token NC presents on every webhook call to `/api/v1/internal/nc-webhook`)
|
||||
|
||||
## Nextcloud integration (since 2026-05-11)
|
||||
|
||||
Phase 1 + 2 of dropping the duplicated indexing work between mule and Nextcloud — see `~/.claude/projects/-root/memory/project_mule_nc_integration.md` for the full detail.
|
||||
|
||||
**Thumbnails (Phase 1, commit 576b0c2):** `GET /api/v1/photos/{id}/thumb/{size}` proxies `https://cloud.hubris.network/index.php/core/preview` keyed by `photos.nextcloud_fileid` (alembic 0018), authenticated with the photo owner's encrypted NC app password. NC handles JPEG/HEIC/TIFF/RAW/DNG/Movie previews natively. `X-Mule-Thumb-Source: nextcloud|disk` on every response. Worker now only writes the `medium` size to `/data/thumbs/{user}/{photo}/medium.webp` (the vision worker still loads it from disk); `small` + `large` are NC-proxied. Backfill script: `backend/scripts/backfill_nextcloud_fileid.py` — 17,591/17,591 photos filled.
|
||||
|
||||
**File events (Phase 2, commits 362fbc6 + f657e2c):** mule no longer runs `watchfiles`. NC's `webhook_listeners` app (already installed, registered via `backend/scripts/register_nc_webhooks.py`) POSTs every `NodeCreated|NodeWritten|NodeDeleted|NodeRenamed` event to `http://192.168.8.136:8001/api/v1/internal/nc-webhook` with `Authorization: Bearer $NEXTCLOUD_WEBHOOK_SECRET`. The endpoint dispatches the same `scan_folder` / `handle_file_deletion` machinery the watcher used. **NC dispatches webhooks asynchronously via its background-job queue** — `crontab -u www-data` on LXC 114 runs `cron.php` every minute (`*/1`), so end-to-end latency is ≲60 s. Re-running the registrar script is idempotent (it deletes existing webhooks pointing at the same URL first).
|
||||
|
||||
## Nextcloud-rooted libraries (since 2026-04-26)
|
||||
|
||||
@@ -80,6 +90,22 @@ For pushes from inside the LXC, gitea creds at `/etc/mule-deploy/git-credentials
|
||||
|
||||
## Changelog
|
||||
|
||||
### 2026-05-11 — Stop duplicating Nextcloud's indexing (Phase 1 + 2)
|
||||
|
||||
Two big architectural shifts in one session, both aimed at killing work mule was doing that NC already does for the same source files.
|
||||
|
||||
**Phase 1 — thumbnail proxy (commits `576b0c2`, `28738ac`).** Photos table gains `nextcloud_fileid` (migration `0018`). `GET /api/v1/photos/{id}/thumb/{size}` proxies NC's `/index.php/core/preview` keyed by that fileid, authenticated with the owner's encrypted app password (small=240, medium=640, large=1280). Worker now only writes the `medium` size to `/data/thumbs` (the vision worker still loads it from disk); `small` + `large` no longer touch disk. Disk fallback stays wired for legacy rows and the rare 404 from NC (iPhone JPEGs mis-extensioned as `.DNG` — verified). Existing 17,591 photos backfilled via `backend/scripts/backfill_nextcloud_fileid.py`. Tested with real CR2, real DNG, fake DNG, JPEG, HEIC — all green.
|
||||
|
||||
**Range support for `/original` (commit `18dce33`).** Pre-existing bug surfaced by Phase 1 testing: `<video>` couldn't play `.mov` because `FileResponse` was returning 200 with the whole body and no `Accept-Ranges` header, so browsers reported "format not supported." Now parses `Range: bytes=START-END`, returns 206 with `Content-Range`, streams in 1 MB chunks.
|
||||
|
||||
**Fix: backfill_gps was the actual CPU drain, not the watcher (commit `d24c64e`).** `_scan_all_source_roots_async` auto-queued `backfill_gps` 30 s after every container boot, which then re-queued `extract_metadata` for every photo with `latitude IS NULL` — ~60k pointless tasks per deploy, pinning worker-light at 180+% CPU for ~30 min. Killed the auto-trigger; manual `POST /api/v1/library/backfill-gps` still works.
|
||||
|
||||
**Phase 2 — webhook receiver replaces watchfiles (commits `362fbc6`, `f657e2c`).** New endpoint `POST /api/v1/internal/nc-webhook` (auth via `Authorization: Bearer $NEXTCLOUD_WEBHOOK_SECRET`, `hmac.compare_digest`) handles `NodeCreated|NodeWritten|NodeDeleted|NodeRenamed`. Maps NC's `/admin/files/...` path to the bind-mounted `/nextcloud-users/admin/files/...` and dispatches the same `scan_folder` / `handle_file_deletion` machinery the watcher used. Registered against NC via `backend/scripts/register_nc_webhooks.py` (idempotent: deletes existing webhooks targeting the same URL first). `mulita-worker-watcher` container retired; celery `--beat` folded into `worker-light` so `discard_missing_photos_beat` still fires. End-to-end verified by uploading a test JPEG through WebDAV — both `NodeCreated` and `NodeWritten` fire, mule receives + dispatches, 200 OK.
|
||||
|
||||
**NC cron tightened to `*/1` so webhook delivery latency drops from up to 5 min to ~60 s** (`crontab -u www-data` on LXC 114). NC dispatches webhook calls through its background-job queue; the cron interval = the worst-case latency. `*/5` was the default; `*/1` is the upstream recommendation anyway.
|
||||
|
||||
**Open: Phase 3.** Enable the Nextcloud Memories app (currently installed but disabled) and read `taken_at` / `lat` / `lon` / `w` / `h` from `oc_memories` instead of running mule's own `extract_metadata`. Keep pHash + file_hash for dedup. When that lands, the vision worker can also stop needing on-disk medium thumbs and the whole `/data/thumbs` pipeline retires.
|
||||
|
||||
### 2026-05-11 — Duplicates view shows parent folder + full-path tooltip
|
||||
|
||||
`GET /api/v1/library/duplicates/groups` now includes `filepath` in each member payload. `DuplicatesView` renders a dark bottom-strip on every thumbnail showing the last two folder segments — `…/<grandparent>/<parent>` (e.g. `…/files/Phone Photos starting 01-26`) via a `duplicatePathLabel` helper. Going up two levels avoids the failure mode where two copies sit under matching parent names (e.g. duplicate `2023/` subfolders under different archives) and the label would collapse. Full filepath surfaces through the native `title` tooltip on hover. Dimensions chip moves from bottom-left to top-left so the path strip can run edge-to-edge. `memberToPhoto` finally stops faking `filepath = filename` — the synthetic Photo it hands to `PhotoThumbnail` now carries the real path.
|
||||
|
||||
Reference in New Issue
Block a user