<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Recall admin</title>
<style>
:root {
color-scheme: light dark;
--bg: #ffffff;
--fg: #1a1a1a;
--muted: #6b7280;
--border: #e5e7eb;
--row-alt: #fafafa;
--error: #b91c1c;
--accent: #111827;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #14161a;
--fg: #e8e8e8;
--muted: #9aa0a6;
--border: #2a2d33;
--row-alt: #1b1d22;
--error: #f87171;
--accent: #e8e8e8;
}
}
* { box-sizing: border-box; }
body {
margin: 0;
padding: 2.5rem 1.5rem;
background: var(--bg);
color: var(--fg);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
main {
max-width: 860px;
margin: 0 auto;
}
h1 {
font-size: 1.25rem;
font-weight: 600;
margin: 0 0 1.5rem;
}
.token-row {
display: flex;
gap: 0.5rem;
margin-bottom: 1.75rem;
}
input[type="password"], input[type="text"] {
flex: 1;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg);
color: var(--fg);
font-size: 0.9rem;
}
button {
padding: 0.5rem 1rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--accent);
color: var(--bg);
font-size: 0.9rem;
cursor: pointer;
}
button:hover { opacity: 0.85; }
.summary {
color: var(--muted);
font-size: 0.9rem;
margin-bottom: 1.5rem;
line-height: 1.6;
}
.error {
color: var(--error);
font-size: 0.9rem;
margin-bottom: 1.5rem;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
th, td {
text-align: left;
padding: 0.6rem 0.75rem;
border-bottom: 1px solid var(--border);
}
th {
color: var(--muted);
font-weight: 500;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.03em;
}
tbody tr:nth-child(even) { background: var(--row-alt); }
.empty {
color: var(--muted);
font-size: 0.9rem;
}
</style>
</head>
<body>
<main>
<h1>Recall admin</h1>
<div class="token-row">
<input type="password" id="token" placeholder="Bearer token">
<button id="load">Load</button>
</div>
<div id="summary" class="summary"></div>
<div id="error" class="error" style="display:none"></div>
<table id="table" style="display:none">
<thead>
<tr>
<th>Project</th>
<th>Active files</th>
<th>Deleted</th>
<th>Sources</th>
<th>Last updated</th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
</main>
<script>
(function () {
var STORAGE_KEY = "recall_admin_token";
var tokenInput = document.getElementById("token");
var loadBtn = document.getElementById("load");
var summaryEl = document.getElementById("summary");
var errorEl = document.getElementById("error");
var tableEl = document.getElementById("table");
var tbodyEl = document.getElementById("tbody");
function clearChildren(el) {
while (el.firstChild) el.removeChild(el.firstChild);
}
function showError(message) {
tableEl.style.display = "none";
clearChildren(summaryEl);
errorEl.textContent = message;
errorEl.style.display = "block";
}
function hideError() {
errorEl.style.display = "none";
errorEl.textContent = "";
}
function renderSummary(data) {
clearChildren(summaryEl);
var totals = data.totals || {};
var parts = [
"Projects: " + totals.project_count,
"Active files: " + totals.file_count,
"Deleted: " + totals.deleted_count,
"git_commit: " + data.git_commit,
"last_backup_at: " + (data.last_backup_at || "never"),
];
parts.forEach(function (text, i) {
if (i > 0) summaryEl.appendChild(document.createTextNode(" ยท "));
summaryEl.appendChild(document.createTextNode(text));
});
}
function renderTable(projects) {
clearChildren(tbodyEl);
if (!projects.length) {
var tr = document.createElement("tr");
var td = document.createElement("td");
td.colSpan = 5;
td.className = "empty";
td.textContent = "No projects yet.";
tr.appendChild(td);
tbodyEl.appendChild(tr);
tableEl.style.display = "";
return;
}
projects.forEach(function (p) {
var tr = document.createElement("tr");
var tdKey = document.createElement("td");
tdKey.textContent = p.project_key;
tr.appendChild(tdKey);
var tdActive = document.createElement("td");
tdActive.textContent = String(p.file_count);
tr.appendChild(tdActive);
var tdDeleted = document.createElement("td");
tdDeleted.textContent = String(p.deleted_count);
tr.appendChild(tdDeleted);
var tdSources = document.createElement("td");
tdSources.textContent = (p.sources || []).join(", ");
tr.appendChild(tdSources);
var tdUpdated = document.createElement("td");
tdUpdated.textContent = p.last_updated_at || "";
tr.appendChild(tdUpdated);
tbodyEl.appendChild(tr);
});
tableEl.style.display = "";
}
function load() {
var token = tokenInput.value.trim();
if (!token) {
showError("enter a token");
return;
}
hideError();
fetch("/admin/stats", { headers: { Authorization: "Bearer " + token } })
.then(function (resp) {
if (resp.status === 401) {
showError("invalid token");
return null;
}
if (!resp.ok) {
showError("request failed (" + resp.status + ")");
return null;
}
sessionStorage.setItem(STORAGE_KEY, token);
return resp.json();
})
.then(function (data) {
if (!data) return;
renderSummary(data);
renderTable(data.projects || []);
})
.catch(function () {
showError("request failed");
});
}
loadBtn.addEventListener("click", load);
tokenInput.addEventListener("keydown", function (e) {
if (e.key === "Enter") load();
});
var stored = sessionStorage.getItem(STORAGE_KEY);
if (stored) {
tokenInput.value = stored;
load();
}
})();
</script>
</body>
</html>