feat(web): update frontend TypeScript modules and API types

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.
This commit is contained in:
2026-02-27 17:06:48 -05:00
parent 4d321528b2
commit ea5ad7a41b
22 changed files with 3133 additions and 2737 deletions
+248 -211
View File
@@ -1,82 +1,84 @@
async function triggerLibraryScan(): Promise<void> {
const token = localStorage.getItem('token');
if (!token) return;
const token = localStorage.getItem("token");
if (!token) return;
try {
const response = await fetch('/api/libraries/scan', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` }
});
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');
}
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;
const token = localStorage.getItem("token");
if (!token) return;
try {
const response = await fetch('/api/libraries/quick-scan', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` }
});
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');
}
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;
const token = localStorage.getItem("token");
if (!token) return;
try {
const response = await fetch('/api/admin/stats', {
headers: { 'Authorization': `Bearer ${token}` }
});
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);
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;
const container = document.getElementById("system-stats");
if (!container) return;
container.innerHTML = `
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>
@@ -99,78 +101,88 @@ function renderSystemStats(stats: Record<string, unknown>): void {
}
async function scanAllLibraries(): Promise<void> {
const token = localStorage.getItem('token');
if (!token) return;
const token = localStorage.getItem("token");
if (!token) return;
try {
const libsResp = await fetch('/api/libraries', {
headers: { 'Authorization': `Bearer ${token}` }
});
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);
}
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;
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;
if (!container || !list) return;
container.classList.remove('hidden');
container.classList.remove('opacity-0', '-translate-y-2.5');
container.classList.remove("hidden");
container.classList.remove("opacity-0", "-translate-y-2.5");
list.innerHTML = jobIds.map(jobId => `
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">
@@ -188,133 +200,158 @@ function showScanProgress(jobIds: string[], libraryNames: Record<string, string>
</div>
</div>
</div>
`).join('');
`,
)
.join("");
pollScanProgress(jobIds, libraryNames);
pollScanProgress(jobIds, libraryNames);
}
function pollScanProgress(jobIds: string[], _libraryNames: Record<string, string>): void {
const token = localStorage.getItem('token');
const startTime = Date.now();
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;
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}` }
});
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();
if (resp.ok) {
const status = await resp.json();
updateLibraryProgress(jobId, status);
updateLibraryProgress(jobId, status);
totalProgress += status.progress || 0;
totalFiles += status.files_scanned || 0;
totalNewItems += status.new_items || 0;
totalErrors += status.errors || 0;
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);
}
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;
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 + '%';
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`;
}
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);
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;
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 (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;
}
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;
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;
if (!resultsDiv || !contentDiv) return;
contentDiv.innerHTML = `
<p>• ${libCount} librar${libCount === 1 ? 'y' : 'ies'} scanned</p>
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>` : ''}
${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');
resultsDiv.classList.remove("hidden");
const statusText = document.getElementById('scan-status') as HTMLElement;
if (statusText) statusText.textContent = 'Scan complete!';
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');
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;
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);
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();
document.addEventListener("DOMContentLoaded", function () {
loadWatchStatus();
});
(window as any).triggerLibraryScan = triggerLibraryScan;