diff --git a/app.py b/app.py index d1c11f5..b435bd8 100644 --- a/app.py +++ b/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) diff --git a/templates/apps_table.html b/templates/apps_table.html index 2d5a4b6..b2eaa00 100644 --- a/templates/apps_table.html +++ b/templates/apps_table.html @@ -1,3 +1,29 @@ +

Live usage

+
+ {{ 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") }} +
+ +{% macro donut(label, pct, subtitle) %} +{% set r = 22 %} +{% set c = 2 * 3.1415926 * r %} +{% set dash = (pct / 100.0) * c %} +
+ + + + {{ pct|round(0) + }}% + +
+
{{ label }}
+
{{ subtitle }}
+
+
+{% endmacro %} +

System

@@ -21,6 +47,7 @@
+

Docker disk usage