let currentTab = 'clients'; // Check authentication on load async function checkAuth() { try { const res = await fetch('/admin/check'); const data = await res.json(); if (!data.authenticated) { window.location.href = '/admin/'; } } catch (e) { console.error('Auth check failed:', e); } } // Logout async function logout() { await fetch('/admin/logout', { method: 'POST' }); location.reload(); } // Toggle dark/light theme function toggleTheme() { const html = document.documentElement; const isDark = html.classList.contains('dark'); if (isDark) { html.classList.remove('dark'); localStorage.setItem('theme', 'light'); } else { html.classList.add('dark'); localStorage.setItem('theme', 'dark'); } } // Initialize theme on load function initTheme() { const savedTheme = localStorage.getItem('theme'); const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; if (savedTheme === 'dark' || (!savedTheme && prefersDark)) { document.documentElement.classList.add('dark'); } } // Load stats async function loadStats() { try { const res = await fetch('/admin/stats'); const data = await res.json(); document.getElementById('stats-grid').innerHTML = '
' + '
' + '
' + '
' + 'Clients' + '
' + '' + '
' + '
' + '
' + data.client_count + '
' + '
' + '
' + '
' + '
' + '
' + '
' + 'Total Snapshots' + '
' + '' + '
' + '
' + '
' + data.total_snapshots + '
' + '
' + '
' + '
' + '
' + '
' + '
' + 'Total Storage' + '
' + '' + '
' + '
' + '
' + data.total_storage_gb.toFixed(2) + ' GB
' + '
' + '
'; } catch (e) { console.error('Failed to load stats:', e); } } // Load clients async function loadClients() { try { const res = await fetch('/admin/clients'); const clients = await res.json(); const tbody = document.getElementById('clients-table'); tbody.innerHTML = clients.map(c => { const usedPercent = c.max_size_bytes > 0 ? (c.current_usage / c.max_size_bytes * 100).toFixed(1) : 0; const usedGB = (c.current_usage / (1024*1024*1024)).toFixed(2); const maxGB = (c.max_size_bytes / (1024*1024*1024)).toFixed(0); return '' + '' + c.client_id + '' + '' + c.storage_type + '' + '' + maxGB + ' GB' + '' + '
' + usedGB + ' GB (' + usedPercent + '%)
' + '
' + '' + '' + c.snapshot_count + '' + '' + (c.enabled ? 'Enabled' : 'Disabled') + '' + '' + '' + '' + '' + '' + '' + ''; }).join(''); // Update snapshot filter const filter = document.getElementById('snapshot-client-filter'); filter.innerHTML = '' + clients.map(c => '').join(''); } catch (e) { console.error('Failed to load clients:', e); } } // Load snapshots async function loadSnapshots() { const clientId = document.getElementById('snapshot-client-filter').value; const url = '/admin/snapshots' + (clientId ? '?client_id=' + clientId : ''); try { const res = await fetch(url); const snapshots = await res.json(); const tbody = document.getElementById('snapshots-table'); tbody.innerHTML = snapshots.map(s => { const sizeGB = (s.size_bytes / (1024*1024*1024)).toFixed(2); return '' + '' + s.client_id + '' + '' + s.snapshot_id + '' + '' + new Date(s.timestamp).toLocaleString() + '' + '' + sizeGB + ' GB' + '' + (s.incremental ? 'Incremental' : 'Full') + (s.compressed ? ' LZ4' : '') + '' + '' + ''; }).join(''); } catch (e) { console.error('Failed to load snapshots:', e); } } // Load admins async function loadAdmins() { try { const res = await fetch('/admin/admins'); const admins = await res.json(); const tbody = document.getElementById('admins-table'); tbody.innerHTML = admins.map(a => '' + '' + a.id + '' + '' + a.username + '' + '' + a.role + '' + '' + new Date(a.created_at).toLocaleDateString() + '' + '' + '' + '' + '' + '' ).join(''); } catch (e) { console.error('Failed to load admins:', e); } } // Tab switching function showTab(tab) { currentTab = tab; document.querySelectorAll('[data-tab]').forEach(t => { t.classList.remove('bg-gradient-to-r', 'from-violet-600', 'to-fuchsia-600', 'text-white', 'shadow-lg', 'shadow-violet-500/25'); t.classList.add('bg-slate-100', 'dark:bg-slate-800', 'text-slate-600', 'dark:text-slate-400', 'hover:bg-slate-200', 'dark:hover:bg-slate-700', 'hover:text-slate-900', 'dark:hover:text-white', 'border', 'border-slate-200', 'dark:border-slate-700'); }); const activeTab = document.querySelector('[data-tab="' + tab + '"]'); activeTab.classList.remove('bg-slate-100', 'dark:bg-slate-800', 'text-slate-600', 'dark:text-slate-400', 'hover:bg-slate-200', 'dark:hover:bg-slate-700', 'hover:text-slate-900', 'dark:hover:text-white', 'border', 'border-slate-200', 'dark:border-slate-700'); activeTab.classList.add('bg-gradient-to-r', 'from-violet-600', 'to-fuchsia-600', 'text-white', 'shadow-lg', 'shadow-violet-500/25'); document.getElementById('clients-tab').classList.add('hidden'); document.getElementById('snapshots-tab').classList.add('hidden'); document.getElementById('admins-tab').classList.add('hidden'); document.getElementById(tab + '-tab').classList.remove('hidden'); if (tab === 'snapshots') loadSnapshots(); if (tab === 'admins') loadAdmins(); } // Modal functions function showModal(id) { const modal = document.getElementById(id); modal.classList.remove('hidden'); modal.classList.add('flex'); } function closeModal(id) { const modal = document.getElementById(id); modal.classList.add('hidden'); modal.classList.remove('flex'); } // Edit client async function editClient(clientId) { try { const res = await fetch('/admin/client?client_id=' + clientId); const data = await res.json(); const c = data.client; document.getElementById('edit-client-id').value = c.client_id; document.getElementById('edit-client-apikey').value = ''; document.getElementById('edit-client-storage').value = c.storage_type; document.getElementById('edit-client-dataset').value = c.dataset; document.getElementById('edit-client-quota').value = Math.round(c.max_size_bytes / (1024*1024*1024)); document.getElementById('edit-client-enabled').checked = c.enabled; if (c.rotation_policy) { document.getElementById('edit-client-hourly').value = c.rotation_policy.keep_hourly || 0; document.getElementById('edit-client-daily').value = c.rotation_policy.keep_daily || 0; document.getElementById('edit-client-weekly').value = c.rotation_policy.keep_weekly || 0; document.getElementById('edit-client-monthly').value = c.rotation_policy.keep_monthly || 0; } showModal('edit-client-modal'); } catch (e) { alert('Failed to load client data'); } } // Delete client async function deleteClient(clientId) { if (!confirm('Are you sure you want to delete client "' + clientId + '"? This will also delete all snapshots.')) { return; } try { const res = await fetch('/admin/client/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ client_id: clientId }) }); const data = await res.json(); if (data.success) { loadClients(); loadStats(); } else { alert(data.message || 'Failed to delete client'); } } catch (e) { alert('Failed to delete client'); } } // Delete snapshot async function deleteSnapshot(clientId, snapshotId) { if (!confirm('Are you sure you want to delete snapshot "' + snapshotId + '"?')) { return; } try { const res = await fetch('/admin/snapshot/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ client_id: clientId, snapshot_id: snapshotId }) }); const data = await res.json(); if (data.success) { loadSnapshots(); loadStats(); } else { alert(data.message || 'Failed to delete snapshot'); } } catch (e) { alert('Failed to delete snapshot'); } } // Delete admin async function deleteAdmin(adminId) { if (!confirm('Are you sure you want to delete this admin?')) { return; } try { const res = await fetch('/admin/admin/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ admin_id: adminId }) }); const data = await res.json(); if (data.success) { loadAdmins(); } else { alert(data.message || 'Failed to delete admin'); } } catch (e) { alert('Failed to delete admin'); } } // Show change password modal function showChangePassword(adminId, username) { document.getElementById('change-password-admin-id').value = adminId; document.getElementById('change-password-username').value = username; document.getElementById('change-password-new').value = ''; document.getElementById('change-password-confirm').value = ''; showModal('change-password-modal'); } // Show client change password modal function showClientChangePassword(clientId) { document.getElementById('client-password-client-id').value = clientId; document.getElementById('client-password-client-name').value = clientId; document.getElementById('client-password-new').value = ''; document.getElementById('client-password-confirm').value = ''; showModal('client-password-modal'); } // Reset client password async function resetClientPassword(clientId) { if (!confirm('Are you sure you want to reset the API key for "' + clientId + '"? A new random key will be generated.')) { return; } try { const res = await fetch('/admin/client/reset-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ client_id: clientId }) }); const data = await res.json(); if (data.success) { alert('New API key for ' + clientId + ': ' + data.api_key + '\n\nPlease save this key as it will not be shown again.'); } else { alert(data.message || 'Failed to reset API key'); } } catch (e) { alert('Failed to reset API key'); } } // Event delegation for click handlers document.addEventListener('click', function(e) { const target = e.target; // Handle modal content clicks - stop propagation if (target.classList.contains('modal-content') || target.closest('.modal-content')) { // Don't process if clicking on close button if (!target.classList.contains('modal-close')) { return; } } // Handle modal close buttons if (target.classList.contains('modal-close')) { const modalId = target.getAttribute('data-modal-id'); if (modalId) { closeModal(modalId); } return; } // Handle modal overlay clicks (close on backdrop click) if (target.classList.contains('fixed') && target.hasAttribute('data-modal-id')) { closeModal(target.id); return; } // Handle data-action buttons const action = target.getAttribute('data-action'); if (!action) return; switch (action) { case 'show-modal': const modalId = target.getAttribute('data-modal'); if (modalId) showModal(modalId); break; case 'edit-client': const clientId = target.getAttribute('data-client-id'); if (clientId) editClient(clientId); break; case 'delete-client': const delClientId = target.getAttribute('data-client-id'); if (delClientId) deleteClient(delClientId); break; case 'set-client-key': const setKeyClientId = target.getAttribute('data-client-id'); if (setKeyClientId) showClientChangePassword(setKeyClientId); break; case 'reset-client-key': const resetKeyClientId = target.getAttribute('data-client-id'); if (resetKeyClientId) resetClientPassword(resetKeyClientId); break; case 'delete-snapshot': const snapClientId = target.getAttribute('data-client-id'); const snapshotId = target.getAttribute('data-snapshot-id'); if (snapClientId && snapshotId) deleteSnapshot(snapClientId, snapshotId); break; case 'change-admin-password': const adminId = target.getAttribute('data-admin-id'); const adminUsername = target.getAttribute('data-admin-username'); if (adminId && adminUsername) showChangePassword(adminId, adminUsername); break; case 'delete-admin': const delAdminId = target.getAttribute('data-admin-id'); if (delAdminId) deleteAdmin(parseInt(delAdminId)); break; } }); // Tab click handler document.addEventListener('click', function(e) { const tab = e.target.getAttribute('data-tab'); if (tab) { showTab(tab); } }); // Snapshot filter change handler document.addEventListener('change', function(e) { if (e.target.id === 'snapshot-client-filter') { loadSnapshots(); } }); // Add client form document.getElementById('add-client-form').addEventListener('submit', async (e) => { e.preventDefault(); const client = { client_id: document.getElementById('new-client-id').value, api_key: document.getElementById('new-client-apikey').value, storage_type: document.getElementById('new-client-storage').value, dataset: document.getElementById('new-client-dataset').value, max_size_bytes: parseInt(document.getElementById('new-client-quota').value) * 1024 * 1024 * 1024, enabled: document.getElementById('new-client-enabled').checked, rotation_policy: { keep_hourly: parseInt(document.getElementById('new-client-hourly').value) || 0, keep_daily: parseInt(document.getElementById('new-client-daily').value) || 0, keep_weekly: parseInt(document.getElementById('new-client-weekly').value) || 0, keep_monthly: parseInt(document.getElementById('new-client-monthly').value) || 0 } }; try { const res = await fetch('/admin/client/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(client) }); const data = await res.json(); if (data.success) { closeModal('add-client-modal'); document.getElementById('add-client-form').reset(); loadClients(); loadStats(); } else { alert(data.message || 'Failed to create client'); } } catch (e) { alert('Failed to create client'); } }); // Edit client form document.getElementById('edit-client-form').addEventListener('submit', async (e) => { e.preventDefault(); const client = { client_id: document.getElementById('edit-client-id').value, api_key: document.getElementById('edit-client-apikey').value, storage_type: document.getElementById('edit-client-storage').value, dataset: document.getElementById('edit-client-dataset').value, max_size_bytes: parseInt(document.getElementById('edit-client-quota').value) * 1024 * 1024 * 1024, enabled: document.getElementById('edit-client-enabled').checked, rotation_policy: { keep_hourly: parseInt(document.getElementById('edit-client-hourly').value) || 0, keep_daily: parseInt(document.getElementById('edit-client-daily').value) || 0, keep_weekly: parseInt(document.getElementById('edit-client-weekly').value) || 0, keep_monthly: parseInt(document.getElementById('edit-client-monthly').value) || 0 } }; try { const res = await fetch('/admin/client/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(client) }); const data = await res.json(); if (data.success) { closeModal('edit-client-modal'); loadClients(); loadStats(); } else { alert(data.message || 'Failed to update client'); } } catch (e) { alert('Failed to update client'); } }); // Add admin form document.getElementById('add-admin-form').addEventListener('submit', async (e) => { e.preventDefault(); const admin = { username: document.getElementById('new-admin-username').value, password: document.getElementById('new-admin-password').value, role: document.getElementById('new-admin-role').value }; try { const res = await fetch('/admin/admin/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(admin) }); const data = await res.json(); if (data.success) { closeModal('add-admin-modal'); document.getElementById('add-admin-form').reset(); loadAdmins(); } else { alert(data.message || 'Failed to create admin'); } } catch (e) { alert('Failed to create admin'); } }); // Change password form document.getElementById('change-password-form').addEventListener('submit', async (e) => { e.preventDefault(); const adminId = document.getElementById('change-password-admin-id').value; const newPassword = document.getElementById('change-password-new').value; const confirmPassword = document.getElementById('change-password-confirm').value; if (newPassword !== confirmPassword) { alert('Passwords do not match'); return; } try { const res = await fetch('/admin/admin/change-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ admin_id: parseInt(adminId), new_password: newPassword }) }); const data = await res.json(); if (data.success) { closeModal('change-password-modal'); } else { alert(data.message || 'Failed to change password'); } } catch (e) { alert('Failed to change password'); } }); // Client password form document.getElementById('client-password-form').addEventListener('submit', async (e) => { e.preventDefault(); const clientId = document.getElementById('client-password-client-id').value; const newKey = document.getElementById('client-password-new').value; const confirmKey = document.getElementById('client-password-confirm').value; if (newKey !== confirmKey) { alert('API keys do not match'); return; } try { const res = await fetch('/admin/client/set-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ client_id: clientId, new_api_key: newKey }) }); const data = await res.json(); if (data.success) { closeModal('client-password-modal'); } else { alert(data.message || 'Failed to set API key'); } } catch (e) { alert('Failed to set API key'); } }); // Initialize checkAuth(); loadStats(); loadClients();