#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ dashboard.py - MoFin management dashboard backend ================================================== Minimal Flask app on :5804. Monitors MoFin services and serves module specs (human_help + ai_spec) via ?§ button system. Adapted from AgentsMeeting dashboard.py. Does NOT modify server.py. """ import os, sys, json, socket, logging, time from pathlib import Path from datetime import datetime from flask import Flask, jsonify, request, send_from_directory # ---- Paths (auto-detect from script location) ---- _SCRIPT_DIR = Path(__file__).resolve().parent # MoFin/ _TEMPLATES_DIR = _SCRIPT_DIR / "templates" _SPECS_DIR = _SCRIPT_DIR / "specs" _GATEWAY_DIR = _SCRIPT_DIR / "gateway" _LOGS_DIR = _GATEWAY_DIR / "logs" _TEMP_DIR = _GATEWAY_DIR / "temp" # Allow override via env _PROJECT_ROOT = os.environ.get("MOFIN_ROOT") if _PROJECT_ROOT: _SCRIPT_DIR = Path(_PROJECT_ROOT) _TEMPLATES_DIR = _SCRIPT_DIR / "templates" _SPECS_DIR = _SCRIPT_DIR / "specs" _GATEWAY_DIR = _SCRIPT_DIR / "gateway" _LOGS_DIR = _GATEWAY_DIR / "logs" _TEMP_DIR = _GATEWAY_DIR / "temp" app = Flask(__name__, template_folder=str(_TEMPLATES_DIR)) # ---- Logging ---- _LOG_FILE = _LOGS_DIR / "dashboard.log" _LOG_FILE.parent.mkdir(parents=True, exist_ok=True) logging.basicConfig( filename=str(_LOG_FILE), level=logging.INFO, format="%(asctime)s [dashboard] %(message)s", ) log = logging.getLogger("dashboard") # ---- Constants ---- PORT = int(os.environ.get("MOFIN_DASHBOARD_PORT", 5807)) START_TIME = time.time() # ---- Monitored Services ---- SERVICES = [ { "name": "mofin_api", "label": "MoFin API", "port": 8899, "host": "127.0.0.1", "type": "http", "check": "/api/portfolio", "layer": "核心服务", "critical": True, }, { "name": "mofin_dashboard", "label": "Dashboard", "port": 5807, "host": "127.0.0.1", "type": "http", "check": "/api/health", "layer": "核心服务", "critical": True, }, { "name": "zhiwei_gateway", "label": "知微 Gateway", "port": 8643, "host": "127.0.0.1", "type": "http", "check": "/v1/health", "layer": "AI 网关", "critical": True, }, { "name": "ejabberd", "label": "ejabberd XMPP", "port": 5222, "host": "127.0.0.1", "type": "tcp", "check": None, "layer": "通信层", "critical": True, }, { "name": "mofin_db", "label": "MoFin 数据库", "port": 0, "host": "127.0.0.1", "type": "db", "check": "/home/hmo/web-dashboard/data/mofin.db", "layer": "数据层", "critical": True, }, ] # ---- Service Check Helpers ---- def _check_tcp(host, port, timeout=3): """Check if TCP port is open.""" try: sock = socket.create_connection((host, port), timeout=timeout) sock.close() return True except Exception: return False def _check_http(host, port, path, timeout=3): """Check HTTP endpoint returns 2xx.""" import urllib.request try: url = f"http://{host}:{port}{path}" if host else f"http://127.0.0.1:{port}{path}" req = urllib.request.Request(url) resp = urllib.request.urlopen(req, timeout=timeout) return 200 <= resp.status < 300 except Exception: return False def _check_db(db_path): """Check SQLite database is accessible.""" import sqlite3 try: conn = sqlite3.connect(db_path) conn.execute("SELECT 1") conn.close() return True except Exception: return False def _check_service(svc): """Check a single service, return (ok, detail).""" if svc["type"] == "tcp": ok = _check_tcp(svc["host"], svc["port"]) return ok, "port open" if ok else "port closed" elif svc["type"] == "http": ok = _check_http(svc["host"], svc["port"], svc["check"]) return ok, "HTTP 2xx" if ok else "HTTP fail" elif svc["type"] == "db": ok = _check_db(svc["check"]) return ok, "DB accessible" if ok else "DB fail" return False, "unknown type" # ---- API Endpoints ---- @app.route("/") def index(): return send_from_directory(str(_TEMPLATES_DIR), "dashboard.html") @app.route("/api/health") def api_health(): return jsonify({ "status": "ok", "uptime": int(time.time() - START_TIME), "version": "1.0", }) @app.route("/api/services") def api_services(): """Return status of all monitored services.""" result = [] for svc in SERVICES: ok, detail = _check_service(svc) result.append({ "name": svc["name"], "label": svc["label"], "port": svc["port"], "type": svc["type"], "layer": svc["layer"], "critical": svc["critical"], "health": {"ok": ok}, "detail": detail, }) ok_count = sum(1 for s in result if s["health"]["ok"]) return jsonify({ "services": result, "summary": {"ok": ok_count, "total": len(result)}, }) @app.route("/api/expected") def api_expected(): """Return expectation matrix.""" expected = [] for svc in SERVICES: expected.append({ "name": svc["name"], "label": svc["label"], "port": svc["port"], "expected": "running", "critical": svc["critical"], "layer": svc["layer"], "check": f"{svc['type']}:{svc['port']}" if svc["port"] else svc["type"], }) # Actual status actual = {} for svc in SERVICES: ok, _ = _check_service(svc) actual[svc["name"]] = "running" if ok else "stopped" return jsonify({ "expected": expected, "actual": actual, }) @app.route("/api/monitor") def api_monitor(): """Aggregate health check data from Tier1/Tier2 reports.""" tasks = [] tier1 = {"summary": {"ok": 0, "total": 0}, "services": []} tier2 = {"summary": {"ok": 0, "total": 0}, "services": []} # Try to read Tier1 report t1_path = _TEMP_DIR / "last_health_check.json" if t1_path.exists(): try: with open(t1_path, encoding="utf-8") as f: tier1 = json.load(f) tasks.append({"name": "agents-health-check", "status": "cron_ok"}) except Exception: tasks.append({"name": "agents-health-check", "status": "error"}) else: tasks.append({"name": "agents-health-check", "status": "not_deployed"}) # Try to read Tier2 report t2_path = _TEMP_DIR / "last_daily_health.json" if t2_path.exists(): try: with open(t2_path, encoding="utf-8") as f: tier2 = json.load(f) tasks.append({"name": "agents-daily-health", "status": "cron_ok"}) except Exception: tasks.append({"name": "agents-daily-health", "status": "error"}) else: tasks.append({"name": "agents-daily-health", "status": "not_deployed"}) # Self-check: are we running? svc_result = api_services().get_json() tasks.append({ "name": "dashboard", "status": "running", "detail": f"services: {svc_result.get('summary', {}).get('ok', 0)}/{svc_result.get('summary', {}).get('total', 0)}", }) return jsonify({ "tasks": tasks, "tier1": tier1, "tier2": tier2, "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), }) @app.route("/api/module-spec/") def api_module_spec(module): """Serve spec JSON for a module.""" # Safety: prevent path traversal module = module.replace("..", "").replace("/", "").replace("\\", "") spec_path = _SPECS_DIR / f"{module}.json" if spec_path.exists(): try: with open(spec_path, encoding="utf-8") as f: return jsonify(json.load(f)) except Exception as e: return jsonify({"error": f"Failed to read spec: {e}"}), 500 return jsonify({"error": f"Module '{module}' not found"}), 404 # ---- Main ---- if __name__ == "__main__": # Ensure directories exist _LOGS_DIR.mkdir(parents=True, exist_ok=True) _TEMP_DIR.mkdir(parents=True, exist_ok=True) log.info(f"MoFin Dashboard starting on port {PORT}") log.info(f"Specs dir: {_SPECS_DIR}") log.info(f"Templates dir: {_TEMPLATES_DIR}") # Optional: PID guard try: sys.path.insert(0, str(_SCRIPT_DIR)) from proc_guard import guard if not guard("mofin_dashboard"): log.error("Another dashboard instance is already running") sys.exit(1) except ImportError: log.warning("proc_guard not available, skipping PID lock") app.run(host="0.0.0.0", port=PORT, debug=False)