← 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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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>