feat(admin): a Pending Deletes panel in the Libraries tab - #883
Merged
Merged
Conversation
This was referenced Sep 21, 2026
ajslater
added this pull request to stack #891
September 21, 2026 16:20
ajslater
force-pushed
the
feat/pending-deletes-admin
branch
from
September 21, 2026 16:20
892ff2b to
256e092
Compare
ajslater
force-pushed
the
feat/pending-deletes-admin
branch
from
September 21, 2026 16:21
256e092 to
f4e5da4
Compare
ajslater
force-pushed
the
feat/pending-deletes-admin
branch
from
September 21, 2026 16:22
f4e5da4 to
d673d42
Compare
ajslater
force-pushed
the
feat/pending-deletes-admin
branch
from
September 21, 2026 16:22
d673d42 to
0678249
Compare
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
force-pushed
the
feat/pending-deletes-admin
branch
from
September 21, 2026 16:23
0678249 to
8f8c0fd
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements P1f of
tasks/followups-implementation-plan.md§4.9, per decision D10 (actionable, not merely visible). Last of the pending-delete series.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_staffexemption 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/*.pyreturns exactly one hit, andAdminFolderListViewlists filesystem directories viaos.scandir, notFolderrows.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.vuebinds nothing and forwards by attribute fallthrough, so expand slots through two$attrslayers 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
AdminSectionheader-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:
AdminAPIView, not a JSON:API resource viewset. There is no single model to be the resource.test_revive_needs_the_collection_toopins it: with only the folder stamped,POST …/comics/<same pk>/reviveis a 404 and the folder keeps its stamp.reap_afteris 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
AdminAPIViewshapeAdminCustomCoverDeleteViewalready 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.pyand is called by both the poller's automatic revival and the admin's manual one, so they cannot diverge.PENDING_DELETE_WINDOWmoves there too, so the reaper that enforces the deadline and the endpoint that displays it read the same value.The view enqueues
UpdateCollectionsTaskfor the parent re-stamp rather than runningTimestampUpdaterinline — a view has no write lock.Plumbing
New
PENDING_DELETES_CHANGEDnotification toChannelGroups.ADMIN, its payload-map entry, and asocket.jscase that reloads the panel and the library table together.missing_countis a correlated index-only subquery in the same shape as the existingcomic_count/failed_count.make build-choicesregenerateswebsocket-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 computedreapAfter, 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 themissingCountannotation.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 tyclean. Fullmake testgreen: 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_forcecannot recover a stamped library — it setsstat[8] = 0.0on 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