Skip to content

feat(admin): a Pending Deletes panel in the Libraries tab - #883

Merged
ajslater merged 1 commit into
developfrom
feat/pending-deletes-admin
Sep 21, 2026
Merged

ajslater merged 1 commit into
developfrom
feat/pending-deletes-admin

Conversation

@ajslater

Copy link
Copy Markdown
Owner

Implements P1f of tasks/followups-implementation-plan.md §4.9, per decision D10 (actionable, not merely visible). Last of the pending-delete series.

Stacked on #882#881#880#879. Retarget as each merges.

Why it is not optional

With #879#882 alone, an admin sees stamped rows in the ordinary browser rendered identically to live rows — the visibility filter exempts staff. They cannot tell which is which, cannot revive one, and cannot force an early delete. The is_staff exemption means the admin is the one person who cannot see the state the feature creates.

There was nothing to extend, either: rg "is_staff|admin" codex/serializers/browser/*.py returns exactly one hit, and AdminFolderListView lists filesystem directories via os.scandir, not Folder rows.

Shape

A panel below Failed Imports in the Libraries tab, plus a conditional per-library count column.

Not a new tab — it would be empty and confusing on the overwhelming majority of installs. Not a per-row expandable sub-table — admin-table.vue binds nothing and forwards by attribute fallthrough, so expand slots through two $attrs layers would be a real refactor of a component Users, Groups and Custom Covers also use.

Failed Imports is the proven pattern and the conceptual sibling: "on disk, could not import" next to "not on disk, not deleted yet". Copied wholesale — anchor/scroll behaviour, the AdminSection header-actions slot, non-emptiness gating — and the two hints cross-reference each other.

Two models, one list

Comic and Folder both carry stamps, which drives three decisions:

  • Envelope AdminAPIView, not a JSON:API resource viewset. There is no single model to be the resource.
  • Revive takes a collection and a pk. A comic and a folder can share a pk, so the pk alone does not identify a row. test_revive_needs_the_collection_too pins it: with only the folder stamped, POST …/comics/<same pk>/revive is a 404 and the folder keeps its stamp.
  • reap_after is computed server-side so one definition of the window exists rather than the client re-deriving it.

Verbs

"Reap now" needed no new endpoint. Registering the janitor job in #882 already made it POST-able through the existing POST /admin/tasks/run + librarianTask(...) store action, exactly as Poll and Force Update work from the library rows — which gets the Jobs-tab entry, the sidebar progress row and the abort plumbing for free.

"Keep" does need one, on the per-pk AdminAPIView shape AdminCustomCoverDeleteView already uses. It is the answer to "I know this is coming back on its own schedule, stop counting down."

A refactor that prevents drift

The revival follow-up — remove-then-recreate covers, clear the cache, broadcast — now lives in codex/librarian/pending_deletes.py and is called by both the poller's automatic revival and the admin's manual one, so they cannot diverge. PENDING_DELETE_WINDOW moves there too, so the reaper that enforces the deadline and the endpoint that displays it read the same value.

The view enqueues UpdateCollectionsTask for the parent re-stamp rather than running TimestampUpdater inline — a view has no write lock.

Plumbing

New PENDING_DELETES_CHANGED notification to ChannelGroups.ADMIN, its payload-map entry, and a socket.js case that reloads the panel and the library table together. missing_count is a correlated index-only subquery in the same shape as the existing comic_count / failed_count. make build-choices regenerates websocket-messages.json.

The Missing column is amber, not red, and appears only when something is pending — a pending delete is a warning with a way out, not a failure.

Tests

pytest (tests/test_admin_pending_deletes.py, 10 cases): empty list, a stamped comic's full row shape including the computed reapAfter, comics and folders sharing one list, revive clearing the stamp, the collection+pk pairing, 404s for a live row and an unknown collection, and 403s for a non-admin on both read and write. Plus the missingCount annotation.

vitest (pending-deletes-panel.test.js, 5 cases): hidden when empty, a row per pending delete, Keep passing the row's own collection and pk, Delete Expired Now going through the ordinary task action, and the hint's content.

make fix && make lint && make ty clean. Full make test green: 1304 pytest (+10), 587 vitest (+5).

NEWS, under Features: "Admins can see and manage comics that disappeared from a library but have not been deleted yet."

Carried forward from #882

poll_force cannot recover a stamped library — it sets stat[8] = 0.0 on every row and skips the stale-stat refresh, so every path reads as modified, never as revived. The panel's Keep button is the workaround an admin has today; teaching the force path to run the unstamp pass is the real fix and deserves its own change.

🤖 Generated with Claude Code

@ajslater
ajslater added this pull request to stack #891 September 21, 2026 16:20
@ajslater
ajslater force-pushed the feat/pending-deletes-admin branch from 892ff2b to 256e092 Compare September 21, 2026 16:20
@ajslater
ajslater force-pushed the feat/pending-deletes-admin branch from 256e092 to f4e5da4 Compare September 21, 2026 16:21
@ajslater
ajslater force-pushed the feat/pending-deletes-admin branch from f4e5da4 to d673d42 Compare September 21, 2026 16:22
@ajslater
ajslater force-pushed the feat/pending-deletes-admin branch from d673d42 to 0678249 Compare September 21, 2026 16:22
Base automatically changed from feat/pending-deletes-reaper to develop September 21, 2026 16:23
The visibility filter exempts staff, so without this an admin sees
stamped rows in the ordinary browser rendered identically to live ones:
they cannot tell which is which, cannot put one back, and cannot force
an early delete. The admin is otherwise the one person who cannot see
the state the feature creates.

A panel below Failed Imports rather than a new tab -- it would be empty
and confusing on the overwhelming majority of installs -- and not a
per-row expandable sub-table, which would mean refactoring a component
Users, Groups and Custom Covers also use. Failed Imports is the proven
pattern and the conceptual sibling: "on disk, could not import" next to
"not on disk, not deleted yet". They cross-reference each other.

Comic and Folder are two models rendered into one list, which is why
the backend is an envelope APIView rather than a JSON:API resource, and
why revive takes a collection as well as a pk -- a comic and a folder
can share one, so the pk alone does not identify a row. A test pins
that.

"Reap now" needed no new endpoint: registering the janitor job made it
POST-able through the existing tasks endpoint, which also gets the
Jobs-tab progress row and the abort plumbing for free. Only revive
needs its own route.

The revival follow-up -- covers, cache, broadcasts -- moves into a
shared module so the poller's automatic revival and the admin's manual
one cannot drift. The retention window constant moves there too, so the
reaper and the endpoint that displays the deadline read the same value.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ajslater
ajslater force-pushed the feat/pending-deletes-admin branch from 0678249 to 8f8c0fd Compare September 21, 2026 16:23
@ajslater
ajslater merged commit ed1c908 into develop Sep 21, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant