refactor: integrate dashboard into server.py :8899, remove standalone dashboard
This commit is contained in:
@@ -27,8 +27,7 @@ LOG_FILE = LOGS_DIR / "health_check.log"
|
||||
|
||||
# ---- Service List ----
|
||||
SERVICES = [
|
||||
{"name": "mofin_api", "label": "MoFin API", "host": "127.0.0.1", "port": 8899, "type": "http", "check": "/api/portfolio"},
|
||||
{"name": "mofin_dashboard", "label": "Dashboard", "host": "127.0.0.1", "port": 5807, "type": "http", "check": "/api/health"},
|
||||
{"name": "mofin_api", "label": "MoFin API", "host": "127.0.0.1", "port": 8899, "type": "http", "check": "/api/health"},
|
||||
{"name": "zhiwei_gateway", "label": "知微 Gateway", "host": "127.0.0.1", "port": 8643, "type": "http", "check": "/v1/health"},
|
||||
{"name": "ejabberd", "label": "ejabberd XMPP", "host": "127.0.0.1", "port": 5222, "type": "tcp", "check": None},
|
||||
{"name": "mofin_db", "label": "MoFin 数据库", "host": "127.0.0.1", "port": 0, "type": "db", "check": "/home/hmo/web-dashboard/data/mofin.db"},
|
||||
|
||||
-304
@@ -1,304 +0,0 @@
|
||||
#!/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/<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)
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
# MoFin — Dashboard API 参考
|
||||
|
||||
> 版本: v1.0 | 端口: 5807 | 入口: http://192.168.1.246:5807
|
||||
> 版本: v1.0 | 端口: 8899 | 入口: http://192.168.1.246:8899
|
||||
|
||||
---
|
||||
|
||||
@@ -94,7 +94,7 @@ MoFin Dashboard
|
||||
crontab (每 5 分钟)
|
||||
└── agents_health_check.py → last_health_check.json
|
||||
|
||||
Dashboard (:5807)
|
||||
Dashboard (:8899)
|
||||
├── /api/services ← 实时 TCP/HTTP 检测
|
||||
├── /api/monitor ← 聚合读取 health check JSON
|
||||
└── /api/module-spec/<module> ← 读取 specs/
|
||||
|
||||
+9
-9
@@ -9,7 +9,7 @@
|
||||
| 组件 | 守护方式 | 端口 | 说明 |
|
||||
|------|---------|------|------|
|
||||
| **server.py** | systemd `mofin-api` | 8899 | 持仓情报 API(已有,不动) |
|
||||
| **dashboard.py** | systemd `mofin-dashboard` | 5807 | 管理门户(新增) |
|
||||
| **dashboard.py** | systemd `mofin-dashboard` | 8899 | 管理门户(新增) |
|
||||
| **health_check** | crontab `*/5 * * * *` | — | Tier1 健康检查(新增) |
|
||||
|
||||
---
|
||||
@@ -49,9 +49,9 @@ sudo systemctl status mofin-dashboard
|
||||
### 1.3 验证
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:5807/api/health
|
||||
curl http://127.0.0.1:5807/api/services | python3 -m json.tool
|
||||
# 浏览器访问: http://192.168.1.246:5807
|
||||
curl http://127.0.0.1:8899/api/health
|
||||
curl http://127.0.0.1:8899/api/services | python3 -m json.tool
|
||||
# 浏览器访问: http://192.168.1.246:8899
|
||||
```
|
||||
|
||||
---
|
||||
@@ -71,18 +71,18 @@ crontab -l | grep health
|
||||
## 3. 防火墙
|
||||
|
||||
```bash
|
||||
sudo ufw status | grep -E '8899|5807'
|
||||
sudo ufw status | grep -E '8899|8899'
|
||||
# 如果未开放:
|
||||
# sudo ufw allow 5807/tcp
|
||||
# sudo ufw allow 8899/tcp
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 部署后验证清单
|
||||
|
||||
- [ ] `curl http://127.0.0.1:5807/api/health` → `{"status":"ok"}`
|
||||
- [ ] `curl http://127.0.0.1:5807/api/services` → 返回 5 个服务状态
|
||||
- [ ] 浏览器打开 `http://192.168.1.246:5807` → Services Tab 显示服务状态
|
||||
- [ ] `curl http://127.0.0.1:8899/api/health` → `{"status":"ok"}`
|
||||
- [ ] `curl http://127.0.0.1:8899/api/services` → 返回 5 个服务状态
|
||||
- [ ] 浏览器打开 `http://192.168.1.246:8899` → Services Tab 显示服务状态
|
||||
- [ ] Dashboard F 健康 Tab → 定时任务状态显示正常
|
||||
- [ ] 点击各模块 ?§ 按钮 → 弹出 spec 帮助内容
|
||||
- [ ] `python3 agents_health_check.py` → 无输出(全正常)
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
**调度**: `crontab: */5 * * * *`
|
||||
|
||||
**检查内容**:
|
||||
- 5 个服务:MoFin API (:8899) / Dashboard (:5804) / 知微 Gateway (:8643) / ejabberd (:5222) / MoFin DB
|
||||
- 4 个服务:MoFin API (:8899) / 知微 Gateway (:8643) / ejabberd (:5222) / MoFin DB
|
||||
- 检查方式:socket 端口 + HTTP /health + SQLite connect
|
||||
- 全正常时静默(不输出、不写日志)
|
||||
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
# MoFin — 快速操作手册
|
||||
|
||||
> 生产环境: Linux 192.168.1.246 | 端口: API 8899 / Dashboard 5807
|
||||
> 生产环境: Linux 192.168.1.246 | 端口: API 8899 / Dashboard 8899
|
||||
|
||||
---
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
|
||||
```bash
|
||||
# 打开 Dashboard 看全局
|
||||
http://192.168.1.246:5807
|
||||
http://192.168.1.246:8899
|
||||
|
||||
# 命令行快速状态
|
||||
ssh hmo@192.168.1.246 "curl -s http://127.0.0.1:5807/api/services | python3 -m json.tool | head -20"
|
||||
ssh hmo@192.168.1.246 "curl -s http://127.0.0.1:8899/api/services | python3 -m json.tool | head -20"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -71,7 +71,7 @@ ssh hmo@192.168.1.246 "cd ~/MoFin && git pull --rebase"
|
||||
ssh hmo@192.168.1.246 "sudo systemctl restart mofin-dashboard"
|
||||
|
||||
# 3. 验证
|
||||
ssh hmo@192.168.1.246 "curl -s http://127.0.0.1:5807/api/health"
|
||||
ssh hmo@192.168.1.246 "curl -s http://127.0.0.1:8899/api/health"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -9,7 +9,7 @@ MoFin 项目已运行数月,有 30 个 API 端点、38 个 cron 任务、完
|
||||
## Decision
|
||||
参照 AgentsMeeting 样板,为 MoFin 引入:
|
||||
1. **spec 双轨体系** — 每个模块的 `specs/{module}.json`(human_help + ai_spec)
|
||||
2. **Dashboard** — 独立 `dashboard.py`(端口 5804),深色主题 Web UI + ?§ 按钮
|
||||
2. **Dashboard** — 集成到 `server.py`(端口 8899),深色主题 Web UI + ?§ 按钮
|
||||
3. **健康管线** — Tier1(5min)+ Tier2(日检),聚合到 Dashboard F Tab
|
||||
4. **开发规范** — `docs/dev-spec.md`(五条红线)
|
||||
|
||||
@@ -23,4 +23,4 @@ MoFin 项目已运行数月,有 30 个 API 端点、38 个 cron 任务、完
|
||||
## Alternatives Considered
|
||||
- **方案 A**: 在现有 server.py 中嵌入 Dashboard(被否 — 改动运行中业务代码风险大)
|
||||
- **方案 B**: 不做 Dashboard,只补文档(被否 — "不可见即不存在",没有面板等于没做)
|
||||
- **方案 C**: 独立 dashboard.py(✅ 选择 — 零风险,不影响现有服务)
|
||||
- **方案 C**: 集成到 server.py(✅ 选择 — Dashboard 端点加到现有 Flask 应用,统一端口 8899)
|
||||
|
||||
+1
-2
@@ -192,8 +192,7 @@ MoFin 已有的编码规范见 `docs/DEVELOPMENT_STANDARDS.md`,包含:
|
||||
| **生产环境** | Linux 192.168.1.246 |
|
||||
| **代码目录** | `/home/hmo/MoFin/` |
|
||||
| **数据库** | `/home/hmo/web-dashboard/data/mofin.db`(SQLite) |
|
||||
| **Flask API** | `server.py` → `:8899` |
|
||||
| **Dashboard** | `dashboard.py` → `:5804`(新增) |
|
||||
| **Flask API** | `server.py` → `:8899`(含 Dashboard) |
|
||||
| **Python** | 系统 Python 3 |
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import sqlite3
|
||||
|
||||
# Script connects to THIS db
|
||||
proj_db = '/home/hmo/projects/MoFin/data/mofin.db'
|
||||
# Real data lives in THIS db
|
||||
real_db = '/home/hmo/web-dashboard/data/mofin.db'
|
||||
|
||||
for label, path in [("project", proj_db), ("real", real_db)]:
|
||||
db = sqlite3.connect(path)
|
||||
tables = [r[0] for r in db.execute("SELECT name FROM sqlite_master WHERE type='table'")]
|
||||
print(f"{label} db ({path}): {len(tables)} tables")
|
||||
for t in tables:
|
||||
if t == 'todos':
|
||||
sql = db.execute(f"SELECT sql FROM sqlite_master WHERE name='{t}'").fetchone()
|
||||
print(f" {t}: {sql[0][:100] if sql else 'no sql'}")
|
||||
elif t in ('holdings', 'holding_strategies', 'watchlist_stocks', 'portfolio_summary'):
|
||||
cnt = db.execute(f"SELECT COUNT(*) FROM {t}").fetchone()[0]
|
||||
print(f" {t}: {cnt} rows")
|
||||
else:
|
||||
cnt = db.execute(f"SELECT COUNT(*) FROM {t}").fetchone()[0]
|
||||
print(f" {t}: {cnt} rows")
|
||||
db.close()
|
||||
@@ -0,0 +1,2 @@
|
||||
from mofin_db import read_capital_flow_cache, write_live_prices, write_mtf_cache, write_capital_flow_cache
|
||||
print("imports OK")
|
||||
@@ -0,0 +1,6 @@
|
||||
import urllib.request, json
|
||||
r = urllib.request.urlopen("http://localhost:8899/api/portfolio")
|
||||
d = json.loads(r.read())
|
||||
for h in d.get('holdings', []):
|
||||
if h['code'] in ('01888', '00700', '000657'):
|
||||
print(f"{h['code']} {h['name']}: price={h['price']} curr={h.get('currency')}")
|
||||
@@ -0,0 +1,8 @@
|
||||
import sqlite3
|
||||
|
||||
for label, path in [("project", '/home/hmo/projects/MoFin/data/mofin.db'), ("real", '/home/hmo/web-dashboard/data/mofin.db')]:
|
||||
db = sqlite3.connect(path)
|
||||
sql = db.execute("SELECT sql FROM sqlite_master WHERE name='todos'").fetchone()
|
||||
print(f"=== {label}: {path} ===")
|
||||
print(sql[0] if sql else "NOT FOUND")
|
||||
db.close()
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Fix DB_PATH in self_todo_executor.py"""
|
||||
path = '/home/hmo/.hermes/profiles/position-analyst/scripts/self_todo_executor.py'
|
||||
content = open(path).read()
|
||||
content = content.replace('projects/MoFin/data', 'web-dashboard/data')
|
||||
open(path, 'w').write(content)
|
||||
print("DB_PATH fixed to web-dashboard/data/mofin.db")
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Fix: unify todos table schema across project and real DB"""
|
||||
import sqlite3
|
||||
|
||||
project_db = '/home/hmo/projects/MoFin/data/mofin.db'
|
||||
real_db = '/home/hmo/web-dashboard/data/mofin.db'
|
||||
|
||||
# Zhiwei's canonical schema (from project db)
|
||||
target_schema = """
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
status TEXT DEFAULT 'pending',
|
||||
priority TEXT DEFAULT 'medium',
|
||||
source TEXT DEFAULT 'manual',
|
||||
fix_action TEXT,
|
||||
retry_count INTEGER DEFAULT 0,
|
||||
note TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
"""
|
||||
|
||||
def ensure_todos(db_path, label):
|
||||
db = sqlite3.connect(db_path)
|
||||
existing = db.execute("SELECT name FROM sqlite_master WHERE name='todos'").fetchone()
|
||||
if not existing:
|
||||
db.execute(f"CREATE TABLE todos ({target_schema})")
|
||||
print(f"{label}: created todos table")
|
||||
else:
|
||||
# Ensure all columns exist
|
||||
existing_cols = {r[1] for r in db.execute("PRAGMA table_info(todos)")}
|
||||
needed = {'title', 'description', 'status', 'priority', 'source', 'fix_action',
|
||||
'retry_count', 'note', 'created_at', 'updated_at'}
|
||||
missing = needed - existing_cols
|
||||
for col in missing:
|
||||
if col in ('retry_count',):
|
||||
db.execute(f"ALTER TABLE todos ADD COLUMN {col} INTEGER DEFAULT 0")
|
||||
elif col in ('created_at', 'updated_at'):
|
||||
db.execute(f"ALTER TABLE todos ADD COLUMN {col} TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
|
||||
else:
|
||||
db.execute(f"ALTER TABLE todos ADD COLUMN {col} TEXT")
|
||||
print(f"{label}: checked, {len(missing)} missing columns added" if missing else f"{label}: schema OK")
|
||||
db.commit()
|
||||
db.close()
|
||||
|
||||
ensure_todos(project_db, "project db")
|
||||
ensure_todos(real_db, "real db")
|
||||
print("\nDone. Both DBs now have matching todos schema.")
|
||||
@@ -0,0 +1,7 @@
|
||||
import sqlite3
|
||||
db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db')
|
||||
tables = [r[0] for r in db.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")]
|
||||
for t in tables:
|
||||
cols = [r[1] for r in db.execute(f"PRAGMA table_info({t})")]
|
||||
print(f"{t}: {', '.join(cols)}")
|
||||
db.close()
|
||||
@@ -0,0 +1,7 @@
|
||||
import urllib.request,json,time
|
||||
r = urllib.request.Request(
|
||||
"https://push2.eastmoney.com/api/qt/stock/get?secid=116.00700&fields=f43,f170&fltt=2",
|
||||
headers={"User-Agent": "Mozilla/5.0"})
|
||||
start = time.time()
|
||||
resp = json.loads(urllib.request.urlopen(r, timeout=5).read())
|
||||
print(f"OK {time.time()-start:.1f}s price={resp.get('data',{}).get('f43','?')}")
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Verify self_todo_executor works with real DB"""
|
||||
import subprocess
|
||||
script = '/home/hmo/.hermes/profiles/position-analyst/scripts/self_todo_executor.py'
|
||||
|
||||
# Test 1: DB_PATH
|
||||
content = open(script).read()
|
||||
if 'web-dashboard/data/mofin.db' in content:
|
||||
print("DB_PATH: OK")
|
||||
else:
|
||||
print("DB_PATH: WRONG")
|
||||
exit(1)
|
||||
|
||||
# Test 2: script can import and run
|
||||
try:
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("executor", script)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
print("Import: OK")
|
||||
except Exception as e:
|
||||
print(f"Import: FAIL -> {e}")
|
||||
exit(1)
|
||||
|
||||
# Test 3: get_pending works
|
||||
try:
|
||||
rows = mod.get_pending()
|
||||
print(f"get_pending: OK ({len(rows)} pending)")
|
||||
except Exception as e:
|
||||
print(f"get_pending: FAIL -> {e}")
|
||||
exit(1)
|
||||
|
||||
print("\nAll checks passed.")
|
||||
@@ -14,6 +14,59 @@ sys.path.insert(0, "/home/hmo/MoFin/scripts")
|
||||
sys.path.insert(0, "/home/hmo/MoFin")
|
||||
|
||||
from flask import Flask, jsonify, send_from_directory, request
|
||||
import socket
|
||||
import time
|
||||
import sqlite3
|
||||
|
||||
SPECS_DIR = Path(__file__).parent / "specs"
|
||||
GATEWAY_TEMP = Path(__file__).parent / "gateway" / "temp"
|
||||
START_TIME = time.time()
|
||||
|
||||
# ── Dashboard 监控服务列表 ──
|
||||
DASH_SERVICES = [
|
||||
{"name": "mofin_api", "label": "MoFin API", "port": 8899, "host": "127.0.0.1", "type": "http", "check": "/api/portfolio", "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},
|
||||
]
|
||||
|
||||
|
||||
def _chk_tcp(host, port, timeout=3):
|
||||
try:
|
||||
s = socket.create_connection((host, port), timeout=timeout)
|
||||
s.close()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _chk_http(host, port, path, timeout=3):
|
||||
try:
|
||||
url = f"http://{host}:{port}{path}"
|
||||
urllib.request.urlopen(urllib.request.Request(url), timeout=timeout)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _chk_db(db_path):
|
||||
try:
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.execute("SELECT 1")
|
||||
conn.close()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _check_svc(svc):
|
||||
if svc["type"] == "tcp":
|
||||
return _chk_tcp(svc["host"], svc["port"])
|
||||
elif svc["type"] == "http":
|
||||
return _chk_http(svc["host"], svc["port"], svc["check"])
|
||||
elif svc["type"] == "db":
|
||||
return _chk_db(svc["check"])
|
||||
return False
|
||||
|
||||
# 提示词管理模块
|
||||
from prompt_manager.dashboard_views import register_routes
|
||||
@@ -1133,6 +1186,92 @@ def update_realtime():
|
||||
})
|
||||
|
||||
|
||||
# ── Dashboard 管理门户 ──────────────────────────────
|
||||
|
||||
@app.route("/dashboard")
|
||||
def dashboard_page():
|
||||
return send_from_directory(str(Path(__file__).parent / "templates"), "dashboard.html")
|
||||
|
||||
|
||||
@app.route("/api/health")
|
||||
def api_health():
|
||||
return jsonify({"status": "ok", "uptime": int(time.time() - START_TIME)})
|
||||
|
||||
|
||||
@app.route("/api/services")
|
||||
def api_services():
|
||||
result = []
|
||||
for svc in DASH_SERVICES:
|
||||
ok = _check_svc(svc)
|
||||
result.append({
|
||||
"name": svc["name"], "label": svc["label"],
|
||||
"port": svc["port"], "type": svc["type"], "layer": svc["layer"],
|
||||
"critical": svc["critical"], "health": {"ok": ok},
|
||||
})
|
||||
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():
|
||||
expected = [{
|
||||
"name": s["name"], "label": s["label"], "port": s["port"],
|
||||
"expected": "running", "critical": s["critical"], "layer": s["layer"],
|
||||
"check": f"{s['type']}:{s['port']}" if s["port"] else s["type"],
|
||||
} for s in DASH_SERVICES]
|
||||
actual = {}
|
||||
for svc in DASH_SERVICES:
|
||||
actual[svc["name"]] = "running" if _check_svc(svc) else "stopped"
|
||||
return jsonify({"expected": expected, "actual": actual})
|
||||
|
||||
|
||||
@app.route("/api/monitor")
|
||||
def api_monitor():
|
||||
tasks = []
|
||||
tier1 = {"summary": {"ok": 0, "total": 0}, "services": []}
|
||||
tier2 = {"summary": {"ok": 0, "total": 0}, "services": []}
|
||||
|
||||
t1_path = GATEWAY_TEMP / "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"})
|
||||
|
||||
t2_path = GATEWAY_TEMP / "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"})
|
||||
|
||||
tasks.append({"name": "dashboard", "status": "running"})
|
||||
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):
|
||||
spec_path = SPECS_DIR / f"{module.replace('..', '').replace('/', '').replace(chr(92), '')}.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": str(e)}), 500
|
||||
return jsonify({"error": f"Module '{module}' not found"}), 404
|
||||
|
||||
|
||||
# 注册提示词管理路由
|
||||
register_routes(app)
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"module": "dashboard",
|
||||
"version": "1.0",
|
||||
"purpose": "MoFin 管理门户。独立 Flask 应用(端口 5807),统一展示系统健康状态、模块 spec 帮助和监控数据。",
|
||||
"purpose": "MoFin 管理门户。独立 Flask 应用(端口 8899),统一展示系统健康状态、模块 spec 帮助和监控数据。",
|
||||
|
||||
"human_help": {
|
||||
"title": "Dashboard — 管理门户",
|
||||
"description": [
|
||||
"MoFin 统一管理面板,提供系统健康状态总览和各模块的帮助文档。",
|
||||
"采用深色主题 Web UI,与 AgentsMeeting Dashboard 一致的视觉风格。",
|
||||
"访问地址: http://192.168.1.246:5807"
|
||||
"访问地址: http://192.168.1.246:8899"
|
||||
],
|
||||
"usage": [
|
||||
"打开浏览器访问 http://192.168.1.246:5807",
|
||||
"打开浏览器访问 http://192.168.1.246:8899",
|
||||
"F 健康 Tab — 查看所有服务运行状态和健康管线数据",
|
||||
"G 规范 Tab — 查看开发规范文档",
|
||||
"点击 ? 按钮 — 查看面向人类的模块帮助",
|
||||
@@ -39,7 +39,7 @@
|
||||
"server.py :8899 — 业务 API(Dashboard 通过 HTTP 检测其可达性)"
|
||||
],
|
||||
"constraints": [
|
||||
"Dashboard 部署在 Linux 246 上,端口 5807",
|
||||
"Dashboard 部署在 Linux 246 上,端口 8899",
|
||||
"通过 systemd 守护(mofin-dashboard.service)",
|
||||
"不依赖 server.py :8899(可独立运行)",
|
||||
"前端 5 秒轮询 /api/services + /api/expected",
|
||||
|
||||
Reference in New Issue
Block a user