From 21423d9f2df56bf33a551ebc06d0316b3279c6d0 Mon Sep 17 00:00:00 2001 From: Claudio Date: Sun, 10 May 2026 22:23:50 +0200 Subject: [PATCH] docs(mule-image): hard-delete SourceRoot + 30-min discard catch-up Trash icon is now an actual hard delete (SourceRoot + Folders + Photos, Nextcloud files untouched). Watcher's 5-minute soft-time- limit kill is fixed. Beat schedule fires discard_missing_photos every 30 min as a safety net for events the real-time watcher might miss. Co-Authored-By: Claude Opus 4.7 (1M context) --- containers/120-mule-images.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/containers/120-mule-images.md b/containers/120-mule-images.md index 88d7aa8..15bdea7 100644 --- a/containers/120-mule-images.md +++ b/containers/120-mule-images.md @@ -80,6 +80,16 @@ For pushes from inside the LXC, gitea creds at `/etc/mule-deploy/git-credentials ## Changelog +### 2026-05-10 — Hard-remove Nextcloud SourceRoot + reliable delete sync + +`DELETE /api/v1/nextcloud/source-roots/{id}` was a soft-deactivate (`is_active=false`) — the trash icon in Settings only hid the SourceRoot from active queries while leaving every Folder + Photo row in the DB forever. Re-adding the same path resurrected ghosts; `prune-missing` reported zero deletes for the soft-removed entry because the cleanup code skipped inactive source roots. + +Endpoint now hard-deletes the SourceRoot, every Folder under it (chunked at 500), and every Photo in those folders. `photo_tags` and `heap_photos` cascade automatically via `ON DELETE CASCADE` on the join tables. `FolderShare` uses a stringly-typed `folder_id` (no FK) so cleaned manually for both `folder_type='folder'` and `folder_type='source_root'`. Returns `{deleted_photos, deleted_folders}` so the UI can toast a count. Files in Nextcloud are untouched. + +Sync side: the `watch_folders` celery task in `backend/app/tasks/scan.py` already detected filesystem deletions and soft-marked photos `is_discarded=true`, but the global `task_soft_time_limit=300` in `app/tasks/celery.py` was killing the watcher every five minutes and dropping every FS event during the restart window. The `soft_time_limit=None` on the decorator was being interpreted as "use worker default" rather than "unlimited". Override to `soft_time_limit=0, time_limit=0` (Celery convention for unbounded). Backstop: a new `discard_missing_photos()` in `app/services/cleanup.py` — soft variant of `prune_missing_photos`, walks every `_sr_state == 'present'` source root and flips `is_discarded=true` on Photo rows whose file is gone. Wired as `discard_missing_photos_beat` celery task scheduled every 30 minutes via `beat_schedule` on the celery app. Beat runs in-process on `worker-watcher` (`--beat` flag in `docker-compose.yml`) — the watcher is already a Redis-locked singleton so no need for a separate beat container. + +Manual `POST /api/v1/library/maintenance/prune-missing` remains the hard-delete path for when the user wants to permanently drop orphan rows; the new beat job only soft-discards (file gone → mule-image trash, restorable). + ### 2026-05-10 — OIDC auto-redirect on LoginPage `OIDC_ENABLED=true` was already set in `.env`, so the LoginPage rendered a "Sign in with Authentik" button next to the password form. With a single trusted IdP and a logged-in Authentik session, that extra click was friction without upside.