import { api, protectedFetch } from "./api.js?v=__ASSET_VERSION__"; import { initSideNav, loadNavUser } from "./nav.js?v=__ASSET_VERSION__"; import { showToast } from "./toast.js?v=__ASSET_VERSION__"; let currentUsers = []; let planFilterUserId = null; let currentUserId = null; function fmtDate(value) { return value ? new Date(value).toLocaleString() : "—"; } async function loadMetrics() { const grid = document.getElementById("metrics"); try { const { metrics } = await api("/api/admin/metrics"); const tiles = [ ["Users", metrics.totalUsers], ["Plans", metrics.totalPlans], ["Plans updated (7d)", metrics.plansUpdatedLast7Days], ["Active sessions", metrics.activeSessions], ]; grid.replaceChildren( ...tiles.map(([label, value]) => { const tile = document.createElement("div"); tile.className = "stat-tile"; tile.innerHTML = `
`; tile.querySelector(".stat-tile-label").textContent = label; tile.querySelector(".stat-tile-value").textContent = value; return tile; }), ); } catch { grid.textContent = ""; } } async function loadResetLinks() { try { const { links } = await api("/api/admin/password-resets"); const card = document.getElementById("reset-links-card"); if (!links.length) { card.hidden = true; return; } card.hidden = false; document.getElementById("resets-body").replaceChildren( ...links.map((link) => { const tr = document.createElement("tr"); const email = document.createElement("td"); email.textContent = link.email; const url = document.createElement("td"); // Not a clickable : an admin's own session immediately 302s /reset to /app, // so the link is only useful copied out and handed to the actual user. const copyBtn = document.createElement("button"); copyBtn.className = "ghost-btn small"; copyBtn.type = "button"; copyBtn.textContent = "Copy link"; copyBtn.addEventListener("click", async () => { try { await navigator.clipboard.writeText(link.url); copyBtn.textContent = "Copied!"; } catch { copyBtn.textContent = link.url; } setTimeout(() => (copyBtn.textContent = "Copy link"), 2000); }); url.append(copyBtn); const expires = document.createElement("td"); expires.textContent = fmtDate(link.expiresAt); tr.append(email, url, expires); return tr; }), ); } catch { /* optional feature; ignore failures */ } } function wireSignupToggle() { const toggle = document.getElementById("signup-toggle"); toggle.addEventListener("change", async () => { try { await api("/api/admin/signup-enabled", { method: "PUT", body: JSON.stringify({ enabled: toggle.checked }), }); showToast(toggle.checked ? "Signups enabled." : "Signups disabled."); } catch (error) { toggle.checked = !toggle.checked; showToast(error.message, "fail"); } }); } async function loadLlm() { const select = document.getElementById("llm-model-select"); const note = document.getElementById("llm-model-note"); try { const { current, models, modelsError } = await api("/api/admin/llm"); const auto = document.createElement("option"); auto.value = ""; auto.textContent = "Auto (first available model)"; select.replaceChildren( auto, ...models.map((model) => { const option = document.createElement("option"); option.value = model.key; option.textContent = `${model.name} (${model.provider})`; return option; }), ); // A previously-saved model that is no longer configured must still show as selected // rather than silently displaying Auto while the server keeps trying the saved one. if (current && !models.some((model) => model.key === current)) { const missing = document.createElement("option"); missing.value = current; missing.textContent = `${current} (no longer configured)`; select.append(missing); } select.value = current; note.textContent = modelsError ? "No models are configured on the server — reviews and prefill will fail until one is set up." : current ? "" : models.length ? `Auto currently resolves to ${models[0].name} (${models[0].provider}).` : ""; } catch { note.textContent = "Could not load LLM settings."; } } function wireLlmSave() { const button = document.getElementById("llm-model-save"); button.addEventListener("click", async () => { button.disabled = true; try { const model = document.getElementById("llm-model-select").value; await api("/api/admin/llm", { method: "PUT", body: JSON.stringify({ model }), }); showToast(model ? "LLM model saved." : "LLM model set to auto."); await loadLlm(); } catch (error) { showToast(error.message, "fail"); } finally { button.disabled = false; } }); } function wireBackupImport() { const input = document.getElementById("backup-import-file"); const note = document.getElementById("backup-note"); input.addEventListener("change", async (event) => { const file = event.target.files?.[0]; event.target.value = ""; if (!file) return; let backup; try { backup = JSON.parse(await file.text()); } catch { note.textContent = "That file is not valid JSON."; return; } if (backup.format !== "roast-planner-backup") { note.textContent = "That file is not a Roast Planner backup export (expected the file downloaded by “Export full backup”)."; return; } const userCount = backup.tables?.users?.length ?? 0; if ( !confirm( `Replace the ENTIRE database with "${file.name}" (${userCount} user${userCount === 1 ? "" : "s"}, exported ${backup.exportedAt ?? "unknown date"})?\n\nEverything currently stored will be deleted. This cannot be undone.`, ) ) return; note.textContent = "Importing…"; try { const result = await api("/api/admin/backup/import", { method: "POST", body: JSON.stringify(backup), }); note.textContent = `Imported: ${Object.entries(result.counts) .map(([table, count]) => `${table} ${count}`) .join(", ")}.`; showToast("Backup imported."); if (!result.sessionKept) { alert("The restored data does not include your current session's account — log in again with the restored credentials."); location.assign("/login"); return; } await Promise.all([loadMetrics(), loadUsers(), loadPlans(), loadAudit()]); } catch (error) { note.textContent = `Import failed: ${error.message}. Nothing was changed.`; showToast(error.message, "fail"); } }); } function roleBadge(user) { return user.role === "admin" ? `Admin` : `User`; } function statusBadge(user) { return user.disabled_at ? `Disabled` : `Active`; } async function loadUsers() { const body = document.getElementById("users-body"); try { const { users, signupEnabled } = await api("/api/admin/users"); currentUsers = users; document.getElementById("signup-toggle").checked = signupEnabled; renderUsers(); } catch { body.innerHTML = `