Files
bookhoard/web/src/admin.ts
T
john-okeefe abcc468024 frontend: add force rescan to button and watch status display
- Send force: true in scan API request body
- Rename 'Scan Library' button to 'Rescan Library'
- Replace Settings card with Watch Status display
- Add loadWatchStatus() to fetch and display watch mode status
- Remove inline script from admin.templ (moved to admin.ts)
2026-02-26 10:11:52 -05:00

326 lines
12 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;