← back to Whatsmystyle
public/admin-cron.html
101 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="noindex,nofollow" />
<title>WhatsMyStyle — Cron diagnostics</title>
<link rel="stylesheet" href="/css/app.css" />
<style>
body { font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; background: #faf7f2; color: #1d1d1f; max-width: 1100px; margin: 40px auto; padding: 0 24px; }
h1 { font-size: 32px; font-weight: 600; letter-spacing: -0.02em; margin: 0 0 8px; }
.sub { color: #707070; margin-bottom: 24px; font-size: 14px; }
.nav { margin-bottom: 24px; }
.nav a { color: #707070; text-decoration: none; font-size: 14px; margin-right: 12px; }
.nav a:hover { color: #1d1d1f; }
.card { background: #fff; border: 1px solid #e6e1d8; border-radius: 16px; padding: 18px 22px; margin: 14px 0; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #f0eadf; vertical-align: top; }
th { font-weight: 600; color: #707070; font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; }
.kind-launchd { color: #1e6cff; }
.kind-setInterval { color: #5a3a1f; }
.pill { display: inline-block; background: #faf7f2; border: 1px solid #e6e1d8; padding: 3px 10px; border-radius: 999px; font-size: 11px; color: #555; }
.pill-good { background: #e6f6ec; border-color: #b7e0c4; color: #14572a; }
.pill-bad { background: #fbe4e4; border-color: #e9b8b8; color: #7a1717; }
.muted { color: #707070; }
.hint { font-size: 12px; color: #707070; max-width: 360px; }
h2 { font-size: 18px; font-weight: 600; margin: 0 0 14px; letter-spacing: -0.01em; }
.flag-row { display: flex; gap: 12px; flex-wrap: wrap; }
code { background: #efe9dd; padding: 2px 6px; border-radius: 5px; font-size: 12px; }
</style>
</head>
<body>
<div class="nav"><a href="/admin-config">← admin config</a><a href="/admin/drifts">drifts</a><a href="/">app home</a></div>
<h1>Cron diagnostics</h1>
<p class="sub">Every recurring task in the server + launchd. Last-run timestamps come from <code>app_config</code> updated_at columns or log file mtimes. Auto-refresh every 30s.</p>
<div class="card">
<h2>Flags</h2>
<div class="flag-row" id="flags-row"></div>
</div>
<div class="card">
<h2>Mac1</h2>
<div id="mac1-row" class="muted">checking…</div>
</div>
<div class="card">
<h2>Tasks</h2>
<table id="tasks-tbl">
<thead><tr><th>Task</th><th>Kind</th><th>Every</th><th>Last run</th><th>Notes</th></tr></thead>
<tbody></tbody>
</table>
</div>
<script>
const $ = (s) => document.querySelector(s);
function escapeHtml(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
function fmtInterval(s) {
if (s < 60) return s + 's';
if (s < 3600) return Math.round(s/60) + 'm';
if (s < 86400) return Math.round(s/3600) + 'h';
return Math.round(s/86400) + 'd';
}
function fmtSinceTs(ts) {
if (!ts) return '<span class="muted">never</span>';
const ms = Date.now() - new Date(ts).getTime();
if (isNaN(ms)) return '<span class="muted">' + escapeHtml(ts) + '</span>';
const s = Math.round(ms / 1000);
if (s < 60) return s + 's ago';
if (s < 3600) return Math.round(s/60) + 'm ago';
if (s < 86400) return Math.round(s/3600) + 'h ago';
return Math.round(s/86400) + 'd ago';
}
async function load() {
const r = await fetch('/api/admin/cron');
if (!r.ok) { document.body.innerHTML = '<p class="muted">admin gate failed</p>'; return; }
const j = await r.json();
$('#flags-row').innerHTML = Object.entries(j.flags || {}).map(([k, v]) =>
`<span class="pill ${v ? 'pill-good' : ''}">${escapeHtml(k)}: <strong>${v ? 'on' : 'off'}</strong></span>`).join('');
const m = j.mac1;
if (m) {
const cls = m.state === 'idle' ? 'pill-good' : m.state === 'busy' ? '' : 'pill-bad';
$('#mac1-row').innerHTML = `<span class="pill ${cls}">${m.state}</span> <span class="muted">${escapeHtml(m.detail || '')} · checked ${fmtSinceTs(m.checked_at)}</span>`;
} else $('#mac1-row').innerHTML = '<span class="muted">no status cached yet</span>';
$('#tasks-tbl tbody').innerHTML = (j.tasks || []).map(t => `
<tr>
<td><strong>${escapeHtml(t.name)}</strong></td>
<td class="kind-${escapeHtml(t.kind)}">${escapeHtml(t.kind)}</td>
<td>${fmtInterval(t.interval_s)}</td>
<td>${fmtSinceTs(t.last_run)}</td>
<td class="hint">${escapeHtml(t.hint || '')}</td>
</tr>
`).join('');
}
load();
setInterval(load, 30 * 1000);
</script>
</body>
</html>