summaryrefslogtreecommitdiff
path: root/web/js/admin.js
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-29 20:19:34 +0300
committerPaul Buetow <paul@buetow.org>2026-04-29 20:19:34 +0300
commit4a70c0ea083306cb79d0fa325d331ffa4333154a (patch)
tree84df7e2f781974ef45ac8c34f1a59e80fdd5c41d /web/js/admin.js
parentcf30414c2a0696cc75c615f4da66ea85d0522c42 (diff)
feat: create web PWA frontend
Diffstat (limited to 'web/js/admin.js')
-rw-r--r--web/js/admin.js101
1 files changed, 101 insertions, 0 deletions
diff --git a/web/js/admin.js b/web/js/admin.js
new file mode 100644
index 0000000..c992d1d
--- /dev/null
+++ b/web/js/admin.js
@@ -0,0 +1,101 @@
+import { API } from './api.js';
+
+export function initAdmin() {
+ const btn = document.getElementById('admin-toggle');
+ const modal = document.getElementById('admin-modal');
+ const closeBtn = document.getElementById('admin-close');
+ const rescanBtn = document.getElementById('admin-rescan');
+ const trashBtn = document.getElementById('admin-trash');
+ const form = document.getElementById('admin-user-form');
+
+ btn?.addEventListener('click', () => {
+ modal?.classList.add('open');
+ refreshAdmin();
+ });
+ closeBtn?.addEventListener('click', () => modal?.classList.remove('open'));
+ modal?.addEventListener('click', (e) => { if (e.target === modal) modal.classList.remove('open'); });
+
+ rescanBtn?.addEventListener('click', async () => {
+ rescanBtn.disabled = true;
+ try { await API.rescan(); toast('Rescan triggered'); }
+ catch (err) { toast(err.message, 'error'); }
+ finally { rescanBtn.disabled = false; }
+ });
+
+ trashBtn?.addEventListener('click', async () => {
+ try {
+ const data = await API.trash();
+ renderTrash(data);
+ } catch (err) { toast(err.message, 'error'); }
+ });
+
+ form?.addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const fd = new FormData(form);
+ const body = {
+ username: fd.get('username'),
+ password: fd.get('password'),
+ is_admin: !!fd.get('is_admin'),
+ };
+ try {
+ await API.createUser(body);
+ form.reset();
+ toast('User created');
+ await refreshAdmin();
+ } catch (err) { toast(err.message, 'error'); }
+ });
+}
+
+async function refreshAdmin() {
+ try {
+ const [users, perms] = await Promise.all([API.users(), API.permissions()]);
+ renderUsers(users);
+ renderPermissions(perms);
+ } catch (err) { toast(err.message, 'error'); }
+}
+
+function esc(s) {
+ return (s ?? '').replace(/[&<>"']/g, (c) => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[c]));
+}
+
+function renderUsers(users) {
+ const el = document.getElementById('admin-users');
+ if (!el) return;
+ el.innerHTML = `<ul style="list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:0.35rem;">
+ ${users.map((u) => `<li style="display:flex;align-items:center;gap:0.5rem;justify-content:space-between;padding:0.25rem 0.35rem;border-radius:var(--radius-sm);${u.is_admin ? 'background:var(--accent-soft);' : ''}">
+ <span>${esc(u.username)}${u.is_admin ? ' <span class="badge">admin</span>' : ''}</span>
+ <button class="btn btn-danger btn-sm" data-id="${u.id}">Remove</button>
+ </li>`).join('')}
+ </ul>`;
+ el.querySelectorAll('button[data-id]').forEach((b) => {
+ b.addEventListener('click', async () => {
+ try { await API.deleteUser(b.dataset.id); toast('User removed'); await refreshAdmin(); }
+ catch (err) { toast(err.message, 'error'); }
+ });
+ });
+}
+
+function renderPermissions(data) {
+ const el = document.getElementById('admin-permissions');
+ if (!el || !data) return;
+ el.innerHTML = `<p class="text-muted" style="font-size:0.8rem;">Use the API to manage permissions directly.</p>`;
+}
+
+function renderTrash(data) {
+ const el = document.getElementById('admin-users');
+ if (!el || !Array.isArray(data)) return;
+ el.innerHTML = `<h4 style="font-size:0.9rem;">Trash</h4><ul style="list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:0.35rem;">
+ ${data.map((m) => `<li style="display:flex;align-items:center;justify-content:space-between;padding:0.25rem 0.35rem;border-radius:var(--radius-sm);">
+ <span>${esc(m.file_name)}</span>
+ <span class="text-muted" style="font-size:0.75rem;">${esc(m.deleted_at || '')}</span>
+ </li>`).join('')}
+ </ul>`;
+}
+
+function toast(msg, type = 'info') {
+ const t = document.getElementById('toast');
+ if (!t) return;
+ t.textContent = msg;
+ t.className = 'toast show ' + type;
+ setTimeout(() => t.classList.remove('show'), 2800);
+}