This commit updates the web frontend TypeScript modules: Core modules: - admin.ts: Admin panel functionality and user management - analytics.ts: Analytics dashboard and data visualization - api-explorer.ts: Interactive API documentation explorer - api.ts: Core API client with request/response handling - collections.ts: Book collection management UI - conflicts.ts: Sync conflict resolution interface - custom-section-builder.ts: Dynamic section builder for UI - docs.ts: Documentation viewer and navigation - dom.ts: DOM manipulation utilities and helpers - header.ts: Application header with navigation - library.ts: Library view and book grid management - linking.ts: Device-book linking interface - password_validation.ts: Client-side password strength validation - queue.ts: Device sync queue management UI - search.ts: Full-text search with Lunr integration - storage.ts: Local storage and cache management - theme.ts: Theme management and CSS variable updates - themeDropdown.ts: Theme selector dropdown component - toast.ts: Toast notification system - woodPaneling.ts: Visual theme effects - woodPanelingInit.ts: Visual effects initialization Type definitions: - api.d.ts: Updated TypeScript definitions for API responses These updates enhance the frontend with improved functionality for book management, device synchronization, and user experience.
363 lines
11 KiB
TypeScript
363 lines
11 KiB
TypeScript
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) {
|
|
if ((window as any).showToast?.success) {
|
|
(window as any).showToast.success("Library scan started");
|
|
}
|
|
} else {
|
|
const error = await response.json();
|
|
if ((window as any).showToast?.error) {
|
|
(window as any).showToast.error(error.error || "Failed to start scan");
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Scan error:", error);
|
|
if ((window as any).showToast?.error) {
|
|
(window as any).showToast.error("Failed to start library scan");
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
if ((window as any).showToast?.success) {
|
|
(window as any).showToast.success("Quick scan started");
|
|
}
|
|
} else {
|
|
const error = await response.json();
|
|
if ((window as any).showToast?.error) {
|
|
(window as any).showToast.error(
|
|
error.error || "Failed to start quick scan",
|
|
);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Quick scan error:", error);
|
|
if ((window as any).showToast?.error) {
|
|
(window as any).showToast.error("Failed to start quick scan");
|
|
}
|
|
}
|
|
}
|
|
|
|
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");
|
|
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) {
|
|
if ((window as any).showToast?.error) {
|
|
(window as any).showToast.error(
|
|
"No libraries found. Please create a library first.",
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const libraries = libsData.data;
|
|
|
|
const jobs: string[] = [];
|
|
const libraryNames: Record<string, string> = {};
|
|
|
|
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) {
|
|
if ((window as any).showToast?.error) {
|
|
(window as any).showToast.error("Failed to start scan for any library");
|
|
}
|
|
return;
|
|
}
|
|
|
|
showScanProgress(jobs, libraryNames);
|
|
} catch (error) {
|
|
console.error("Scan error:", error);
|
|
if ((window as any).showToast?.error) {
|
|
(window as any).showToast.error(
|
|
"Failed to start scan: " + (error as Error).message,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function showScanProgress(
|
|
jobIds: string[],
|
|
libraryNames: Record<string, string>,
|
|
): 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) => `
|
|
<div id="progress-${jobId}" class="p-3 rounded border"
|
|
style="background-color: var(--bg-primary); border-color: var(--border);">
|
|
<div class="flex justify-between items-center mb-2">
|
|
<span class="font-medium" style="color: var(--text-primary)">
|
|
${libraryNames[jobId]}
|
|
</span>
|
|
<span id="status-${jobId}" class="text-sm" style="color: var(--text-secondary)">
|
|
Pending...
|
|
</span>
|
|
</div>
|
|
<div class="w-full bg-gray-700 rounded-full h-2">
|
|
<div id="bar-${jobId}"
|
|
class="h-2 rounded-full transition-all duration-500"
|
|
style="width: 0%; background-color: var(--accent);">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`,
|
|
)
|
|
.join("");
|
|
|
|
pollScanProgress(jobIds, libraryNames);
|
|
}
|
|
|
|
function pollScanProgress(
|
|
jobIds: string[],
|
|
_libraryNames: Record<string, string>,
|
|
): void {
|
|
const token = localStorage.getItem("token");
|
|
const startTime = Date.now();
|
|
|
|
const interval = 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(interval);
|
|
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<string, string> = {
|
|
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 = `
|
|
<p>• ${libCount} librar${libCount === 1 ? "y" : "ies"} scanned</p>
|
|
<p>• ${files} files processed</p>
|
|
<p>• ${items} new items added</p>
|
|
${errors > 0 ? `<p style="color: var(--accent);">• ${errors} errors</p>` : ""}
|
|
<p style="color: var(--text-secondary)">Completed in ${elapsed} seconds</p>
|
|
`;
|
|
|
|
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<void> {
|
|
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);
|
|
}
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
loadWatchStatus();
|
|
});
|
|
|
|
(window as any).triggerLibraryScan = triggerLibraryScan;
|
|
(window as any).triggerQuickScan = triggerQuickScan;
|
|
(window as any).loadSystemStats = loadSystemStats;
|
|
(window as any).scanAllLibraries = scanAllLibraries;
|
|
(window as any).loadWatchStatus = loadWatchStatus;
|
|
(window as any).hideScanProgress = hideScanProgress;
|