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:
@@ -289,6 +289,67 @@ async function purgeArchivedItems(): Promise<void> {
|
||||
|
||||
(window as any).purgeArchivedItems = purgeArchivedItems;
|
||||
|
||||
// Restore a hidden/archived item to library visibility without waiting for
|
||||
// its file to return (POST /api/media-items/:id/unarchive). If the file is
|
||||
// still gone the next scan hides it again.
|
||||
async function unarchiveItem(id: string): Promise<void> {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
try {
|
||||
const resp = await fetch(`/api/media-items/${id}/unarchive`, {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
throw new Error(err.error || "Failed to restore item");
|
||||
}
|
||||
showToast("Item restored to libraries", "success");
|
||||
setTimeout(() => window.location.reload(), 500);
|
||||
} catch (e) {
|
||||
showToast(e instanceof Error ? e.message : "Failed to restore item", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// Permanently delete one archived item and its reading history
|
||||
// (DELETE /api/media-items/:id).
|
||||
async function deleteArchivedItem(id: string): Promise<void> {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
if (
|
||||
!window.confirm(
|
||||
"Permanently delete this item? Its reading progress, notes, and highlights will be lost.",
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const resp = await fetch(`/api/media-items/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
throw new Error(err.error || "Failed to delete item");
|
||||
}
|
||||
showToast("Item deleted", "success");
|
||||
setTimeout(() => window.location.reload(), 500);
|
||||
} catch (e) {
|
||||
showToast(e instanceof Error ? e.message : "Failed to delete item", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// The archived-items page renders per-row buttons with data attributes
|
||||
// (escaping-safe); bind them here. Module scripts run after DOM parse.
|
||||
document.querySelectorAll<HTMLElement>("[data-unarchive]").forEach((el) => {
|
||||
el.addEventListener("click", () => unarchiveItem(el.dataset.unarchive || ""));
|
||||
});
|
||||
document.querySelectorAll<HTMLElement>("[data-delete-archived]").forEach((el) => {
|
||||
el.addEventListener("click", () =>
|
||||
deleteArchivedItem(el.dataset.deleteArchived || ""),
|
||||
);
|
||||
});
|
||||
|
||||
export {
|
||||
hideScanProgress,
|
||||
loadWatchStatus,
|
||||
|
||||
Reference in New Issue
Block a user