F可视化: dashboard 完整实现 A-F 六原则呈现 + 弹窗修复
- A: git log + dirty status - B: 服务架构矩阵 (/health + watchdog覆盖 + 拓扑依赖) - C: Tier1/Tier2 监控结果 + 定时任务状态 - D: TODO执行记录 + escalation日志 - F: 期望状态 vs 实际状态合规矩阵 - Kanban: dashboard内嵌版块 - 弹窗修复: 计划任务改用pythonw.exe + subprocess加CREATE_NO_WINDOW - 修复ast_grep_replace导致的
This commit is contained in:
@@ -557,6 +557,186 @@ def api_kanban():
|
||||
return jsonify({"error": str(e), "db_exists": True})
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# A — 源码管理可视化
|
||||
# ════════════════════════════════════════════════════════════
|
||||
@app.route("/api/git")
|
||||
def api_git():
|
||||
"""最近 git 提交历史 + 分支状态"""
|
||||
try:
|
||||
agents_dir = _PROJECT_DIR
|
||||
r = subprocess.run(["git", "log", "--oneline", "-10"],
|
||||
cwd=str(agents_dir), capture_output=True, text=True, timeout=5)
|
||||
log_lines = r.stdout.strip().split("\n") if r.returncode == 0 else ["git not available"]
|
||||
r2 = subprocess.run(["git", "status", "--short"],
|
||||
cwd=str(agents_dir), capture_output=True, text=True, timeout=5)
|
||||
dirty = r2.stdout.strip() != ""
|
||||
return jsonify({"ok": True, "log": log_lines, "dirty": dirty, "branch": "master"})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": str(e), "log": [], "dirty": False})
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# B — 服务架构可视化
|
||||
# ════════════════════════════════════════════════════════════
|
||||
@app.route("/api/services")
|
||||
def api_services():
|
||||
"""服务功能树 + watchdog 覆盖矩阵"""
|
||||
try:
|
||||
services = []
|
||||
# xmpp_bot
|
||||
svc1 = {"name": "xmpp_bot", "port": 5802, "health": _health_status("http://127.0.0.1:5802/health"),
|
||||
"watchdog": True, "pid_lock": True, "type": "core", "depends_on": ["ejabberd"]}
|
||||
services.append(svc1)
|
||||
# article_processor
|
||||
svc2 = {"name": "article_processor", "port": 5810, "health": _health_status("http://127.0.0.1:5810/health"),
|
||||
"watchdog": True, "pid_lock": False, "type": "bridge", "depends_on": []}
|
||||
services.append(svc2)
|
||||
# dashboard
|
||||
svc3 = {"name": "dashboard", "port": 5803, "health": _health_status("http://127.0.0.1:5803/api/health"),
|
||||
"watchdog": False, "pid_lock": True, "type": "mgmt", "depends_on": ["xmpp_bot"]}
|
||||
services.append(svc3)
|
||||
# ejabberd
|
||||
online = _ejabberd_online_jids()
|
||||
services.append({"name": "ejabberd", "port": 5222, "health": {"ok": len(online) > 0},
|
||||
"watchdog": True, "pid_lock": False, "type": "infra", "depends_on": []})
|
||||
# 统计
|
||||
watched = sum(1 for s in services if s.get("watchdog"))
|
||||
return jsonify({"ok": True, "services": services, "watched": watched, "total": len(services)})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": str(e)})
|
||||
|
||||
|
||||
def _health_status(url, timeout=3):
|
||||
try:
|
||||
r = urllib.request.urlopen(url, timeout=timeout)
|
||||
return {"ok": r.status == 200, "status": r.status, "detail": "ok"}
|
||||
except urllib.error.HTTPError as e:
|
||||
return {"ok": False, "status": e.code, "detail": f"HTTP {e.code}"}
|
||||
except Exception as e:
|
||||
return {"ok": False, "status": 0, "detail": str(e)[:50]}
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# C — 监控结果可视化
|
||||
# ════════════════════════════════════════════════════════════
|
||||
@app.route("/api/monitor")
|
||||
def api_monitor():
|
||||
"""Tier1/Tier2 最近检查结果 + 定时任务状态"""
|
||||
result = {"tier1": None, "tier2": None, "tasks": []}
|
||||
# Tier1 最新报告
|
||||
t1_file = TEMP_DIR / "last_health_check.json"
|
||||
if t1_file.exists():
|
||||
try:
|
||||
with open(str(t1_file)) as f:
|
||||
result["tier1"] = json.load(f)
|
||||
except:
|
||||
pass
|
||||
# Tier2 最新报告
|
||||
t2_file = TEMP_DIR / "last_daily_health.json"
|
||||
if t2_file.exists():
|
||||
try:
|
||||
with open(str(t2_file)) as f:
|
||||
result["tier2"] = json.load(f)
|
||||
except:
|
||||
pass
|
||||
# 定时任务状态
|
||||
expected_tasks = ["agents-health-check", "agents-daily-health", "agents-todo-executor"]
|
||||
for name in expected_tasks:
|
||||
try:
|
||||
r = subprocess.run(["schtasks", "/Query", "/TN", name, "/FO", "CSV", "/NH"],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW)
|
||||
status = "ok" if "Ready" in r.stdout else ("missing" if name not in r.stdout else "other")
|
||||
except:
|
||||
status = "error"
|
||||
result["tasks"].append({"name": name, "status": status})
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# D — 自修复流水线可视化
|
||||
# ════════════════════════════════════════════════════════════
|
||||
@app.route("/api/todos")
|
||||
def api_todos():
|
||||
"""TODO 执行记录 + escalation 日志"""
|
||||
todos = []
|
||||
todo_file = TEMP_DIR / "health_todos.jsonl"
|
||||
if todo_file.exists():
|
||||
try:
|
||||
with open(str(todo_file)) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line:
|
||||
todos.append(json.loads(line))
|
||||
except:
|
||||
pass
|
||||
# executor 日志最新行
|
||||
executor_log = LOGS_DIR / "todo_executor.log"
|
||||
executor_tail = ""
|
||||
if executor_log.exists():
|
||||
try:
|
||||
lines = open(str(executor_log)).read().strip().split("\n")
|
||||
executor_tail = "\n".join(lines[-5:])
|
||||
except:
|
||||
pass
|
||||
return jsonify({"todos": todos, "executor_log_tail": executor_tail, "count": len(todos)})
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# F — 期望状态 vs 实际状态
|
||||
# ════════════════════════════════════════════════════════════
|
||||
@app.route("/api/expected")
|
||||
def api_expected():
|
||||
"""期望状态矩阵:应该运行的 vs 实际运行的"""
|
||||
expected = [
|
||||
{"name": "xmpp_bot", "port": 5802, "expected": "running", "critical": True},
|
||||
{"name": "article_processor", "port": 5810, "expected": "running", "critical": True},
|
||||
{"name": "dashboard", "port": 5803, "expected": "running", "critical": True},
|
||||
{"name": "watchdog", "expected": "running", "critical": True},
|
||||
{"name": "agents-health-check", "expected": "scheduled (5min)", "critical": False},
|
||||
{"name": "agents-daily-health", "expected": "scheduled (daily 08:00)", "critical": False},
|
||||
{"name": "agents-todo-executor", "expected": "scheduled (10min)", "critical": False},
|
||||
]
|
||||
actual = {}
|
||||
for e in expected:
|
||||
name = e["name"]
|
||||
port = e.get("port")
|
||||
if port:
|
||||
actual[name] = "running" if port_open(port) else "stopped"
|
||||
elif name == "watchdog":
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["tasklist", "/FI", "IMAGENAME eq python.exe", "/NH"],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
)
|
||||
actual[name] = "running" if "python" in r.stdout else "stopped"
|
||||
except:
|
||||
actual[name] = "unknown"
|
||||
elif "scheduled" in e["expected"]:
|
||||
task_name = name
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["schtasks", "/Query", "/TN", task_name, "/FO", "CSV", "/NH"],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
)
|
||||
actual[name] = "scheduled" if "Ready" in r.stdout else "missing"
|
||||
except:
|
||||
actual[name] = "error"
|
||||
return jsonify({"expected": expected, "actual": actual})
|
||||
|
||||
|
||||
def port_open(port):
|
||||
try:
|
||||
r = subprocess.run(["netstat", "-ano"], capture_output=True, text=True, timeout=5,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW)
|
||||
return any(f":{port} " in line and "LISTENING" in line for line in r.stdout.splitlines())
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
@app.route("/api/platform")
|
||||
@app.route("/api/health")
|
||||
def api_health():
|
||||
|
||||
Reference in New Issue
Block a user