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 — 监控驱动开发

-
加载中...
- - -
-

A — 源码管理

-
加载中...
-
- - -
-

B — 服务架构

-
加载中...
-
- - -
-

C — 监控结果

-
加载中...
-
- - -
-

D — 自修复流水线

-
加载中...
-
- - -
-

F — 期望状态 vs 实际

-
加载中...
-
- - -
-

Kanban

-
加载中...
-
- - -
-

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
ONLINE
Ejabberd XMPP
'); + }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='';d.services.forEach(s=>{const a=s.health&&s.health.ok;h+='';}); +h+='
ServicePortHealthWatchdogTypeDepends
'+esc(s.name)+''+(s.port||'-')+''+(s.health?s.health.status:'?')+''+(s.watchdog?'yes':'no')+''+esc(s.type)+''+(s.depends_on||[]).join(', ')+'
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+='';d.tasks.forEach(t=>{const o=t.status==='ok';if(!o)f=1;h+='';});h+='
TaskStatus
'+esc(t.name)+''+esc(t.status)+'
';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+='';d.todos.slice(-20).reverse().forEach(t=>{const st=t.status==='completed'?'done':t.status==='failed'?'fail':''+esc(t.status)+'';h+='';});h+='
TimeServiceIssueStatusResult
'+esc((t.created||'').substring(11,19))+''+esc(t.service)+''+esc((t.issue||'').substring(0,50))+''+st+''+esc((t.result||'').substring(0,30))+'
';}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=''; +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+='';}); +h+='
ComponentExpectedActualStatus
'+(e.critical?'':'')+esc(e.name)+(e.critical?'':'')+''+esc(e.expected)+''+esc(a)+''+(cmp?'OK':'FAIL')+'
';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); +