305 lines
8.8 KiB
Python
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)
|