chore(web): library-admin inline script moves to admin.ts; rendering guardrail

The library admin page carried an inline <script> block of global
functions (toggleLibraryPanel, openEditModal, openDeleteModal,
openFolderBrowser plus the delete-modal close listener). They move into
the admin Alpine component as typed methods; the page body gains
x-data="admin" and the call sites switch onclick= -> @click.
Behavior unchanged.

Guardrail: make lint-rendering (wired into make test) fails when a
web/src file builds more HTML template literals than its recorded
baseline (web/src/rendering-allowlist.txt, seeded at 18 sites across 9
files post-twin-deletion) or when a .templ file gains an inline script
block. theme_flash_guard.templ is the only sanctioned inline script;
web/src/reader is a sanctioned island.

theme.ts: Array.from(classList) — DOMTokenList spread needs
lib.dom.iterable, which the tsconfig does not include.
This commit is contained in:
John O'Keefe committed 2026-10-18 12:28:50 -04:00
1 parent f1844591b4
commit 506a04b55d
6 files changed
+164 -94

No files matched your search

+76
View File
@@ -450,10 +450,86 @@ export {
stopScanStatusPolling,
};
// Library admin page helpers (rendering-ownership: these orchestrate htmx
// swaps and modal visibility on server-rendered DOM; they render nothing).
function toggleLibraryPanel(btn: HTMLElement): void {
const libId = btn.dataset.libId || "";
const panel = document.getElementById(`library-panel-${libId}`);
if (!panel) return;
if (panel.innerHTML.trim() !== "") {
panel.innerHTML = "";
} else {
window.htmx.ajax("GET", `/admin/library/${libId}/panel`, {
target: `#library-panel-${libId}`,
swap: "innerHTML",
});
}
}
function openEditModal(id: string, name: string, description: string): void {
const form = document.getElementById("edit-library-form");
if (!form) return;
form.setAttribute("hx-put", `/admin/library/${id}`);
const nameInput = document.getElementById("edit-library-name");
if (nameInput) (nameInput as HTMLInputElement).value = name;
const descInput = document.getElementById("edit-library-desc");
if (descInput) (descInput as HTMLInputElement).value = description;
window.htmx.process(form);
document.getElementById("edit-library-modal")?.classList.remove("hidden");
}
function openDeleteModal(id: string, name: string): void {
const btn = document.getElementById("delete-library-confirm");
if (!btn) return;
btn.setAttribute("hx-delete", `/admin/library/${id}`);
btn.setAttribute("hx-target", "#libraries-container");
btn.setAttribute("hx-swap", "innerHTML");
const nameEl = document.getElementById("delete-library-name");
if (nameEl) nameEl.textContent = name;
window.htmx.process(btn);
document.getElementById("delete-library-modal")?.classList.remove("hidden");
}
function openFolderBrowser(targetInputId: string, libraryId: string): void {
const content = document.getElementById("folder-browser-content");
if (!content) return;
content.setAttribute("hx-get", "/admin/library/browse");
content.setAttribute(
"hx-vals",
JSON.stringify({ target_input: targetInputId, library_id: libraryId }),
);
content.setAttribute("hx-trigger", "load");
window.htmx.process(content);
document.getElementById("folder-browser-modal")?.classList.remove("hidden");
}
// Close the delete-library modal after a successful delete: the htmx swap
// already removes the deleted row from the libraries list, but the modal
// lives outside the swapped container and would otherwise stay open.
function bindDeleteLibraryModalClose(): void {
document
.getElementById("delete-library-confirm")
?.addEventListener("htmx:afterRequest", function (evt) {
const detail = (evt as CustomEvent).detail as { successful?: boolean };
if (detail.successful) {
document
.getElementById("delete-library-modal")
?.classList.add("hidden");
}
});
}
Alpine.data("admin", () => ({
init() {
bindDeleteLibraryModalClose();
},
checkActiveScans,
hideScanProgress,
loadWatchStatus,
scanAllLibraries,
stopScanStatusPolling,
openDeleteModal,
openEditModal,
openFolderBrowser,
toggleLibraryPanel,
}));