diff --git a/gateway/scripts/dashboard.py b/gateway/scripts/dashboard.py
index 683f298..4a23da5 100644
--- a/gateway/scripts/dashboard.py
+++ b/gateway/scripts/dashboard.py
@@ -564,14 +564,20 @@ def api_kanban():
def api_git():
"""最近 git 提交历史 + 分支状态"""
try:
- agents_dir = _PROJECT_DIR
+ git_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..")
+ git_dir = os.path.normpath(git_dir) # → AgentsMeeting/
+ if not os.path.exists(os.path.join(git_dir, ".git")):
+ # 如果 _PROJECT_OVERRIDE 设置了,用它
+ git_dir = os.environ.get("AGENTSMEETING_ROOT", "")
+ if not git_dir or not os.path.exists(os.path.join(git_dir, ".git")):
+ return jsonify({"ok": False, "log": [".git not found at " + git_dir], "dirty": False})
r = subprocess.run(["git", "log", "--oneline", "-10"],
- cwd=str(agents_dir), capture_output=True, text=True, timeout=5)
+ cwd=git_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"})
+ cwd=git_dir, capture_output=True, text=True, timeout=5)
+ dirty = r2.stdout.strip() != "" if r2.returncode == 0 else False
+ return jsonify({"ok": True, "log": log_lines if log_lines != [""] else [], "dirty": dirty, "branch": "master"})
except Exception as e:
return jsonify({"ok": False, "error": str(e), "log": [], "dirty": False})
@@ -647,7 +653,8 @@ def api_monitor():
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")
+ # 兼容中文"就绪"和英文"Ready"
+ status = "ok" if ("Ready" in r.stdout or "\u5c31\u7eea" in r.stdout) else ("missing" if name not in r.stdout else "other")
except:
status = "error"
result["tasks"].append({"name": name, "status": status})
@@ -722,7 +729,9 @@ def api_expected():
capture_output=True, text=True, timeout=5,
creationflags=subprocess.CREATE_NO_WINDOW,
)
- actual[name] = "scheduled" if "Ready" in r.stdout else "missing"
+ # 中文"就绪"、英文"Ready"
+ ready = "Ready" in r.stdout or "\u5c31\u7eea" in r.stdout
+ actual[name] = "scheduled" if ready else "missing"
except:
actual[name] = "error"
return jsonify({"expected": expected, "actual": actual})
diff --git a/gateway/scripts/templates/dashboard.html b/gateway/scripts/templates/dashboard.html
index da905c4..78ae981 100644
--- a/gateway/scripts/templates/dashboard.html
+++ b/gateway/scripts/templates/dashboard.html
@@ -1,258 +1,104 @@
-
+
AgentsMeeting — 监控驱动开发 Dashboard
-
-
-
-AgentsMeeting — 监控驱动开发
-加载中...
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
F — 期望状态 vs 实际
-
加载中...
-
-
-
-
-
-
-
-
Infrastructure
-
加载中...
-
-
+:root{--bg:#0d1117;--card:#161b22;--border:#30363d;--text:#c9d1d9;--dim:#8b949e;--accent:#58a6ff;--green:#3fb950;--red:#f85149;--yellow:#d29922}
+*{margin:0;padding:0;box-sizing:border-box}
+body{background:var(--bg);color:var(--text);font:14px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;padding:20px}
+h1{font-size:22px;font-weight:600;color:var(--accent);margin-bottom:2px}
+.subtitle{color:var(--dim);font-size:12px;margin-bottom:16px}
+.tab-bar{display:flex;gap:4px;margin-bottom:16px;flex-wrap:wrap;border-bottom:1px solid var(--border)}
+.tab-btn{padding:8px 16px;font-size:13px;font-weight:500;background:0 0;border:none;color:var(--dim);cursor:pointer;border-bottom:2px solid transparent;transition:all .15s}
+.tab-btn:hover{color:var(--text);background:rgba(255,255,255,.03)}
+.tab-btn.active{color:var(--accent);border-bottom-color:var(--accent)}
+.tab-btn .badge{font-size:10px;padding:1px 7px;border-radius:8px;background:var(--bg);margin-left:6px;font-weight:400}
+.tab-btn .badge.green{color:var(--green);background:#3fb95015}
+.tab-btn .badge.red{color:var(--red);background:#f8514915}
+.tab-btn .badge.yellow{color:var(--yellow);background:#d2992215}
+.tab-panel{display:none}
+.tab-panel.active{display:block}
+.section{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:14px 16px;margin-bottom:12px}
+.mono{font-family:'Cascadia Code',Consolas,monospace;font-size:12px}
+.tag{display:inline-block;padding:1px 8px;border-radius:8px;font-size:11px;font-weight:600}
+.tag.green{background:#3fb95020;color:var(--green)}
+.tag.red{background:#f8514920;color:var(--red)}
+.tag.yellow{background:#d2992220;color:var(--yellow)}
+.tag.dim{background:#8b949e20;color:var(--dim)}
+.dot{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:6px}
+.dot.green{background:var(--green)}
+.dot.red{background:var(--red)}
+.dot.yellow{background:var(--yellow)}
+.dot.dim{background:var(--dim)}
+table{width:100%;border-collapse:collapse;font-size:12px}
+th,td{padding:6px 8px;text-align:left;border-bottom:1px solid var(--border)}
+th{color:var(--dim);font-weight:600;font-size:11px;text-transform:uppercase}
+.log-line{font:11px/1.6 'Cascadia Code',Consolas,monospace;color:var(--dim);padding:1px 0}
+.log-line .hash{color:var(--yellow)}
+.status-row{display:flex;align-items:center;gap:10px;padding:5px 0}
+.grid2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
+@media(max-width:768px){.grid2{grid-template-columns:1fr}}
+
+AgentsMeeting
+监控驱动开发 Dashboard
+
+
-
-
+function badge(id,t,c){const e=document.getElementById('badge-'+id);if(e){e.textContent=t;e.className='badge '+(c||'')}}
+function fill(id,h){document.getElementById('content-'+id).innerHTML=h;}
+async function fetchOverview(){
+ try{const[s,e,m]=await Promise.all([fetch('/api/services').then(r=>r.json()),fetch('/api/expected').then(r=>r.json()),fetch('/api/monitor').then(r=>r.json())]);
+ const sc=s.services.filter(x=>x.health&&x.health.ok).length,ec=e.expected.filter(x=>{const a=e.actual[x.name]||'';return(x.expected.startsWith('running')&&a==='running')||(x.expected.startsWith('scheduled')&&(a==='scheduled'||a==='running'));}).length;
+ fill('overview',''+sc+'/'+s.services.length+'
Services Healthy
'+(sc===s.services.length?'OK':'⚠')+'
Expected '+(sc===s.services.length?'Compliant':'Issues')+'
'+ec+'/'+e.expected.length+'
Expected Compliant
');
+ }catch(e){fill('overview','Error');}}
+async function fetchGit(){try{const r=await fetch('/api/git'),d=await r.json();badge('git',d.log.length+' commits',d.dirty?'yellow':'green');
+let h=d.log.map(l=>{const m=l.match(/^([a-f0-9]+)\s(.*)/);return''+(m?''+m[1]+' '+esc(m[2]):esc(l))+'
';}).join('');if(!h)h='No commits';if(d.dirty)h+='Uncommitted changes
';fill('git',h);}catch(e){fill('git','Error');}}
+async function fetchServices(){try{const r=await fetch('/api/services'),d=await r.json(),ok=d.services.filter(s=>s.health&&s.health.ok).length;badge('services',ok+'/'+d.total+' ok',ok===d.total?'green':'red');
+let h='| Service | Port | Health | Watchdog | Type | Depends |
';d.services.forEach(s=>{const a=s.health&&s.health.ok;h+='| '+esc(s.name)+' | '+(s.port||'-')+' | '+(s.health?s.health.status:'?')+' | '+(s.watchdog?'yes':'no')+' | '+esc(s.type)+' | '+(s.depends_on||[]).join(', ')+' |
';});
+h+='
Watchdog: '+d.watched+'/'+d.total+'
';fill('services',h);}catch(e){fill('services','Error');}}
+async function fetchMonitor(){try{const r=await fetch('/api/monitor'),d=await r.json();let f=0,h='';
+if(d.tier1){const s=d.tier1.summary||{};if(s.fail>0)f=1;h+='Tier 1 '+s.ok+'/'+s.total+' ok'+esc(d.tier1.time)+'
';}
+if(d.tier2){const s=d.tier2.summary||{};if(s.fail>0)f=1;h+='Tier 2 '+s.ok+'/'+s.total+' ok'+esc(d.tier2.time)+'
';}
+h+='| Task | Status |
';d.tasks.forEach(t=>{const o=t.status==='ok';if(!o)f=1;h+='| '+esc(t.name)+' | '+esc(t.status)+' |
';});h+='
';badge('monitor',f?'issues':'all ok',f?'red':'green');fill('monitor',h);}catch(e){fill('monitor','Error');}}
+async function fetchTodos(){try{const r=await fetch('/api/todos'),d=await r.json(),p=d.todos.filter(t=>t.status==='pending').length;badge('todos',d.count+' total',p>0?'yellow':'green');
+let h='';if(d.todos.length){h+='| Time | Service | Issue | Status | Result |
';d.todos.slice(-20).reverse().forEach(t=>{const st=t.status==='completed'?'done':t.status==='failed'?'fail':''+esc(t.status)+'';h+='| '+esc((t.created||'').substring(11,19))+' | '+esc(t.service)+' | '+esc((t.issue||'').substring(0,50))+' | '+st+' | '+esc((t.result||'').substring(0,30))+' |
';});h+='
';}else h='No records';
+if(d.executor_log_tail){h+='Executor log
'+d.executor_log_tail.split('\n').map(l=>'
'+esc(l)+'
').join('')+'
';}
+fill('todos',h);}catch(e){fill('todos','Error');}}
+async function fetchExpected(){try{const r=await fetch('/api/expected'),d=await r.json();let ok=0,fl=0;
+d.expected.forEach(e=>{const a=d.actual[e.name]||'unknown',cmp=(e.expected.startsWith('running')&&a==='running')||(e.expected.startsWith('scheduled')&&(a==='scheduled'||a==='running'));if(cmp)ok++;else fl++;});
+badge('expected',ok+'/'+(ok+fl)+' ok',fl>0?'red':'green');
+let h='| Component | Expected | Actual | Status |
';
+d.expected.forEach(e=>{const a=d.actual[e.name]||'unknown',cmp=(e.expected.startsWith('running')&&a==='running')||(e.expected.startsWith('scheduled')&&(a==='scheduled'||a==='running'));h+='| '+(e.critical?'':'')+esc(e.name)+(e.critical?'':'')+' | '+esc(e.expected)+' | '+esc(a)+' | '+(cmp?'OK':'FAIL')+' |
';});
+h+='
';fill('expected',h);}catch(e){fill('expected','Error');}}
+async function fetchKanban(){try{const r=await fetch('/api/kanban'),d=await r.json();badge('kanban',(d.tasks||[]).length+' tasks','dim');if(!d.tasks||!d.tasks.length){fill('kanban','No tasks');return;}
+const c={ready:'var(--yellow)',progress:'var(--accent)',done:'var(--green)',blocked:'var(--red)'};fill('kanban',d.tasks.slice(0,30).map(t=>''+esc(t.title)+''+esc(t.status)+''+(t.assignee?''+esc(t.assignee)+'':'')+''+esc((t.created_at||'').substring(0,10))+'
').join(''));}catch(e){fill('kanban','Error');}}
+async function fetchInfra(){try{const r=await fetch('/api/ejabberd'),d=await r.json(),a=d.alive||d.xmpp_bot_connected;fill('infra','Ejabberd XMPP'+(a?'ALIVE':'DOWN')+''+(d.online_jids||[]).join(', ')+'
');badge('infra',a?'alive':'down',a?'green':'red');}catch(e){fill('infra','Error');badge('infra','error','red');}}
+initTabs();
+const FETCHES={overview:fetchOverview,git:fetchGit,services:fetchServices,monitor:fetchMonitor,todos:fetchTodos,expected:fetchExpected,kanban:fetchKanban,infra:fetchInfra};
+async function loadAll(){document.getElementById('subtitle').textContent=new Date().toLocaleString('zh-CN',{timeZone:'Asia/Shanghai'});await Promise.all(Object.values(FETCHES).map(f=>f()));}
+loadAll();setInterval(loadAll,15000);setInterval(fetchKanban,20000);
+