+
- HOST METRICS
- PROMETHEUS
+ CPU LOAD
+ --%
+ PROMETHEUS / NODE EXPORTER
+
+
+ MEMORY
+ --%
+ PROMETHEUS / NODE EXPORTER
+
+
+ HOST UPTIME
+ --
NODE EXPORTER
- CONTAINER METRICS
- CADVISOR
- DOCKER TELEMETRY
+ RUNNING CONTAINERS
+ --
+ CADVISOR VIA PROMETHEUS
- AVAILABILITY MONITORING
- KUMA
- ENDPOINT CHECKS
+ SCRAPE TARGETS
+ --
+ PROMETHEUS TARGET HEALTH
- LOGS
- LOKI
- CENTRALIZED LOGGING
-
-
- DASHBOARDS
- GRAFANA
- OPERATIONS / ANALYSIS
-
-
- IDENTITY
- AUTHENTIK
- OIDC / SSO / 2FA
+ TELEMETRY
+ CONNECTING
+ PROMETHEUS / CADVISOR
diff --git a/nginx.conf b/nginx.conf
new file mode 100644
index 0000000..2716f11
--- /dev/null
+++ b/nginx.conf
@@ -0,0 +1,34 @@
+server {
+ listen 80;
+ server_name _;
+
+ root /usr/share/nginx/html;
+ index index.html;
+
+ location = /api/status {
+ proxy_pass http://status-api:8080/status;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_connect_timeout 2s;
+ proxy_read_timeout 4s;
+ add_header Cache-Control "no-store, max-age=0" always;
+ }
+
+ location / {
+ try_files $uri $uri/ =404;
+ }
+
+ location ~ /. {
+ deny all;
+ return 404;
+ }
+
+ location ^~ /status-api/ {
+ return 404;
+ }
+
+ location = /nginx.conf {
+ return 404;
+ }
+}
diff --git a/script.js b/script.js
index 717ef57..4b056d8 100644
--- a/script.js
+++ b/script.js
@@ -1,4 +1,17 @@
const timeElement = document.querySelector("#local-time");
+const refreshElement = document.querySelector("#refresh-indicator");
+
+const fields = {
+ cpu: document.querySelector("#cpu-value"),
+ memory: document.querySelector("#memory-value"),
+ uptime: document.querySelector("#uptime-value"),
+ containers: document.querySelector("#containers-value"),
+ targets: document.querySelector("#targets-value"),
+ telemetry: document.querySelector("#telemetry-value"),
+};
+
+let lastSuccessfulUpdate = null;
+let requestInFlight = false;
function updateClock() {
const now = new Date();
@@ -11,5 +24,73 @@ function updateClock() {
}).format(now);
}
+function formatUptime(totalSeconds) {
+ const seconds = Math.max(0, Number(totalSeconds) || 0);
+ const days = Math.floor(seconds / 86400);
+ const hours = Math.floor((seconds % 86400) / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+
+ if (days > 0) return days + "d " + hours + "h";
+ if (hours > 0) return hours + "h " + minutes + "m";
+ return minutes + "m";
+}
+
+function setUnavailable() {
+ for (const key of ["cpu", "memory", "uptime", "containers", "targets"]) {
+ fields[key].textContent = "--";
+ }
+ fields.telemetry.textContent = "UNAVAILABLE";
+ fields.telemetry.classList.add("metric-error");
+ refreshElement.textContent = "TELEMETRY UNAVAILABLE";
+ refreshElement.classList.add("telemetry-error");
+}
+
+function renderStatus(data) {
+ fields.cpu.textContent = Number(data.host.cpu_percent).toFixed(1);
+ fields.memory.textContent = Number(data.host.memory_percent).toFixed(1);
+ fields.uptime.textContent = formatUptime(data.host.uptime_seconds);
+ fields.containers.textContent = String(data.containers.running);
+ fields.targets.textContent = data.prometheus.up + "/" + data.prometheus.total;
+ fields.telemetry.textContent = "LIVE";
+ fields.telemetry.classList.remove("metric-error");
+ refreshElement.classList.remove("telemetry-error");
+
+ lastSuccessfulUpdate = Date.now();
+ updateRefreshLabel();
+}
+
+function updateRefreshLabel() {
+ if (!lastSuccessfulUpdate) return;
+ const ageSeconds = Math.max(0, Math.floor((Date.now() - lastSuccessfulUpdate) / 1000));
+ refreshElement.textContent = "LIVE TELEMETRY ยท UPDATED " + String(ageSeconds).padStart(2, "0") + "s AGO";
+}
+
+async function refreshTelemetry() {
+ if (requestInFlight) return;
+ requestInFlight = true;
+
+ try {
+ const response = await fetch("/api/status", {
+ cache: "no-store",
+ headers: { "Accept": "application/json" },
+ });
+
+ if (!response.ok) throw new Error("status " + response.status);
+ const data = await response.json();
+ if (!data.ok) throw new Error("telemetry unavailable");
+
+ renderStatus(data);
+ } catch (error) {
+ console.error("ByteGeist telemetry request failed:", error);
+ setUnavailable();
+ } finally {
+ requestInFlight = false;
+ }
+}
+
updateClock();
+refreshTelemetry();
+
window.setInterval(updateClock, 1000);
+window.setInterval(updateRefreshLabel, 1000);
+window.setInterval(refreshTelemetry, 15000);
diff --git a/status-api/Dockerfile b/status-api/Dockerfile
new file mode 100644
index 0000000..a790b78
--- /dev/null
+++ b/status-api/Dockerfile
@@ -0,0 +1,6 @@
+FROM python:3.12-alpine
+WORKDIR /app
+COPY status_api.py /app/status_api.py
+USER nobody
+EXPOSE 8080
+CMD ["python", "/app/status_api.py"]
diff --git a/status-api/status_api.py b/status-api/status_api.py
new file mode 100644
index 0000000..b4fc861
--- /dev/null
+++ b/status-api/status_api.py
@@ -0,0 +1,91 @@
+import json
+import os
+import time
+import urllib.parse
+import urllib.request
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+
+PROMETHEUS_URL = os.getenv("PROMETHEUS_URL", "http://host.docker.internal:9090").rstrip("/")
+TIMEOUT = float(os.getenv("PROMETHEUS_TIMEOUT", "2.5"))
+
+QUERIES = {
+ "cpu_percent": '100-(avg(rate(node_cpu_seconds_total{mode="idle"}[2m]))*100)',
+ "memory_percent": '(1-(node_memory_MemAvailable_bytes/node_memory_MemTotal_bytes))*100',
+ "host_uptime_seconds": 'time()-node_boot_time_seconds',
+ "running_containers": 'count(count by (name) (container_last_seen{name!=""}))',
+}
+
+def get_json(url):
+ request = urllib.request.Request(url, headers={"Accept": "application/json"})
+ with urllib.request.urlopen(request, timeout=TIMEOUT) as response:
+ return json.load(response)
+
+def prometheus_query(expression):
+ url = f"{PROMETHEUS_URL}/api/v1/query?{urllib.parse.urlencode({'query': expression})}"
+ payload = get_json(url)
+ if payload.get("status") != "success":
+ raise RuntimeError("Prometheus query failed")
+ results = payload.get("data", {}).get("result", [])
+ if not results:
+ raise RuntimeError("Prometheus query returned no data")
+ return float(results[0]["value"][1])
+
+def target_health():
+ payload = get_json(f"{PROMETHEUS_URL}/api/v1/targets")
+ targets = payload.get("data", {}).get("activeTargets", [])
+ jobs = {}
+ for target in targets:
+ job = target.get("labels", {}).get("job", "unknown")
+ jobs[job] = target.get("health") == "up"
+ return {
+ "up": sum(1 for target in targets if target.get("health") == "up"),
+ "total": len(targets),
+ "jobs": jobs,
+ }
+
+def build_status():
+ values = {name: prometheus_query(query) for name, query in QUERIES.items()}
+ targets = target_health()
+ return {
+ "ok": True,
+ "source": "Prometheus / node_exporter / cAdvisor",
+ "sampled_at": int(time.time()),
+ "host": {
+ "cpu_percent": round(values["cpu_percent"], 1),
+ "memory_percent": round(values["memory_percent"], 1),
+ "uptime_seconds": int(values["host_uptime_seconds"]),
+ },
+ "containers": {"running": int(values["running_containers"])},
+ "prometheus": targets,
+ }
+
+class Handler(BaseHTTPRequestHandler):
+ def send_json(self, status_code, payload):
+ body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
+ self.send_response(status_code)
+ self.send_header("Content-Type", "application/json; charset=utf-8")
+ self.send_header("Cache-Control", "no-store, max-age=0")
+ self.send_header("Content-Length", str(len(body)))
+ self.send_header("X-Content-Type-Options", "nosniff")
+ self.end_headers()
+ self.wfile.write(body)
+
+ def do_GET(self):
+ if self.path == "/healthz":
+ self.send_json(200, {"ok": True})
+ return
+ if self.path != "/status":
+ self.send_json(404, {"ok": False, "error": "not_found"})
+ return
+ try:
+ self.send_json(200, build_status())
+ except Exception as exc:
+ self.send_json(503, {"ok": False, "sampled_at": int(time.time()), "error": type(exc).__name__})
+
+ def log_message(self, fmt, *args):
+ print(f"{self.address_string()} - {fmt % args}", flush=True)
+
+if __name__ == "__main__":
+ server = ThreadingHTTPServer(("0.0.0.0", 8080), Handler)
+ print("ByteGeist status API listening on :8080", flush=True)
+ server.serve_forever()
diff --git a/style.css b/style.css
index 592afe8..d7833ed 100644
--- a/style.css
+++ b/style.css
@@ -384,6 +384,19 @@ h1 {
color: var(--green);
}
+.metric strong.metric-value {
+ display: inline-block;
+ margin-top: 16px;
+ font: 500 clamp(1.35rem, 1.8vw, 2.15rem)/1 var(--mono);
+ letter-spacing: -.045em;
+ overflow-wrap: normal;
+}
+
+.metric .metric-error,
+.telemetry-error {
+ color: var(--amber) !important;
+}
+
.spark-bars {
height: 23px;
margin-top: 12px;