chore(frontend): slim admin.ts, remove dead library.ts

- Remove dead functions from admin.ts: loadSystemStats, renderSystemStats,
  triggerLibraryScan, triggerQuickScan, all WebSocket functions
- Remove library.ts (695 lines of innerHTML string-building replaced by
  HTMX server-rendered partials)
- Remove library import from main.ts
This commit is contained in:
2026-08-07 09:35:47 -04:00
parent 706be09dec
commit 006cc0c2c9
4 changed files with 4 additions and 837 deletions
+3 -140
View File
@@ -1,134 +1,5 @@
import { Alpine } from "./alpine";
import { showToast } from "./toast";
import { createWebSocket } from "./websocket";
function initializeScanWebSocket(): void {
createWebSocket({
onMessage: (message) => {
switch (message.type) {
case "scan_progress":
updateScanProgress(message.data);
break;
case "scan_complete":
showScanComplete(message.data);
break;
case "scan_error":
showScanError(message.data);
break;
}
},
enableReconnect: true,
reconnectDelay: 5000,
});
}
function updateScanProgress(data: { progress: number; files_scanned: number; new_items: number }): void {
const progressBar = document.getElementById("scan-progress-bar");
if (progressBar) {
progressBar.style.width = (data.progress * 100) + "%";
}
const progressText = document.getElementById("scan-progress-text");
if (progressText) {
progressText.textContent = `${data.files_scanned} files scanned (${data.new_items} new)`;
}
}
function showScanComplete(_data: unknown): void {
console.log("Scan complete:", _data);
}
function showScanError(_data: unknown): void {
console.error("Scan error:", _data);
}
async function triggerLibraryScan(): Promise<void> {
const token = localStorage.getItem("token");
if (!token) return;
try {
const response = await fetch("/api/libraries/scan", {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
});
if (response.ok) {
showToast("Library scan started", "success");
} else {
const error = await response.json();
showToast(error.error || "Failed to start scan", "error");
}
} catch (error) {
console.error("Scan error:", error);
showToast("Failed to start library scan", "error");
}
}
async function triggerQuickScan(): Promise<void> {
const token = localStorage.getItem("token");
if (!token) return;
try {
const response = await fetch("/api/libraries/quick-scan", {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
});
if (response.ok) {
showToast("Quick scan started", "success");
} else {
const error = await response.json();
showToast(error.error || "Failed to start quick scan", "error");
}
} catch (error) {
console.error("Quick scan error:", error);
showToast("Failed to start quick scan", "error");
}
}
async function loadSystemStats(): Promise<void> {
const token = localStorage.getItem("token");
if (!token) return;
try {
const response = await fetch("/api/admin/stats", {
headers: { Authorization: `Bearer ${token}` },
});
if (response.ok) {
const stats = await response.json();
renderSystemStats(stats);
}
} catch (error) {
console.error("Failed to load stats:", error);
}
}
function renderSystemStats(stats: Record<string, unknown>): void {
const container = document.getElementById("system-stats");
if (!container) return;
container.innerHTML = `
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary)">
<p class="text-2xl font-bold" style="color: var(--text-primary)">${stats.total_books || 0}</p>
<p class="text-sm" style="color: var(--text-secondary)">Total Books</p>
</div>
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary)">
<p class="text-2xl font-bold" style="color: var(--text-primary)">${stats.total_users || 0}</p>
<p class="text-sm" style="color: var(--text-secondary)">Users</p>
</div>
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary)">
<p class="text-2xl font-bold" style="color: var(--text-primary)">${stats.total_devices || 0}</p>
<p class="text-sm" style="color: var(--text-secondary)">Devices</p>
</div>
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary)">
<p class="text-2xl font-bold" style="color: var(--text-primary)">${stats.total_libraries || 0}</p>
<p class="text-sm" style="color: var(--text-secondary)">Libraries</p>
</div>
</div>
`;
}
async function scanAllLibraries(): Promise<void> {
const token = localStorage.getItem("token");
@@ -227,6 +98,8 @@ function showScanProgress(
pollScanProgress(jobIds, libraryNames);
}
let scanPollInterval: ReturnType<typeof setInterval> | undefined = undefined;
function pollScanProgress(
jobIds: string[],
_libraryNames: Record<string, string>,
@@ -283,7 +156,7 @@ function pollScanProgress(
}
if (allComplete) {
clearInterval(scanPollInterval);
clearInterval(scanPollInterval!);
showScanResults(
jobIds.length,
totalFiles,
@@ -369,8 +242,6 @@ async function loadWatchStatus(): Promise<void> {
}
}
let scanPollInterval: ReturnType<typeof setInterval> | undefined = undefined;
function stopScanStatusPolling(): void {
if (scanPollInterval !== undefined) {
clearInterval(scanPollInterval);
@@ -380,22 +251,14 @@ function stopScanStatusPolling(): void {
export {
hideScanProgress,
initializeScanWebSocket,
loadSystemStats,
loadWatchStatus,
scanAllLibraries,
stopScanStatusPolling,
triggerLibraryScan,
triggerQuickScan,
};
Alpine.data("admin", () => ({
hideScanProgress,
initializeScanWebSocket,
loadSystemStats,
loadWatchStatus,
scanAllLibraries,
stopScanStatusPolling,
triggerLibraryScan,
triggerQuickScan,
}));
-695
View File
@@ -1,695 +0,0 @@
// Library management functionality for admin/library page
// Procedural/imperative style t
// nt
// OOP)
import { Alpine } from "./alpine";
import {
apiPut,
apiPost,
apiGet,
handleResponse,
handleError,
handleVoidResponse,
apiDelete,
} from "./api";
import { showToast } from "./toast";
interface Library {
id: string;
name: string;
description: string | null;
library_type_id: string;
type_name: string;
created_at: string;
updated_at: string;
}
interface LibraryFolder {
id: string;
library_id: string;
folder_path: string;
created_at: string;
}
interface LibrariesResponse {
data: Library[];
}
// State (already SSR'd, used for updates)
let libraries: Library[] = [];
// Reload libraries from API (called after create/delete/update)
async function reloadLibraries(): Promise<void> {
try {
const response = await apiGet("/libraries");
const result = (await handleResponse(
response,
)) as unknown as LibrariesResponse;
libraries = result.data;
renderLibraries();
} catch (error) {
handleError(error, "Failed to load libraries");
}
}
// Render libraries list (replaces SSR content after updates)
function renderLibraries(): void {
const container = document.getElementById("libraries-list");
if (!container) return;
if (libraries.length === 0) {
container.innerHTML =
'<p style="color: var(--text-secondary)" class="text-center py-8">No libraries yet. Create your first library to get started.</p>';
return;
}
container.innerHTML = libraries
.map(
(library) =>
'<div class="p-4 border rounded-lg" style="background-color: var(--bg-primary); border-color: var(--border)">' +
'<div class="flex justify-between items-start mb-2">' +
"<div>" +
`<h4 class="font-semibold" style="color: var(--text-primary)">${escapeHtmlLocal(library.name)}</h4>` +
(library.description
? `<p class="text-sm" style="color: var(--text-secondary)">${escapeHtmlLocal(library.description)}</p>`
: "") +
`<span class="inline-block px-2 py-1 text-xs rounded" style="background-color: var(--accent); color: var(--bg-primary)">${escapeHtmlLocal(library.type_name)}</span>` +
"</div>" +
'<div class="flex space-x-2">' +
`<button data-library-id="${library.id}" data-action="show-folders" class="text-xs px-2 py-1 rounded" style="background-color: var(--bg-secondary); color: var(--text-primary)">Folders</button>` +
`<button data-library-id="${library.id}" data-action="edit" class="text-xs px-2 py-1 rounded" style="background-color: var(--accent); color: var(--bg-primary)">Edit</button>` +
`<button data-library-id="${library.id}" data-action="delete" class="text-xs px-2 py-1 rounded text-red-500">Delete</button>` +
"</div>" +
"</div>" +
`<div id="library-folders-${library.id}" class="hidden mt-3 space-y-2"></div>` +
"</div>",
)
.join("");
}
// Load user's visible libraries for visibility management
async function loadUserVisibility(): Promise<void> {
const select = document.getElementById("user-select") as HTMLSelectElement;
const userId = select?.value;
if (!userId) {
const container = document.getElementById("user-libraries");
if (container) {
container.innerHTML =
'<p style="color: var(--text-secondary)">Please select a user</p>';
}
return;
}
try {
const response = await apiGet("/libraries/visible");
const visibleLibraries = (await handleResponse(
response,
)) as unknown as Library[];
const container = document.getElementById("user-libraries");
if (!container) return;
const visibleIds = new Set(visibleLibraries.map((lib: Library) => lib.id));
container.innerHTML = libraries
.map((library) => {
const isVisible = visibleIds.has(library.id);
return (
'<label class="flex items-center space-x-3 p-2 rounded" style="background-color: var(--bg-primary);">' +
`<input type="checkbox" ${isVisible ? "checked" : ""} ` +
`data-user-id="${userId}" data-library-id="${library.id}" ` +
`onchange="setLibraryVisibility('${userId}', '${library.id}', this.checked)" ` +
'class="w-4 h-4">' +
`<span style="color: var(--text-primary)">${escapeHtmlLocal(library.name)} (${escapeHtmlLocal(library.type_name)})</span>` +
"</label>"
);
})
.join("");
} catch (error) {
handleError(error, "Failed to load user libraries");
}
}
// Set library visibility for a user
async function setLibraryVisibility(
userId: string,
libraryId: string,
isVisible: boolean,
): Promise<void> {
// userId is used in the HTML onchange handler but the API gets user from JWT context
console.debug(
"Setting visibility for user:",
userId,
"library:",
libraryId,
"visible:",
isVisible,
);
try {
const response = await apiPost("/libraries/visibility", {
library_id: libraryId,
is_visible: isVisible,
});
await handleVoidResponse(response);
showToast("Library visibility updated", "success");
// Refresh visibility controls
void loadUserVisibility();
} catch (error) {
handleError(error, "Failed to update library visibility");
}
}
// Create library form handler
async function handleCreateLibrarySubmit(event: Event): Promise<void> {
event.preventDefault();
const form = event.target as HTMLFormElement;
const formData = new FormData(form);
const libraryId = (document.getElementById("library-id") as HTMLInputElement)
?.value;
const isEdit = !!libraryId;
const libraryData = {
name: formData.get("name") as string,
description: formData.get("description") as string,
type: formData.get("type") as string,
};
try {
const url = isEdit ? `/libraries/${libraryId}` : "/libraries";
const response = isEdit
? await apiPut(url, libraryData)
: await apiPost(url, libraryData);
if (isEdit) {
await handleVoidResponse(response);
} else {
(await handleResponse(response)) as unknown as { data: Library };
}
showToast(
isEdit ? "Library updated successfully" : "Library created successfully",
"success",
);
hideCreateLibraryModal();
form.reset();
const libraryIdInput = document.getElementById(
"library-id",
) as HTMLInputElement;
if (libraryIdInput) {
libraryIdInput.value = "";
}
void reloadLibraries();
} catch (error) {
handleError(
error,
isEdit ? "Failed to update library" : "Failed to create library",
);
}
}
// Delete library
async function deleteLibrary(libraryId: string): Promise<void> {
const library = libraries.find((l) => l.id === libraryId);
if (!library) return;
showDeleteModal(library);
}
// Show library folders
async function showLibraryFolders(libraryId: string): Promise<void> {
const container = document.getElementById(`library-folders-${libraryId}`);
if (!container) return;
try {
const response = await apiGet(`/libraries/${libraryId}/folders`);
const folders = (await handleResponse(
response,
)) as unknown as LibraryFolder[];
container.innerHTML = folders
.map(
(folder: LibraryFolder) =>
'<div class="flex justify-between items-center p-2 rounded" style="background-color: var(--bg-secondary); border-color: var(--border)">' +
`<span class="text-sm" style="color: var(--text-primary)">${escapeHtmlLocal(folder.folder_path)}</span>` +
`<button data-library-id="${libraryId}" data-folder-path="${escapeHtmlLocal(folder.folder_path)}" data-action="remove-folder" ` +
'class="text-xs text-red-500">Remove</button>' +
"</div>",
)
.join("");
container.innerHTML +=
'<div class="mt-2 flex space-x-2">' +
`<input type="text" id="folder-path-${libraryId}" placeholder="Add folder path" ` +
'class="flex-1 px-2 py-1 text-sm border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)">' +
`<button data-action="browse-folder" data-input-id="folder-path-${libraryId}" ` +
'class="btn-secondary px-2 py-1 text-xs rounded">Browse</button>' +
`<button data-library-id="${libraryId}" data-action="add-folder" ` +
'class="btn-primary px-2 py-1 text-xs rounded">Add</button>' +
"</div>";
container.classList.remove("hidden");
} catch (error) {
handleError(error, "Failed to load folders");
}
}
// Add library folder
async function addLibraryFolder(libraryId: string): Promise<void> {
const input = document.getElementById(
`folder-path-${libraryId}`,
) as HTMLInputElement;
const folderPath = input?.value.trim();
if (!folderPath) {
return;
}
try {
const response = await apiPost(`/libraries/${libraryId}/folders`, {
folder_path: folderPath,
});
await handleVoidResponse(response);
showToast("Folder added successfully", "success");
if (input) {
input.value = "";
}
void showLibraryFolders(libraryId); // Refresh
} catch (error) {
handleError(error, "Failed to add folder");
}
}
// Remove library folder
async function removeLibraryFolder(
libraryId: string,
folderPath: string,
): Promise<void> {
if (!confirm(`Remove folder "${folderPath}" from the library?`)) {
return;
}
try {
const response = await apiDelete(`/libraries/${libraryId}/folders`, {
folder_path: folderPath,
});
await handleVoidResponse(response);
showToast("Folder removed successfully", "success");
void showLibraryFolders(libraryId); // Refresh
} catch (error) {
handleError(error, "Failed to remove folder");
}
}
// Edit library (placeholder - opens modal or navigates to edit page)
function editLibrary(libraryId: string): void {
const library = libraries.find((l) => l.id === libraryId);
if (!library) {
showToast("Library not found", "error");
return;
}
const form = document.getElementById(
"create-library-form",
) as HTMLFormElement;
if (form) {
const nameInput = form.querySelector('[name="name"]') as HTMLInputElement;
const descInput = form.querySelector(
'[name="description"]',
) as HTMLTextAreaElement;
const typeInput = form.querySelector('[name="type"]') as HTMLSelectElement;
if (nameInput) nameInput.value = library.name;
if (descInput) descInput.value = library.description || "";
if (typeInput) typeInput.value = library.type_name;
}
const modalTitle = document.querySelector("#create-library-modal h2");
if (modalTitle) {
modalTitle.textContent = "Edit Library";
}
const libraryIdInput = document.getElementById(
"library-id",
) as HTMLInputElement;
if (libraryIdInput) {
libraryIdInput.value = libraryId;
}
showCreateLibraryModal();
}
// Modal controls
function showCreateLibraryModal(): void {
const modal = document.getElementById("create-library-modal") as HTMLElement;
if (modal) {
modal.classList.remove("hidden");
const modalTitle = document.querySelector("#create-library-modal h2");
if (modalTitle) {
modalTitle.textContent = "Create Library";
}
}
}
function hideCreateLibraryModal(): void {
const modal = document.getElementById("create-library-modal") as HTMLElement;
if (modal) {
modal.classList.add("hidden");
}
}
// Delete modal state
let libraryToDelete: Library | null = null;
function showDeleteModal(library: Library): void {
libraryToDelete = library;
const modal = document.getElementById("delete-library-modal") as HTMLElement;
const content = document.getElementById(
"delete-modal-content",
) as HTMLElement;
if (modal && content) {
const message = `Are you sure you want to delete "<strong>${escapeHtmlLocal(library.name)}</strong>"?
This will remove:
• Library metadata from the database
• All folder references
• All book records from the database
⚠️ Book files on disk will NOT be deleted.
This action cannot be undone.`;
content.innerHTML = message.replace(/\n/g, "<br>");
modal.classList.remove("hidden");
}
}
function hideDeleteModal(): void {
const modal = document.getElementById("delete-library-modal") as HTMLElement;
if (modal) {
modal.classList.add("hidden");
}
libraryToDelete = null;
}
async function confirmDeleteLibrary(): Promise<void> {
if (!libraryToDelete) return;
const libraryId = libraryToDelete.id;
hideDeleteModal();
try {
const response = await apiDelete(`/libraries/${libraryId}`);
await handleVoidResponse(response);
showToast("Library deleted successfully", "success");
await reloadLibraries();
const libraryIdInput = document.getElementById(
"library-id",
) as HTMLInputElement | null;
if (libraryIdInput) {
libraryIdInput.value = "";
}
} catch (error) {
handleError(error, "Failed to delete library");
}
}
// Local escape HTML helper
function escapeHtmlLocal(text: string): string {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
// Event delegation for handling dynamic button clicks
function handleLibraryListClick(event: Event): void {
const target = event.target as HTMLElement;
const button = target.closest("button") as HTMLElement;
if (!button) return;
const action = button.dataset.action;
const libraryId = button.dataset.libraryId;
switch (action) {
case "show-folders":
if (libraryId) showLibraryFolders(libraryId);
break;
case "delete":
if (libraryId) deleteLibrary(libraryId);
break;
case "edit":
if (libraryId) editLibrary(libraryId);
break;
case "add-folder":
if (libraryId) addLibraryFolder(libraryId);
break;
case "remove-folder":
if (libraryId && button.dataset.folderPath) {
removeLibraryFolder(libraryId, button.dataset.folderPath);
}
break;
case "browse-folder":
if (button.dataset.inputId) showFolderBrowser(button.dataset.inputId);
break;
}
}
function handleFolderBrowserClick(event: Event): void {
const target = event.target as HTMLElement;
const button = target.closest("button") as HTMLElement;
const div = target.closest("div[data-action]") as HTMLElement;
if (button) {
const action = button.dataset.action;
const path = button.dataset.path;
switch (action) {
case "browse-parent":
if (path) navigateFolderBrowser(path);
break;
case "browse-cancel":
hideFolderBrowser();
break;
case "browse-select":
if (path) selectBrowseFolder(path);
break;
}
}
if (div && div.dataset.action === "browse-navigate") {
const path = div.dataset.path;
if (path) navigateFolderBrowser(path);
}
}
function handleGlobalClick(event: Event): void {
const target = event.target as HTMLElement;
const button = target.closest("button") as HTMLElement;
if (!button) return;
const action = button.dataset.action;
switch (action) {
case "show-create-modal":
showCreateLibraryModal();
break;
case "hide-create-modal":
hideCreateLibraryModal();
break;
case "hide-delete-modal":
hideDeleteModal();
break;
case "confirm-delete":
void confirmDeleteLibrary();
break;
}
}
// Folder browser state
let currentBrowsePath = "";
let currentBrowseInputId = "";
// Show folder browser modal
function showFolderBrowser(inputId: string): void {
currentBrowseInputId = inputId;
currentBrowsePath = "/";
const modal = document.getElementById("folder-browser-modal") as HTMLElement;
if (modal) {
modal.classList.remove("hidden");
void loadBrowseDirectories(currentBrowsePath);
}
}
// Load directories for browsing
async function loadBrowseDirectories(path: string): Promise<void> {
try {
const response = await apiGet(
`/libraries/browse?path=${encodeURIComponent(path)}`,
);
const data = (await handleResponse(response)) as unknown as {
current_path: string;
parent_path: string;
directories: string[];
};
currentBrowsePath = data.current_path;
renderBrowseDirectories(data);
} catch (error) {
handleError(error, "Failed to load directories");
}
}
// Render browse directories (uses event delegation via data-action attributes)
function renderBrowseDirectories(data: {
current_path: string;
parent_path: string;
directories: string[];
}): void {
const container = document.getElementById("folder-browser-content");
if (!container) return;
let html = `
<div class="flex items-center gap-2 mb-4">
${
data.parent_path
? `<button type="button" data-action="browse-parent" data-path="${escapeHtmlLocal(data.parent_path)}" class="btn-secondary px-3 py-1 rounded">↑ Parent</button>`
: ""
}
<span class="text-sm" style="color: var(--text-secondary)">${escapeHtmlLocal(data.current_path)}</span>
</div>
<div class="max-h-64 overflow-y-auto space-y-1">
`;
if (data.directories.length === 0) {
html +=
'<p style="color: var(--text-secondary)" class="text-center py-4">No subdirectories</p>';
} else {
data.directories.forEach((dir) => {
const fullPath =
data.current_path === "/" ? `/${dir}` : `${data.current_path}/${dir}`;
html += `
<div class="p-2 rounded cursor-pointer hover:opacity-80"
style="background-color: var(--bg-secondary); color: var(--text-primary)"
data-action="browse-navigate"
data-path="${escapeHtmlLocal(fullPath)}">
📁 ${escapeHtmlLocal(dir)}
</div>
`;
});
}
html += `
</div>
<div class="mt-4 flex justify-end gap-2">
<button type="button" data-action="browse-cancel" class="btn-secondary px-4 py-2 rounded">Cancel</button>
<button type="button" data-action="browse-select" data-path="${escapeHtmlLocal(data.current_path)}" class="btn-primary px-4 py-2 rounded">Select This Folder</button>
</div>
`;
container.innerHTML = html;
}
// Navigate to subdirectory
function navigateFolderBrowser(path: string): void {
void loadBrowseDirectories(path);
}
// Select folder and close browser
function selectBrowseFolder(path: string): void {
const input = document.getElementById(
currentBrowseInputId,
) as HTMLInputElement;
if (input) {
input.value = path;
}
hideFolderBrowser();
}
// Hide folder browser modal
function hideFolderBrowser(): void {
const modal = document.getElementById("folder-browser-modal") as HTMLElement;
if (modal) {
modal.classList.add("hidden");
}
}
// Initialize page
function initializeLibraryAdmin(): void {
// Setup event listeners
const librariesList = document.getElementById("libraries-list");
if (librariesList) {
librariesList.addEventListener("click", handleLibraryListClick);
}
const folderBrowserModal = document.getElementById("folder-browser-modal");
if (folderBrowserModal) {
folderBrowserModal.addEventListener("click", handleFolderBrowserClick);
}
document.addEventListener("click", handleGlobalClick);
// Setup form submission
const createLibraryForm = document.getElementById("create-library-form");
if (createLibraryForm) {
createLibraryForm.addEventListener("submit", handleCreateLibrarySubmit);
}
// SSR provides initial library list - no need to fetch on page load
// reloadLibraries() is called after create/delete/update operations
}
// Export functions for global access
export {
addLibraryFolder,
confirmDeleteLibrary,
deleteLibrary,
editLibrary,
handleCreateLibrarySubmit,
hideDeleteModal,
hideFolderBrowser,
initializeLibraryAdmin,
loadUserVisibility,
navigateFolderBrowser,
removeLibraryFolder,
selectBrowseFolder,
setLibraryVisibility,
showDeleteModal,
showFolderBrowser,
showLibraryFolders,
};
Alpine.data("library", () => ({
addLibraryFolder,
confirmDeleteLibrary,
deleteLibrary,
editLibrary,
handleCreateLibrarySubmit,
hideDeleteModal,
hideFolderBrowser,
initializeLibraryAdmin,
loadUserVisibility,
navigateFolderBrowser,
removeLibraryFolder,
selectBrowseFolder,
setLibraryVisibility,
showDeleteModal,
showFolderBrowser,
showLibraryFolders,
}));
-1
View File
@@ -20,7 +20,6 @@ import "./dom";
import "./events";
import "./header";
import "./index";
import "./library";
import "./library-switcher";
import "./linking";
import "./login";
+1 -1
View File
File diff suppressed because one or more lines are too long