<!doctype html>
<!--
  Minimal single-file dashboard. Auto-refresh every 30s.
  Endpoints expected:
    GET /api/health            -> {status: "ok"}
    GET /api/<provider>/quota  -> list of accounts with windows
    GET /api/config            -> masked config state

  Copy this file to `<project>/static/index.html` and start. The card renderer
  auto-adapts to unit=="%" vs unit=="requests" semantics.
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>quota</title>
<style>
  :root { --bg:#0d1117; --card:#161b22; --border:#30363d; --fg:#e6edf3;
          --muted:#8b949e; --accent:#58a6ff; --good:#2ea043; --warn:#d29922;
          --bad:#f85149; --good-bg:rgba(46,160,67,.15); --warn-bg:rgba(210,153,34,.15); --bad-bg:rgba(248,81,73,.15); }
  *{box-sizing:border-box}
  body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;background:var(--bg);color:var(--fg);min-height:100vh}
  header{padding:24px 32px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px}
  header h1{margin:0;font-size:22px;font-weight:600}
  header .meta{font-size:13px;color:var(--muted)}
  main{padding:24px 32px 64px;max-width:1200px;margin:0 auto}
  section{margin-bottom:32px}
  section h2{margin:0 0 16px;font-size:15px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:1px}
  .grid{display:grid;gap:16px;grid-template-columns:repeat(auto-fill,minmax(320px,1fr))}
  .card{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:18px 20px}
  .card.error{border-color:var(--bad);background:var(--bad-bg)}
  .card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}
  .card-name{font-weight:600;font-size:15px}
  .card-workspace{font-size:11px;color:var(--muted);font-family:ui-monospace,monospace}
  .window{margin-top:14px}
  .window:first-of-type{margin-top:0}
  .window-label{display:flex;justify-content:space-between;font-size:12px;color:var(--muted);margin-bottom:6px}
  .bar{height:8px;background:#21262d;border-radius:4px;overflow:hidden}
  .bar-fill{height:100%;transition:width .4s ease,background-color .4s ease;background:var(--accent)}
  .bar-fill.good{background:var(--good)}.bar-fill.warn{background:var(--warn)}.bar-fill.bad{background:var(--bad)}
  .window-numbers{display:flex;justify-content:space-between;margin-top:4px;font-size:12px;font-variant-numeric:tabular-nums}
  .err-msg{color:var(--bad);font-size:12px;font-family:ui-monospace,monospace;background:var(--bad-bg);padding:6px 8px;border-radius:4px;margin-top:10px}
  .empty{padding:32px;text-align:center;color:var(--muted);font-size:13px}
  .badge{display:inline-block;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:500;background:#21262d;color:var(--muted)}
  .badge.ok{background:var(--good-bg);color:var(--good)}
  .badge.err{background:var(--bad-bg);color:var(--bad)}
  button.refresh-btn{background:#21262d;color:var(--fg);border:1px solid var(--border);border-radius:6px;padding:6px 12px;font-size:12px;cursor:pointer}
  button.refresh-btn:disabled{opacity:.5;cursor:wait}
</style>
</head>
<body>
<header>
  <div>
    <h1>quota</h1>
    <div class="meta"><span id="updated-at">never</span> · auto-refresh 30s · <button class="refresh-btn" id="refresh-btn">refresh now</button></div>
  </div>
  <div><span class="badge" id="health-badge">…</span></div>
</header>
<main>
  <!-- One section per provider; copy-paste as needed. -->
  <section id="section-minimax">
    <h2>MiniMax <span class="count" id="minimax-count"></span></h2>
    <div class="grid" id="minimax-grid"><div class="empty">loading…</div></div>
  </section>
  <section id="section-opencode">
    <h2>OpenCode Go <span class="count" id="opencode-count"></span></h2>
    <div class="grid" id="opencode-grid"><div class="empty">loading…</div></div>
  </section>
</main>
<script>
const $ = (id) => document.getElementById(id);
function fmtCountdown(sec) {
  if (sec <= 0) return "resetting…";
  const h = Math.floor(sec / 3600), m = Math.floor((sec % 3600) / 60), s = sec % 60;
  if (h > 0) return `${h}h ${m}m`;
  if (m > 0) return `${m}m ${s}s`;
  return `${s}s`;
}
function severity(p) { return p < 60 ? "good" : p < 90 ? "warn" : "bad"; }
function escapeHTML(s) {
  return String(s).replace(/[&<>"']/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[c]);
}
function renderWindow(w) {
  // unit==="%" → w.used and w.remaining are 0-100 percent. Otherwise absolute units.
  let p, used, left;
  if (w.unit === "%") {
    p = Math.min(100, Math.max(0, w.used));
    used = `${w.used.toFixed(0)}%`;
    left = `${w.remaining.toFixed(0)}% left`;
  } else {
    p = w.total > 0 ? (w.used / w.total) * 100 : 0;
    used = `${w.used.toFixed(0)} / ${w.total.toFixed(0)} used`;
    left = `${w.remaining.toFixed(0)} left`;
  }
  return `<div class="window">
    <div class="window-label"><span>${escapeHTML(w.label)}</span>
      <span>reset in ${fmtCountdown(w.reset_in_sec)}</span></div>
    <div class="bar"><div class="bar-fill ${severity(p)}" style="width:${p}%"></div></div>
    <div class="window-numbers"><span class="used">${used}</span><span>${left}</span></div>
  </div>`;
}
function renderAccount(a) {
  const errClass = a.success ? "" : " error";
  const badge = a.success
    ? `<span class="badge ok">ok</span>`
    : `<span class="badge err">error</span>`;
  const ws = a.workspace ? `<span class="card-workspace">${escapeHTML(a.workspace)}</span>` : "";
  const body = a.success
    ? (a.windows && a.windows.length ? a.windows.map(renderWindow).join("") : '<div class="empty">no windows</div>')
    : `<div class="err-msg">${escapeHTML(a.error || "unknown error")}</div>`;
  return `<div class="card${errClass}">
    <div class="card-header">
      <span class="card-name">${escapeHTML(a.name)}</span>${ws}${badge}
    </div>${body}
  </div>`;
}
function renderEmpty(msg) { return `<div class="empty" style="grid-column:1/-1">${escapeHTML(msg)}</div>`; }
async function fetchJSON(path) {
  const r = await fetch(path, { cache: "no-store" });
  if (!r.ok) throw new Error(`${path} → HTTP ${r.status}`);
  return await r.json();
}
async function reload() {
  const btn = $("refresh-btn");
  btn.disabled = true;
  try {
    const h = await fetchJSON("/api/health").catch(() => null);
    const badge = $("health-badge");
    badge.textContent = h && h.status === "ok" ? "healthy" : "down";
    badge.className = "badge " + (h && h.status === "ok" ? "ok" : "err");

    const providers = [
      { id: "minimax", endpoint: "/api/MiniMax/quota" },
      { id: "opencode", endpoint: "/api/opencode_go/quota" },
    ];
    for (const p of providers) {
      const items = await fetchJSON(p.endpoint);
      const grid = $(p.id + "-grid");
      const count = $(p.id + "-count");
      grid.innerHTML = items.length ? items.map(renderAccount).join("") : renderEmpty("no accounts configured");
      count.textContent = items.length ? `(${items.length})` : "";
    }
    $("updated-at").textContent = "updated " + new Date().toLocaleTimeString();
  } catch (e) {
    $("health-badge").textContent = "error";
    $("health-badge").className = "badge err";
    $("updated-at").textContent = "error: " + e.message;
  } finally {
    btn.disabled = false;
  }
}
$("refresh-btn").addEventListener("click", reload);
setInterval(reload, 30000);
reload();
</script>
</body>
</html>
