Skip to main content

mentedb_server/
console.rs

1//! A tiny, dependency-free management console bundled into the server binary and
2//! served at `GET /console`. It polls the aggregate `/metrics` endpoint and renders
3//! a live health view (up, uptime, memories, cluster nodes, CPU, memory, request
4//! rate and latency), so self-hosters get a management UI with no extra deploy and
5//! no build step. It reads only the already-public `/metrics`, so it needs no auth
6//! of its own.
7
8use axum::response::Html;
9
10const PAGE: &str = r##"<!doctype html>
11<html lang="en">
12<head>
13<meta charset="utf-8" />
14<meta name="viewport" content="width=device-width, initial-scale=1" />
15<title>MenteDB console</title>
16<style>
17  :root { color-scheme: dark; }
18  * { box-sizing: border-box; }
19  body { margin: 0; background: #0a0a0b; color: #e4e4e7; font: 14px/1.5 ui-sans-serif, system-ui, -apple-system, sans-serif; }
20  header { padding: 20px 28px; border-bottom: 1px solid #27272a; display: flex; align-items: center; gap: 12px; }
21  header h1 { font-size: 16px; font-weight: 600; margin: 0; }
22  .dot { width: 10px; height: 10px; border-radius: 50%; background: #52525b; }
23  .dot.up { background: #34d399; box-shadow: 0 0 10px rgba(52,211,153,.6); }
24  .dot.down { background: #f87171; }
25  main { padding: 24px 28px; max-width: 1100px; margin: 0 auto; }
26  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 14px; }
27  .card { border: 1px solid #27272a; background: #131316; border-radius: 12px; padding: 16px 18px; }
28  .card .label { color: #a1a1aa; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; }
29  .card .value { font-size: 26px; font-weight: 600; margin-top: 6px; font-variant-numeric: tabular-nums; }
30  .card .value.ok { color: #34d399; }
31  .card .sub { color: #71717a; font-size: 12px; margin-top: 2px; }
32  .muted { color: #71717a; font-size: 12px; margin-top: 20px; }
33  a { color: #34d399; text-decoration: none; }
34</style>
35</head>
36<body>
37<header>
38  <span id="dot" class="dot"></span>
39  <h1>MenteDB console</h1>
40  <span id="uptime" class="sub" style="color:#71717a"></span>
41</header>
42<main>
43  <div class="grid" id="grid"></div>
44  <p class="muted">Live from <a href="/metrics">/metrics</a>, refreshed every 2s. Aggregate only, no per-account data. For full time-series history, scrape with Prometheus and import the Grafana dashboard.</p>
45</main>
46<script>
47function parse(text) {
48  const m = {}; // name (no labels) -> summed value
49  for (const line of text.split("\n")) {
50    if (!line || line[0] === "#") continue;
51    const sp = line.lastIndexOf(" ");
52    if (sp < 0) continue;
53    let key = line.slice(0, sp).trim();
54    const val = parseFloat(line.slice(sp + 1));
55    if (!isFinite(val)) continue;
56    const name = key.indexOf("{") >= 0 ? key.slice(0, key.indexOf("{")) : key;
57    m[name] = (m[name] || 0) + val;
58  }
59  return m;
60}
61function fmtDur(s) {
62  s = Math.floor(s);
63  const d = Math.floor(s/86400); s%=86400;
64  const h = Math.floor(s/3600); s%=3600;
65  const mi = Math.floor(s/60);
66  return (d?d+"d ":"") + (h?h+"h ":"") + mi + "m";
67}
68function fmtBytes(b) {
69  if (!b) return "-";
70  const u = ["B","KB","MB","GB","TB"]; let i = 0;
71  while (b >= 1024 && i < u.length-1) { b/=1024; i++; }
72  return b.toFixed(1) + " " + u[i];
73}
74let prev = null, prevT = 0;
75function card(label, value, cls, sub) {
76  return `<div class="card"><div class="label">${label}</div><div class="value ${cls||""}">${value}</div>${sub?`<div class="sub">${sub}</div>`:""}</div>`;
77}
78async function tick() {
79  let m, ok = true;
80  try { m = parse(await (await fetch("/metrics", {cache:"no-store"})).text()); }
81  catch { ok = false; m = {}; }
82  const up = ok && m["mentedb_up"] === 1;
83  document.getElementById("dot").className = "dot " + (up ? "up" : "down");
84  document.getElementById("uptime").textContent = up ? "up " + fmtDur(m["mentedb_uptime_seconds"]||0) : "unreachable";
85
86  const now = Date.now()/1000;
87  let reqRate = null, cpu = null;
88  if (prev && now > prevT) {
89    const dt = now - prevT;
90    if (m["mentedb_http_requests_total"] != null && prev["mentedb_http_requests_total"] != null)
91      reqRate = Math.max(0, (m["mentedb_http_requests_total"] - prev["mentedb_http_requests_total"]) / dt);
92    if (m["process_cpu_seconds_total"] != null && prev["process_cpu_seconds_total"] != null)
93      cpu = Math.max(0, (m["process_cpu_seconds_total"] - prev["process_cpu_seconds_total"]) / dt);
94  }
95  prev = m; prevT = now;
96
97  const cards = [
98    card("Status", up ? "Up" : "Down", up ? "ok" : ""),
99    card("Memories stored", (m["mentedb_memory_count"]||0).toLocaleString()),
100    card("Live cluster nodes", m["mentedb_cluster_live_nodes"] != null ? m["mentedb_cluster_live_nodes"] : "-"),
101    card("Requests / sec", reqRate == null ? "…" : reqRate.toFixed(1)),
102    card("CPU (cores)", cpu == null ? (isFinite(m["process_cpu_seconds_total"]) ? "…" : "n/a") : cpu.toFixed(2)),
103    card("Resident memory", isFinite(m["process_resident_memory_bytes"]) ? fmtBytes(m["process_resident_memory_bytes"]) : "n/a"),
104  ];
105  document.getElementById("grid").innerHTML = cards.join("");
106}
107tick(); setInterval(tick, 2000);
108</script>
109</body>
110</html>
111"##;
112
113/// `GET /console`: the bundled management console.
114pub async fn handler() -> Html<&'static str> {
115    Html(PAGE)
116}