"""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)