Add svg gauges

This commit is contained in:
Peter Stockings
2025-12-22 13:17:32 +11:00
parent 2f75b9669b
commit 0fb8ef6f60
2 changed files with 89 additions and 0 deletions

62
app.py
View File

@@ -3,6 +3,7 @@ import json
import subprocess
from datetime import datetime
from flask import Flask, jsonify, render_template
import re
app = Flask(__name__)
@@ -11,6 +12,12 @@ APP_DOMAIN = os.getenv("APP_DOMAIN", "peterstockings.com")
DOCKER = os.getenv("DOCKER_BIN", "/usr/bin/docker")
SHOW_INFRA = os.getenv("SHOW_INFRA", "1") == "1"
_UNIT = {
"b": 1,
"kb": 1000, "mb": 1000**2, "gb": 1000**3, "tb": 1000**4,
"kib": 1024, "mib": 1024**2, "gib": 1024**3, "tib": 1024**4,
}
# Optional JSON map: {"gitea":"https://gitea.peterstockings.com", "bloodpressure":"https://bp.peterstockings.com"}
APP_URL_OVERRIDES = {}
try:
@@ -199,16 +206,71 @@ def collect():
sysinfo["mem_total_h"] = mem_total_h
# --- Gauges (live-ish) ---
total_cpu_pct = 0.0
total_mem_used_bytes = 0
for name, s in stats.items():
total_cpu_pct += pct_str_to_float(s.get("cpu", "0%"))
total_mem_used_bytes += parse_human_bytes(s.get("mem_used", "0B"))
sysinfo = system_summary()
# Host total RAM (bytes) comes from docker info
host_mem_total = int(sysinfo.get("mem_total") or 0)
ram_pct = (total_mem_used_bytes / host_mem_total * 100.0) if host_mem_total else 0.0
# Docker disk: images "Size" and "Reclaimable"
df_images = sysinfo.get("system_df", {}).get("Images", {})
images_size_bytes = parse_human_bytes(df_images.get("size", "0B"))
# Reclaimable looks like "13.93GB (91%)" so grab the first token
reclaimable_raw = (df_images.get("reclaimable") or "").split(" ", 1)[0]
images_reclaimable_bytes = parse_human_bytes(reclaimable_raw) if reclaimable_raw else 0
images_used_bytes = max(0, images_size_bytes - images_reclaimable_bytes)
disk_pct = (images_used_bytes / images_size_bytes * 100.0) if images_size_bytes else 0.0
gauges = {
"cpu_total_pct": clamp(total_cpu_pct), # sum of container CPU%, can exceed 100 if multi-core; we clamp for display
"ram_used_bytes": total_mem_used_bytes,
"ram_total_bytes": host_mem_total,
"ram_pct": clamp(ram_pct),
"docker_images_size_bytes": images_size_bytes,
"docker_images_used_bytes": images_used_bytes,
"docker_images_pct": clamp(disk_pct),
}
return {
"generated_at": datetime.utcnow().isoformat() + "Z",
"poll_seconds": POLL_SECONDS,
"domain": APP_DOMAIN,
"system": sysinfo,
"gauges": gauges,
"apps": apps,
"infra": infra,
"warnings": warnings,
}
def parse_human_bytes(s: str) -> int:
# Handles "58.84MiB", "145.1MB", "423B"
s = s.strip()
m = re.match(r"^([0-9]*\.?[0-9]+)\s*([A-Za-z]+)$", s)
if not m:
return 0
val = float(m.group(1))
unit = m.group(2).lower()
return int(val * _UNIT.get(unit, 0))
def pct_str_to_float(p: str) -> float:
try:
return float(p.strip().replace("%", ""))
except Exception:
return 0.0
def clamp(n: float, lo: float = 0.0, hi: float = 100.0) -> float:
return max(lo, min(hi, n))
@app.get("/")
def index():
return render_template("index.html", poll_seconds=POLL_SECONDS)

View File

@@ -1,3 +1,29 @@
<h2>Live usage</h2>
<div style="display:grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin: 12px 0 18px 0;">
{{ donut("CPU (containers)", data.gauges.cpu_total_pct, "Sum of container CPU%") }}
{{ donut("RAM (containers)", data.gauges.ram_pct, "Container RAM vs host total") }}
{{ donut("Docker images", data.gauges.docker_images_pct, "Used vs total image store") }}
</div>
{% macro donut(label, pct, subtitle) %}
{% set r = 22 %}
{% set c = 2 * 3.1415926 * r %}
{% set dash = (pct / 100.0) * c %}
<div style="border:1px solid #ddd; border-radius: 12px; padding: 12px; display:flex; gap: 12px; align-items:center;">
<svg width="64" height="64" viewBox="0 0 64 64" aria-label="{{ label }}">
<circle cx="32" cy="32" r="{{ r }}" fill="none" stroke="#eee" stroke-width="8" />
<circle cx="32" cy="32" r="{{ r }}" fill="none" stroke="#111" stroke-width="8" stroke-linecap="round"
stroke-dasharray="{{ dash }} {{ c - dash }}" transform="rotate(-90 32 32)" />
<text x="32" y="36" text-anchor="middle" font-size="14" font-family="system-ui" fill="#111">{{ pct|round(0)
}}%</text>
</svg>
<div>
<div style="font-weight:700;">{{ label }}</div>
<div class="muted">{{ subtitle }}</div>
</div>
</div>
{% endmacro %}
<h2>System</h2>
<div style="display:grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin: 12px 0 18px 0;">
<div style="border:1px solid #ddd; border-radius: 10px; padding: 12px;">
@@ -21,6 +47,7 @@
</div>
</div>
<h3>Docker disk usage</h3>
<table>
<thead>