Files
MoFin/dashboard.py
T

305 lines
8.8 KiB
Python

#!/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", 5804))
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": 5804,
"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/<module>")
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)