Add svg gauges
This commit is contained in:
62
app.py
62
app.py
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user