import { Alpine } from "./alpine"; import { showToast } from "./toast"; import { getToken } from "./storage"; // Processing Issues retry queue (2026-10). The "Retry all" button is a // CLIENT-side queue over the single-issue retry endpoint: one book in // flight at a time through the individual scanner (RescanMediaItem / // single-file import), never a whole-library rescan — that resource // trade is the whole point. Issue cards are templ-rendered; the queue // counter and per-card tint below are pure client-local state (the // sanctioned island shape; after the queue drains the templ partial // swap restores server truth). interface RetryResult { ok: boolean; message?: string; error?: string; } async function retryIssue(libraryId: string, issueId: string): Promise { const token = getToken(); let resp: Response; try { resp = await fetch(`/api/libraries/${libraryId}/issues/${issueId}/retry`, { method: "POST", headers: { Authorization: `Bearer ${token}` }, }); } catch (err) { return { ok: false, error: (err as Error).message }; } const body = (await resp.json().catch(() => ({}))) as { success?: boolean; message?: string; error?: string; }; return { ok: resp.ok, message: body.message, error: body.error }; } Alpine.data("processingIssues", () => ({ queueRunning: false, queueDone: 0, queueTotal: 0, libraryId: "", init(): void { this.libraryId = (this.$el as HTMLElement).dataset.libraryId || ""; }, issueIdsOnPage(): string[] { return Array.from(document.querySelectorAll("#issues-content [data-issue-id]")) .map((el) => el.dataset.issueId || "") .filter((id) => id !== ""); }, async retryOne(issueId: string): Promise { await this.runQueue([issueId]); }, async retryAll(): Promise { await this.runQueue(this.issueIdsOnPage()); }, async runQueue(issueIds: string[]): Promise { if (this.queueRunning || issueIds.length === 0 || !this.libraryId) return; this.queueRunning = true; this.queueDone = 0; this.queueTotal = issueIds.length; let failed = 0; for (const issueId of issueIds) { const card = document.querySelector( `#issues-content [data-issue-id="${CSS.escape(issueId)}"]`, ); card?.classList.add("issue-retrying"); const result = await retryIssue(this.libraryId, issueId); card?.classList.remove("issue-retrying"); if (result.ok) { card?.classList.add("issue-retry-done"); } else { failed++; card?.classList.add("issue-retry-failed"); showToast(result.error || `Retry failed for ${issueId.slice(0, 8)}`, "error"); } this.queueDone++; } this.queueRunning = false; if (failed === 0) { showToast( `Retried ${issueIds.length} item${issueIds.length === 1 ? "" : "s"} successfully`, "success", ); } else { showToast( `Retried ${issueIds.length - failed} of ${issueIds.length}; ${failed} still failing`, "warning", ); } this.refreshList(); }, // Server truth after the queue drains: resolved issues drop off the // ledger. Swap the templ partial (rendering ownership: the list is // server data), falling back to a reload when htmx is unavailable. refreshList(): void { if (window.htmx) { window.htmx .ajax("GET", `/admin/libraries/${this.libraryId}/issues/content`, { target: "#issues-content", swap: "outerHTML", }) .catch(() => window.location.reload()); } else { window.location.reload(); } }, }));