fix(admin): un-dead the archived page's purge and delete buttons
Release / build-and-push (push) Successful in 3m15s
Release / build-and-push (push) Successful in 3m15s
Two independent dead-ends, confirmed on prod's /admin/archived (2026-10): Purge All rendered but clicking it did nothing at all, and per-row Delete never even showed its confirm dialog. Purge All is Alpine-bound (@click="purgeArchivedItems()"), but this page's <body> lacked x-data="admin" — the only admin page without it. Alpine evaluates directives only inside an x-data tree, so the attribute was inert: the admin component (and its method, registered in admin.ts) never instantiated here. The body now carries x-data="admin", matching admin.templ and admin_library.templ. Per-item Delete was a second, subtler dead-end: admin.ts's API helpers guard with `if (!token) return;` BEFORE the confirm dialog, and the reporting session had no localStorage token — SSR pages run on the cookie session, so the web UI works while every Bearer- authenticated admin action (purge, delete, restore, merge, scan-all, cancel-scan) silently no-ops with zero feedback. Those guards now go through requireToken, which toasts "Session expired — sign out and back in, then retry" when the token is absent. The two passive page-load probes (loadWatchStatus, checkActiveScans) deliberately keep the silent return: a toast there would fire on every visit. Pinned by TestAdminArchivedPageHasAlpineScope: renders the page and asserts the body scope and the action buttons coexist in one render. After deploy: hard-refresh prod, click Purge All (confirm appears); if the token toast fires on any admin action, log out and back in once to stash a fresh API token.
This commit is contained in:
1 parent
795225f097
commit
f1e4b2a603
4 files changed
+76
-9
No files matched your search
+27
-7
@@ -1,8 +1,23 @@
|
||||
import { Alpine } from "./alpine";
|
||||
import { showToast } from "./toast";
|
||||
|
||||
async function scanAllLibraries(): Promise<void> {
|
||||
// Admin API calls authenticate with the Bearer token stashed at login,
|
||||
// while the SSR pages themselves run on the cookie session — so a valid
|
||||
// web session can still have an empty/stale localStorage token. User-
|
||||
// triggered actions must never die silently in that state (the
|
||||
// /admin/archived purge + delete buttons were invisible dead-ends: the
|
||||
// missing-token return fired before the confirm dialog): requireToken
|
||||
// fails loudly instead.
|
||||
function requireToken(): string | null {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) {
|
||||
showToast("Session expired — sign out and back in, then retry", "error");
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
async function scanAllLibraries(): Promise<void> {
|
||||
const token = requireToken();
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
@@ -225,7 +240,7 @@ function updateLibraryProgress(jobId: string, status: any): void {
|
||||
// aborts the walk at its next context check and skips the archive pass,
|
||||
// so cancelling never archives unvisited files.
|
||||
async function cancelScan(jobId: string): Promise<void> {
|
||||
const token = localStorage.getItem("token");
|
||||
const token = requireToken();
|
||||
if (!token) return;
|
||||
|
||||
const btn = document.querySelector<HTMLButtonElement>(`[data-cancel-job="${jobId}"]`);
|
||||
@@ -257,7 +272,7 @@ async function cancelScan(jobId: string): Promise<void> {
|
||||
// list so it also covers jobs this page never started (e.g. started
|
||||
// before a refresh), then cancels each one.
|
||||
async function cancelAllScans(): Promise<void> {
|
||||
const token = localStorage.getItem("token");
|
||||
const token = requireToken();
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
@@ -287,6 +302,8 @@ function setCancelAllVisible(visible: boolean): void {
|
||||
// widget and re-attach polling. Websocket start-events only reach pages
|
||||
// that were already open when the scan began.
|
||||
async function checkActiveScans(): Promise<void> {
|
||||
// Passive page-load probe, not a user action: stay silent when the API
|
||||
// token is absent (requireToken's toast would fire on every page visit).
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
try {
|
||||
@@ -369,6 +386,9 @@ function hideScanProgress(): void {
|
||||
}
|
||||
|
||||
async function loadWatchStatus(): Promise<void> {
|
||||
// Passive status fetch inside the admin component's init, not a user
|
||||
// action: stay silent when the API token is absent (requireToken's
|
||||
// toast would fire on every page visit).
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
|
||||
@@ -399,7 +419,7 @@ function stopScanStatusPolling(): void {
|
||||
// window for the library admin page's inline button; reading history is
|
||||
// deleted with the rows, so a confirm dialog guards it.
|
||||
async function purgeArchivedItems(): Promise<void> {
|
||||
const token = localStorage.getItem("token");
|
||||
const token = requireToken();
|
||||
if (!token) return;
|
||||
|
||||
if (
|
||||
@@ -431,7 +451,7 @@ async function purgeArchivedItems(): Promise<void> {
|
||||
// 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");
|
||||
const token = requireToken();
|
||||
if (!token) return;
|
||||
try {
|
||||
const resp = await fetch(`/api/media-items/${id}/unarchive`, {
|
||||
@@ -452,7 +472,7 @@ async function unarchiveItem(id: string): Promise<void> {
|
||||
// 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");
|
||||
const token = requireToken();
|
||||
if (!token) return;
|
||||
if (
|
||||
!window.confirm(
|
||||
@@ -481,7 +501,7 @@ async function deleteArchivedItem(id: string): Promise<void> {
|
||||
// same content hash (POST /api/media-items/:id/merge), then remove the
|
||||
// archived entry. Covers copy-then-delete-later moves between libraries.
|
||||
async function mergeArchivedItem(sourceId: string, targetId: string): Promise<void> {
|
||||
const token = localStorage.getItem("token");
|
||||
const token = requireToken();
|
||||
if (!token) return;
|
||||
if (
|
||||
!window.confirm(
|
||||
|
||||
Reference in new issue
Block a user