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 { 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 { 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 { 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): void { const container = document.getElementById("system-stats"); if (!container) return; container.innerHTML = `

${stats.total_books || 0}

Total Books

${stats.total_users || 0}

Users

${stats.total_devices || 0}

Devices

${stats.total_libraries || 0}

Libraries

`; } async function scanAllLibraries(): Promise { const token = localStorage.getItem("token"); if (!token) return; try { const libsResp = await fetch("/api/libraries", { headers: { Authorization: `Bearer ${token}` }, }); if (!libsResp.ok) { throw new Error("Failed to get libraries"); } const libsData = await libsResp.json(); if (!libsData.data || libsData.data.length === 0) { showToast("No libraries found. Please create a library first.", "error"); return; } const libraries = libsData.data; const jobs: string[] = []; const libraryNames: Record = {}; for (const lib of libraries) { const scanResp = await fetch(`/api/libraries/${lib.id}/scan`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ force: true }), }); if (scanResp.ok) { const result = await scanResp.json(); jobs.push(result.job_id); libraryNames[result.job_id] = lib.name; } else { console.error(`Failed to scan library: ${lib.name}`); } } if (jobs.length === 0) { showToast("Failed to start scan for any library", "error"); return; } showScanProgress(jobs, libraryNames); } catch (error) { console.error("Scan error:", error); showToast("Failed to start scan: " + (error as Error).message, "error"); } } function showScanProgress( jobIds: string[], libraryNames: Record, ): void { const container = document.getElementById( "scan-progress-container", ) as HTMLElement; const list = document.getElementById("library-progress-list") as HTMLElement; if (!container || !list) return; container.classList.remove("hidden"); container.classList.remove("opacity-0", "-translate-y-2.5"); list.innerHTML = jobIds .map( (jobId) => `
${libraryNames[jobId]} Pending...
`, ) .join(""); pollScanProgress(jobIds, libraryNames); } function pollScanProgress( jobIds: string[], _libraryNames: Record, ): void { const token = localStorage.getItem("token"); const startTime = Date.now(); scanPollInterval = setInterval(async () => { let allComplete = true; let totalProgress = 0; let totalFiles = 0; let totalNewItems = 0; let totalErrors = 0; for (const jobId of jobIds) { try { const resp = await fetch(`/api/scanner/status/${jobId}`, { headers: { Authorization: `Bearer ${token}` }, }); if (resp.ok) { const status = await resp.json(); updateLibraryProgress(jobId, status); totalProgress += status.progress || 0; totalFiles += status.files_scanned || 0; totalNewItems += status.new_items || 0; totalErrors += status.errors || 0; if (status.status !== "completed" && status.status !== "failed") { allComplete = false; } } } catch (error) { console.error(`Failed to poll job ${jobId}:`, error); } } const overallProgress = Math.round(totalProgress / jobIds.length); const progressBar = document.getElementById( "scan-progress-bar", ) as HTMLElement; const progressText = document.getElementById( "scan-progress-text", ) as HTMLElement; const statusText = document.getElementById("scan-status") as HTMLElement; if (progressBar) progressBar.style.width = overallProgress + "%"; if (progressText) progressText.textContent = overallProgress + "%"; const elapsed = Math.round((Date.now() - startTime) / 1000); if (!allComplete && statusText) { statusText.textContent = `Scanning... ${elapsed}s elapsed • ${totalFiles} files processed`; } if (allComplete) { clearInterval(scanPollInterval); showScanResults( jobIds.length, totalFiles, totalNewItems, totalErrors, elapsed, ); } }, 2000); } function updateLibraryProgress(jobId: string, status: any): void { const bar = document.getElementById(`bar-${jobId}`) as HTMLElement; const statusText = document.getElementById(`status-${jobId}`) as HTMLElement; if (bar) { bar.style.width = (status.progress || 0) + "%"; } if (statusText) { const statusMessages: Record = { pending: "Pending...", running: `Scanning... ${status.progress || 0}%`, completed: `✓ Complete (${status.new_items || 0} items)`, failed: `✗ Failed`, }; statusText.textContent = statusMessages[status.status] || status.status; } } function showScanResults( libCount: number, files: number, items: number, errors: number, elapsed: number, ): void { const resultsDiv = document.getElementById("scan-results") as HTMLElement; const contentDiv = document.getElementById( "scan-results-content", ) as HTMLElement; if (!resultsDiv || !contentDiv) return; contentDiv.innerHTML = `

• ${libCount} librar${libCount === 1 ? "y" : "ies"} scanned

• ${files} files processed

• ${items} new items added

${errors > 0 ? `

• ${errors} errors

` : ""}

Completed in ${elapsed} seconds

`; resultsDiv.classList.remove("hidden"); const statusText = document.getElementById("scan-status") as HTMLElement; if (statusText) statusText.textContent = "Scan complete!"; } function hideScanProgress(): void { const container = document.getElementById( "scan-progress-container", ) as HTMLElement; if (container) container.classList.add("hidden"); } async function loadWatchStatus(): Promise { const token = localStorage.getItem("token"); if (!token) return; try { const response = await fetch("/api/scanner/watch/status", { headers: { Authorization: `Bearer ${token}` }, }); if (response.ok) { const data = await response.json(); const countEl = document.getElementById("watch-count"); if (countEl) { countEl.textContent = data.total_watching?.toString() || "0"; } } } catch (error) { console.error("Failed to load watch status:", error); } } let scanPollInterval: ReturnType | undefined = undefined; function stopScanStatusPolling(): void { if (scanPollInterval !== undefined) { clearInterval(scanPollInterval); scanPollInterval = undefined; } } export { hideScanProgress, initializeScanWebSocket, loadSystemStats, loadWatchStatus, scanAllLibraries, stopScanStatusPolling, triggerLibraryScan, triggerQuickScan, }; Alpine.data("admin", () => ({ hideScanProgress, initializeScanWebSocket, loadSystemStats, loadWatchStatus, scanAllLibraries, stopScanStatusPolling, triggerLibraryScan, triggerQuickScan, }));