- Flask backend + static SPA (HTML/CSS/JS) - Docker container discovery via unix socket (stdlib only, no docker SDK) - Host stats (CPU/mem/disk/load/uptime) via psutil - Live-updating UI with per-state filters and resource gauges - Dockerfile for containerized deployment
124 lines
3.3 KiB
Python
124 lines
3.3 KiB
Python
"""ServerDash — self-hosted Docker + host monitoring dashboard.
|
|
|
|
Backend: Flask + a small REST API. Frontend: a static SPA
|
|
(index.html + css + js) served by the same Flask app.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import threading
|
|
import time
|
|
from datetime import datetime, timezone
|
|
|
|
from flask import (
|
|
Flask,
|
|
jsonify,
|
|
render_template,
|
|
request,
|
|
Response,
|
|
abort,
|
|
)
|
|
|
|
from docker_monitor import DockerMonitor
|
|
from host_stats import HostStats
|
|
|
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
app = Flask(
|
|
__name__,
|
|
static_folder=os.path.join(BASE_DIR, "static"),
|
|
template_folder=os.path.join(BASE_DIR, "templates"),
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Background refresh loop. Keeps the container list fresh so the dashboard
|
|
# always shows current data without blocking API responses.
|
|
# ---------------------------------------------------------------------------
|
|
docker = DockerMonitor()
|
|
host = HostStats()
|
|
REFRESH_INTERVAL = float(os.environ.get("REFRESH_INTERVAL", "3"))
|
|
_stop = False
|
|
|
|
|
|
def _background_refresh():
|
|
while not _stop:
|
|
try:
|
|
docker.refresh()
|
|
except Exception as exc: # keep the loop alive on transient errors
|
|
app.logger.warning("docker refresh failed: %s", exc)
|
|
time.sleep(REFRESH_INTERVAL)
|
|
|
|
|
|
_thread = threading.Thread(target=_background_refresh, daemon=True)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# API routes
|
|
# ---------------------------------------------------------------------------
|
|
@app.route("/api/v1/containers")
|
|
def api_containers():
|
|
"""Return the normalized list of discovered containers."""
|
|
# Allow a forced refresh.
|
|
if request.args.get("refresh") == "1":
|
|
try:
|
|
docker.refresh()
|
|
except Exception as exc:
|
|
return jsonify({"error": str(exc)}), 502
|
|
containers = docker.get()
|
|
return jsonify({"count": len(containers), "containers": containers})
|
|
|
|
|
|
@app.route("/api/v1/containers/summary")
|
|
def api_container_summary():
|
|
"""Aggregate counts by state class (used for the header cards)."""
|
|
containers = docker.get()
|
|
summary = {
|
|
"total": len(containers),
|
|
"running": 0,
|
|
"starting": 0,
|
|
"paused": 0,
|
|
"stopped": 0,
|
|
"dead": 0,
|
|
}
|
|
for c in containers:
|
|
key = c["state_class"]
|
|
if key in summary:
|
|
summary[key] += 1
|
|
return jsonify(summary)
|
|
|
|
|
|
@app.route("/api/v1/host")
|
|
def api_host():
|
|
"""Return host resource stats (cpu, memory, disk, load, uptime)."""
|
|
return jsonify(host.get())
|
|
|
|
|
|
@app.route("/api/v1/healthz")
|
|
def api_healthz():
|
|
return jsonify({"status": "ok", "time": datetime.now(timezone.utc).isoformat()})
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SPA
|
|
# ---------------------------------------------------------------------------
|
|
@app.route("/")
|
|
def index():
|
|
return render_template("index.html")
|
|
|
|
|
|
@app.errorhandler(404)
|
|
def not_found(_e):
|
|
return render_template("index.html"), 200
|
|
|
|
|
|
def create_server():
|
|
"""Return a configured wsgi app (used by `python -m` and gunicorn)."""
|
|
return app
|
|
|
|
|
|
if __name__ == "__main__":
|
|
_thread.start()
|
|
port = int(os.environ.get("PORT", "8787"))
|
|
host_bind = os.environ.get("HOST_BIND", "0.0.0.0")
|
|
app.run(host=host_bind, port=port, debug=False, threaded=True)
|