feat(admin): archived-items page with purge dates, restore, and purge

Admins need to see what the archive lifecycle is holding: a dedicated
/admin/archived page listing every hidden item (archived or still in
the missing grace window) with library, file path, status, and - when
retention is enabled - the exact date it will be permanently deleted
(archived_at + ARCHIVE_RETENTION_DAYS).

Per row: Restore (POST /api/media-items/:id/unarchive, clears the
archive state so it reappears; if the file is still gone the next scan
hides it again) and Delete (existing DELETE endpoint for single-row
purge with its reading history). Purge All Archived reuses the existing
bulk button. The library admin banner links to the page and keeps its
purge button; row actions use data attributes with delegated listeners
in admin.ts since this templ version has no JSFunctionCall helper.

Verified live: page 200 with purge dates shown, unarchive returned 204
and reset the row, per-item delete and bulk purge ({purged:1}) both
removed their rows with no leftovers.
This commit is contained in:
John O'Keefe
2026-09-13 12:01:05 -04:00
parent fe5ab9e5f8
commit aac7c72900
8 changed files with 529 additions and 43 deletions
+24
View File
@@ -1330,6 +1330,30 @@ func (mh *MediaHandler) UpdateMediaItem(c *echo.Context) error {
return c.JSON(http.StatusOK, item)
}
// UnarchiveMediaItem handles POST /api/media-items/:id/unarchive (admin
// only). Manually restores a hidden/archived item to library visibility
// without waiting for its file to return. If the file is still gone, the
// next scan hides it again.
func (mh *MediaHandler) UnarchiveMediaItem(c *echo.Context) error {
user := MustGetAuthenticatedUser(c)
if user.Role != "admin" {
return c.JSON(http.StatusForbidden, map[string]string{"error": "admin access required"})
}
mediaID := c.Param("id")
mediaUUID, err := uuid.Parse(mediaID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
}
if err := mh.db.ClearMediaItemArchive(c.Request().Context(), pgtype.UUID{Bytes: mediaUUID, Valid: true}); err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.NoContent(http.StatusNoContent)
}
// PurgeArchivedMediaItems handles POST /api/media-items/purge-archived
// (admin only). Hard-deletes every archived item (files missing from disk for
// 2+ scans) together with its reading history. The archive retention window