feat(admin): purge-archived endpoint with an archived-items banner

Bulk escape hatch for archived rows (files missing from disk for 2+
scans) so a mass external deletion never has to wait out the retention
window or be clicked away row by row:

- POST /api/media-items/purge-archived (admin only) hard-deletes all
  archived items and returns the purged count; reading history goes with
  the rows, so the call is confirmed in the UI first.
- The library admin page shows an 'Archived items: N' card (only when
  non-zero) with a Purge Archived Now button that calls the endpoint,
  toasts the result, and reloads.
- Frontend admin JS exposes window.purgeArchivedItems following the
  existing localStorage-bearer-token pattern.
This commit is contained in:
John O'Keefe
2026-09-12 17:50:24 -04:00
parent 14445a7c3f
commit cd119a74da
6 changed files with 388 additions and 277 deletions
+21
View File
@@ -1330,6 +1330,27 @@ func (mh *MediaHandler) UpdateMediaItem(c *echo.Context) error {
return c.JSON(http.StatusOK, item)
}
// 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
// eventually does the same automatically; this is the manual bulk escape hatch.
func (mh *MediaHandler) PurgeArchivedMediaItems(c *echo.Context) error {
user := MustGetAuthenticatedUser(c)
if user.Role != "admin" {
return c.JSON(http.StatusForbidden, map[string]string{"error": "admin access required"})
}
purged, err := mh.db.PurgeAllArchivedMediaItems(c.Request().Context())
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, map[string]interface{}{
"purged": len(purged),
})
}
// RescanMediaItem handles POST /api/media-items/:id/rescan (admin only)
func (mh *MediaHandler) RescanMediaItem(c *echo.Context) error {
user := MustGetAuthenticatedUser(c)