import { MockApi } from './mock_api.js'; import { RealApi } from './real_api.js'; // Toggle this to switch modes const USE_REAL_API = true; const Api = USE_REAL_API ? RealApi : MockApi; // Utils const formatBytes = (bytes, decimals = 2) => { if (!+bytes) return '0 Bytes'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; }; const formatDate = (dateString) => { if (!dateString) return 'Never'; const options = { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }; return new Date(dateString).toLocaleDateString(undefined, options); }; // Component Rendering Functions const renderDashboard = async () => { const stats = await Api.getDashboardStats(); const jobs = await Api.getRecentJobs(); return `

System Overview

+12%
${stats.totalJobs}
Total Jobs Run
${stats.successRate}%
Success Rate
${stats.storageUsage}% used
${formatBytes(stats.totalBytes)}
Total Backup Size
${stats.activeClients}
Active Clients

Recent Jobs

${jobs.map(job => ` `).join('')}
Status Job Name Client Level Files Size Duration
${job.status} ${job.name} ${job.client} ${job.level} ${job.files.toLocaleString()} ${formatBytes(job.bytes)} ${job.duration}
`; }; // State for Jobs View const jobsState = { filter: 'All', search: '' }; // Global handlers for filter updates window.updateJobFilter = (value) => { jobsState.filter = value; // Trigger re-render of current page window.dispatchEvent(new Event('hashchange')); }; window.updateJobSearch = (value) => { jobsState.search = value.toLowerCase(); // Debounce re-render could be better, but simple re-render works for now // We'll just re-render the table body via DOM manipulation ideally, // but for simplicity with this router, we can re-trigger route or just rely on input keeping focus if we are careful. // Re-rendering whole page loses focus. // Better strategy: Store the value, but let the user press enter or wait. // For now, let's just make the search work on 'onchange' or 'onkeyup' with a small re-render hack // Actually, to avoid losing focus on every keystroke, let's just use the router re-render // but we need to manage focus. // Simplified approach: Just update state. The user has to trigger search? // Or simpler: Just re-render. To keep input focus, we can select it after render. const contentArea = document.getElementById('content-area'); // For this prototype, a full re-render is jarring. // But let's stick to the request pattern. window.dispatchEvent(new Event('hashchange')); }; // Helper to keep focus helper (hacky but works for simple vanilla js SPA) const restoreFocus = () => { const searchInput = document.querySelector('#job-search-input'); if (searchInput) { searchInput.focus(); // Move cursor to end const val = searchInput.value; searchInput.value = ''; searchInput.value = val; } } const renderJobs = async () => { const jobs = await Api.getRecentJobs(); const allJobs = [...jobs, ...jobs, ...jobs]; // Demo duplication // Apply Filters const filteredJobs = allJobs.filter(job => { const matchesStatus = jobsState.filter === 'All' || job.status === jobsState.filter; const matchesSearch = jobsState.search === '' || job.name.toLowerCase().includes(jobsState.search) || job.client.toLowerCase().includes(jobsState.search); return matchesStatus && matchesSearch; }); // Schedule focus restore after render setTimeout(restoreFocus, 0); return `

Backup Jobs

${filteredJobs.length > 0 ? filteredJobs.map(job => ` `).join('') : ` `}
Status Job Name Client Level Start Time Files Size
${job.status} ${job.name} ${job.client} ${job.level} ${formatDate(job.startTime)} ${job.files.toLocaleString()} ${formatBytes(job.bytes)}
No jobs found matching your filters.
`; }; const renderClients = async () => { const clients = await Api.getClients(); return `

Clients

${clients.map(client => `
${client.status}

${client.name}

${client.os}

Last Backup ${formatDate(client.lastBackup)}
`).join('')}
`; }; // Router const router = async () => { const contentArea = document.getElementById('content-area'); const hash = window.location.hash || '#dashboard'; const page = hash.substring(1); // Update Sidebar Active State document.querySelectorAll('.nav-item').forEach(el => { el.classList.toggle('active', el.dataset.page === page); }); // Update Breadcrumbs document.querySelector('.current-page').textContent = page.charAt(0).toUpperCase() + page.slice(1); contentArea.innerHTML = `

Loading...

`; try { let content = ''; switch (page) { case 'dashboard': content = await renderDashboard(); break; case 'jobs': content = await renderJobs(); break; case 'clients': content = await renderClients(); break; case 'storage': const storageDevices = await Api.getStorage(); content = `

Storage Daemons

${storageDevices.length > 0 ? storageDevices.map(dev => ` `).join('') : ` `}
Name Type Status Capacity Usage
${dev.name} ${dev.type} ${dev.status} ${dev.capacity}
${dev.used} ${dev.percent > 0 ? `
` : ''}
No storage devices found.
`; break; case 'settings': content = '

Settings

Coming soon...

'; break; default: content = '

404 Not Found

'; } contentArea.innerHTML = content; } catch (error) { console.error(error); contentArea.innerHTML = '

Error loading content

'; } }; // Init window.addEventListener('hashchange', router); window.addEventListener('DOMContentLoaded', () => { router(); // Append Modal to Body if not exists if (!document.getElementById('client-modal')) { const modal = document.createElement('div'); modal.id = 'client-modal'; modal.innerHTML = ` `; document.body.appendChild(modal); } }); window.openAddClientModal = () => { document.getElementById('client-modal-overlay').style.display = 'flex'; }; window.closeClientModal = () => { document.getElementById('client-modal-overlay').style.display = 'none'; }; window.handleClientSubmit = async (e) => { e.preventDefault(); const formData = new FormData(e.target); const data = Object.fromEntries(formData.entries()); try { const btn = e.target.querySelector('button[type="submit"]'); const origText = btn.textContent; btn.textContent = 'Saving...'; btn.disabled = true; const res = await fetch(`${Api.getBaseUrl()}/clients`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (!res.ok) throw new Error((await res.json()).error); alert('Client added successfully!'); closeClientModal(); e.target.reset(); window.dispatchEvent(new Event('hashchange')); // Reload view } catch (err) { alert('Error adding client: ' + err.message); } finally { const btn = e.target.querySelector('button[type="submit"]'); btn.textContent = 'Save Client'; btn.disabled = false; } };