@tekyzinc/gsd-t 2.50.12 → 2.53.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +379 -372
- package/bin/component-registry.js +250 -0
- package/bin/graph-cgc.js +510 -510
- package/bin/graph-indexer.js +147 -147
- package/bin/graph-overlay.js +195 -195
- package/bin/graph-parsers.js +327 -327
- package/bin/graph-query.js +453 -452
- package/bin/graph-store.js +154 -154
- package/bin/qa-calibrator.js +194 -0
- package/bin/scan-data-collector.js +153 -153
- package/bin/scan-diagrams-generators.js +187 -187
- package/bin/scan-diagrams.js +79 -79
- package/bin/scan-renderer.js +92 -92
- package/bin/scan-report-sections.js +121 -121
- package/bin/scan-report.js +184 -184
- package/bin/scan-schema-parsers.js +199 -199
- package/bin/scan-schema.js +103 -103
- package/bin/token-budget.js +246 -0
- package/commands/Claude-md.md +10 -10
- package/commands/branch.md +15 -15
- package/commands/checkin.md +45 -45
- package/commands/global-change.md +209 -209
- package/commands/gsd-t-audit.md +199 -0
- package/commands/gsd-t-backlog-add.md +94 -94
- package/commands/gsd-t-backlog-edit.md +111 -111
- package/commands/gsd-t-backlog-list.md +63 -63
- package/commands/gsd-t-backlog-move.md +94 -94
- package/commands/gsd-t-backlog-promote.md +123 -123
- package/commands/gsd-t-backlog-remove.md +86 -86
- package/commands/gsd-t-backlog-settings.md +158 -158
- package/commands/gsd-t-complete-milestone.md +528 -515
- package/commands/gsd-t-debug.md +506 -399
- package/commands/gsd-t-discuss.md +174 -174
- package/commands/gsd-t-execute.md +758 -634
- package/commands/gsd-t-feature.md +276 -276
- package/commands/gsd-t-health.md +142 -142
- package/commands/gsd-t-help.md +465 -457
- package/commands/gsd-t-impact.md +302 -302
- package/commands/gsd-t-init.md +320 -280
- package/commands/gsd-t-integrate.md +365 -249
- package/commands/gsd-t-milestone.md +87 -87
- package/commands/gsd-t-partition.md +442 -361
- package/commands/gsd-t-pause.md +82 -82
- package/commands/gsd-t-plan.md +345 -344
- package/commands/gsd-t-populate.md +111 -111
- package/commands/gsd-t-prd.md +326 -326
- package/commands/gsd-t-project.md +211 -211
- package/commands/gsd-t-promote-debt.md +123 -123
- package/commands/gsd-t-prompt.md +137 -137
- package/commands/gsd-t-qa.md +266 -266
- package/commands/gsd-t-quick.md +357 -234
- package/commands/gsd-t-reflect.md +134 -134
- package/commands/gsd-t-resume.md +72 -72
- package/commands/gsd-t-scan.md +615 -615
- package/commands/gsd-t-setup.md +76 -0
- package/commands/gsd-t-status.md +192 -166
- package/commands/gsd-t-test-sync.md +381 -381
- package/commands/gsd-t-triage-and-merge.md +171 -171
- package/commands/gsd-t-verify.md +382 -382
- package/commands/gsd-t-visualize.md +118 -118
- package/commands/gsd-t-wave.md +401 -378
- package/docs/GSD-T-README.md +425 -422
- package/docs/architecture.md +385 -369
- package/docs/harness-design-analysis.md +371 -0
- package/docs/infrastructure.md +205 -205
- package/docs/prd-graph-engine.md +398 -398
- package/docs/prd-gsd2-hybrid.md +559 -559
- package/docs/prd-harness-evolution.md +583 -0
- package/docs/requirements.md +14 -0
- package/docs/workflows.md +226 -226
- package/examples/.gsd-t/domains/example-domain/scope.md +13 -13
- package/package.json +40 -40
- package/scripts/gsd-t-auto-route.js +39 -39
- package/scripts/gsd-t-dashboard-mockup.html +1143 -1143
- package/scripts/gsd-t-dashboard-server.js +171 -171
- package/scripts/gsd-t-dashboard.html +262 -262
- package/scripts/gsd-t-event-writer.js +128 -128
- package/scripts/gsd-t-statusline.js +94 -94
- package/scripts/gsd-t-tools.js +175 -175
- package/templates/CLAUDE-global.md +639 -614
- package/templates/CLAUDE-project.md +24 -0
- package/templates/backlog-settings.md +18 -18
- package/templates/backlog.md +1 -1
- package/templates/progress.md +40 -40
- package/templates/shared-services-contract.md +60 -60
- package/templates/stacks/desktop.ini +2 -2
- package/bin/desktop.ini +0 -2
- package/commands/desktop.ini +0 -2
- package/docs/ci-examples/desktop.ini +0 -2
- package/docs/desktop.ini +0 -2
- package/examples/.gsd-t/contracts/desktop.ini +0 -2
- package/examples/.gsd-t/desktop.ini +0 -2
- package/examples/.gsd-t/domains/desktop.ini +0 -2
- package/examples/.gsd-t/domains/example-domain/desktop.ini +0 -2
- package/examples/desktop.ini +0 -2
- package/examples/rules/desktop.ini +0 -2
- package/scripts/desktop.ini +0 -2
- package/templates/desktop.ini +0 -2
|
@@ -1,262 +1,262 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<title>GSD-T Agent Dashboard</title>
|
|
6
|
-
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
|
|
7
|
-
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
|
|
8
|
-
<script src="https://unpkg.com/dagre@0.8.5/dist/dagre.min.js"></script>
|
|
9
|
-
<script src="https://unpkg.com/reactflow@11.11.4/dist/umd/index.js"></script>
|
|
10
|
-
<link rel="stylesheet" href="https://unpkg.com/reactflow@11.11.4/dist/style.css">
|
|
11
|
-
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
|
|
12
|
-
<style>
|
|
13
|
-
:root{--bg:#0d1117;--surface:#161b22;--border:#30363d;--text:#e6edf3;--muted:#7d8590;
|
|
14
|
-
--green:#3fb950;--green-bg:#1a3a1e;--red:#f85149;--red-bg:#3a1a1a;
|
|
15
|
-
--yellow:#d29922;--yellow-bg:#3a2c10;--blue:#388bfd;--blue-bg:#1f3a5f;
|
|
16
|
-
--font:'SF Mono','Fira Code',monospace;}
|
|
17
|
-
*{box-sizing:border-box;margin:0;padding:0;}
|
|
18
|
-
body{background:var(--bg);color:var(--text);font-family:var(--font);font-size:12px;
|
|
19
|
-
height:100vh;display:flex;flex-direction:column;overflow:hidden;}
|
|
20
|
-
.hdr{display:flex;align-items:center;gap:12px;padding:0 16px;background:var(--surface);
|
|
21
|
-
border-bottom:1px solid var(--border);height:40px;flex-shrink:0;}
|
|
22
|
-
.logo{color:var(--blue);font-weight:bold;font-size:13px;}
|
|
23
|
-
.status{display:flex;align-items:center;gap:5px;padding:2px 8px;border-radius:10px;
|
|
24
|
-
font-size:10px;border:1px solid;}
|
|
25
|
-
.status.live{background:var(--green-bg);border-color:var(--green);color:var(--green);}
|
|
26
|
-
.status.wait{background:var(--yellow-bg);border-color:var(--yellow);color:var(--yellow);}
|
|
27
|
-
.dot{width:6px;height:6px;border-radius:50%;background:currentColor;animation:pdot 1.5s ease-in-out infinite;}
|
|
28
|
-
@keyframes pdot{0%,100%{opacity:1}50%{opacity:.3}}
|
|
29
|
-
.hright{margin-left:auto;color:var(--muted);font-size:11px;}.main{display:flex;flex:1;overflow:hidden;}
|
|
30
|
-
.garea{flex:1;position:relative;background:var(--bg);}.rfwrap{width:100%;height:100%;}
|
|
31
|
-
.react-flow__background{background:var(--bg);}.react-flow__node{font-family:var(--font);font-size:11px;}
|
|
32
|
-
.agent-node{background:var(--surface);border:1px solid var(--border);border-radius:6px;
|
|
33
|
-
padding:8px 12px;min-width:120px;text-align:center;}
|
|
34
|
-
.agent-node.success{border-color:var(--green);background:var(--green-bg);}.agent-node.failure{border-color:var(--red);background:var(--red-bg);}
|
|
35
|
-
.agent-node.learning,.agent-node.deferred{border-color:var(--yellow);background:var(--yellow-bg);}.agent-node.session{border-color:var(--blue);background:var(--blue-bg);}
|
|
36
|
-
.node-id{color:var(--text);font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:140px;}.node-cnt{color:var(--muted);font-size:9px;margin-top:2px;}
|
|
37
|
-
.sidebar{width:300px;background:var(--surface);border-left:1px solid var(--border);
|
|
38
|
-
display:flex;flex-direction:column;overflow:hidden;}
|
|
39
|
-
.sb-hdr{padding:10px 12px;border-bottom:1px solid var(--border);color:var(--muted);
|
|
40
|
-
font-size:10px;text-transform:uppercase;letter-spacing:.8px;flex-shrink:0;}
|
|
41
|
-
.feed{flex:1;overflow-y:auto;padding:8px;}.feed::-webkit-scrollbar{width:4px;}.feed::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px;}
|
|
42
|
-
.ev{border-radius:4px;padding:6px 8px;margin-bottom:5px;border-left:3px solid var(--border);
|
|
43
|
-
background:#1c2128;}
|
|
44
|
-
.ev.success{border-left-color:var(--green);}.ev.failure{border-left-color:var(--red);}.ev.learning,.ev.deferred{border-left-color:var(--yellow);}
|
|
45
|
-
.ev-type{font-weight:500;color:var(--text);font-size:11px;}.ev-cmd{color:var(--blue);font-size:10px;margin-top:1px;}.ev-ts{color:var(--muted);font-size:9px;margin-top:2px;}
|
|
46
|
-
.badge{display:inline-block;font-size:9px;padding:1px 5px;border-radius:3px;font-weight:bold;margin-left:4px;}
|
|
47
|
-
.badge.success{background:var(--green-bg);color:var(--green);}.badge.failure{background:var(--red-bg);color:var(--red);}.badge.learning,.badge.deferred{background:var(--yellow-bg);color:var(--yellow);}
|
|
48
|
-
.noevents{color:var(--muted);font-size:11px;padding:12px;text-align:center;}
|
|
49
|
-
.models{padding:8px 12px;border-bottom:1px solid var(--border);flex-shrink:0;}
|
|
50
|
-
.model-row{display:flex;align-items:center;gap:8px;margin-bottom:4px;font-size:11px;}
|
|
51
|
-
.model-name{width:50px;color:var(--text);font-weight:500;}.model-bar-bg{flex:1;height:8px;background:var(--bg);border-radius:4px;overflow:hidden;}
|
|
52
|
-
.model-bar{height:100%;border-radius:4px;transition:width .3s ease;}.model-bar.opus{background:var(--blue);}.model-bar.sonnet{background:var(--green);}.model-bar.haiku{background:var(--yellow);}
|
|
53
|
-
.model-cnt{color:var(--muted);font-size:10px;min-width:20px;text-align:right;}
|
|
54
|
-
.metrics-panel{background:var(--surface);border-top:1px solid var(--border);padding:12px;flex-shrink:0;max-height:280px;overflow-y:auto;}
|
|
55
|
-
.metrics-panel .sb-hdr{padding:0 0 8px;border-bottom:none;}
|
|
56
|
-
.elo-display{display:flex;align-items:center;gap:8px;margin-bottom:8px;font-size:12px;}
|
|
57
|
-
.elo-score{font-size:18px;font-weight:bold;color:var(--blue);}
|
|
58
|
-
.elo-delta{font-size:12px;}.elo-delta.up{color:var(--green);}.elo-delta.down{color:var(--red);}
|
|
59
|
-
.metrics-chart{height:120px;margin-bottom:8px;}
|
|
60
|
-
.domain-heat{width:100%;border-collapse:collapse;font-size:10px;margin-top:4px;}
|
|
61
|
-
.domain-heat th{color:var(--muted);text-align:left;padding:2px 6px;border-bottom:1px solid var(--border);}
|
|
62
|
-
.domain-heat td{padding:2px 6px;}.rate-good{color:var(--green);}.rate-warn{color:var(--yellow);}.rate-bad{color:var(--red);}
|
|
63
|
-
</style>
|
|
64
|
-
</head>
|
|
65
|
-
<body>
|
|
66
|
-
<div class="hdr">
|
|
67
|
-
<span class="logo">GSD-T Agent Dashboard</span>
|
|
68
|
-
<div id="status" class="status wait"><span class="dot"></span><span id="status-txt">Connecting...</span></div>
|
|
69
|
-
<div class="hright"><span id="ev-count">0 events</span></div>
|
|
70
|
-
</div>
|
|
71
|
-
<div class="main">
|
|
72
|
-
<div class="garea"><div id="rf-root" class="rfwrap"></div></div>
|
|
73
|
-
<div class="sidebar">
|
|
74
|
-
<div class="sb-hdr">Models Invoked</div>
|
|
75
|
-
<div id="models" class="models"><div class="noevents">No model data yet</div></div>
|
|
76
|
-
<div class="sb-hdr">Live Event Feed</div>
|
|
77
|
-
<div id="feed" class="feed"><div class="noevents">Waiting for events...</div></div>
|
|
78
|
-
<div class="metrics-panel">
|
|
79
|
-
<div class="sb-hdr">Process Metrics</div>
|
|
80
|
-
<div id="elo-display" class="elo-display"><span class="noevents">No metrics data</span></div>
|
|
81
|
-
<div class="metrics-chart"><canvas id="trend-chart"></canvas></div>
|
|
82
|
-
<table id="domain-heat" class="domain-heat" style="display:none">
|
|
83
|
-
<thead><tr><th>Domain</th><th>Pass%</th><th>Avg(s)</th><th>Tasks</th></tr></thead>
|
|
84
|
-
<tbody id="domain-heat-body"></tbody>
|
|
85
|
-
</table>
|
|
86
|
-
</div>
|
|
87
|
-
</div>
|
|
88
|
-
</div>
|
|
89
|
-
<script>
|
|
90
|
-
const {useState,useEffect,useCallback,useRef}=React;
|
|
91
|
-
const {ReactFlow,useNodesState,useEdgesState,Background,Controls,MarkerType}=window.ReactFlow||{};
|
|
92
|
-
const dagre=window.dagre;
|
|
93
|
-
const params=new URLSearchParams(location.search);
|
|
94
|
-
const PORT=params.get('port')||'7433';
|
|
95
|
-
const MAX_EVENTS=200;
|
|
96
|
-
const GRAPH_W=160,GRAPH_H=60;
|
|
97
|
-
|
|
98
|
-
function outcomeClass(o){return o==='success'?'success':o==='failure'?'failure':o==='learning'||o==='deferred'?o:'';}
|
|
99
|
-
|
|
100
|
-
function layoutGraph(nodes,edges){
|
|
101
|
-
const g=new dagre.graphlib.Graph();
|
|
102
|
-
g.setGraph({rankdir:'TB',ranksep:60,nodesep:40});
|
|
103
|
-
g.setDefaultEdgeLabel(()=>({}));
|
|
104
|
-
nodes.forEach(n=>g.setNode(n.id,{width:GRAPH_W,height:GRAPH_H}));
|
|
105
|
-
edges.forEach(e=>g.setEdge(e.source,e.target));
|
|
106
|
-
dagre.layout(g);
|
|
107
|
-
return nodes.map(n=>{const p=g.node(n.id);return{...n,position:{x:p.x-GRAPH_W/2,y:p.y-GRAPH_H/2}};});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function AgentNode({data}){
|
|
111
|
-
return React.createElement('div',{className:`agent-node ${data.outcome} ${data.nodeType||''}`},
|
|
112
|
-
React.createElement('div',{className:'node-id',title:data.label},data.label),
|
|
113
|
-
React.createElement('div',{className:'node-cnt'},`${data.count} event${data.count!==1?'s':''}`));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const nodeTypes={agentNode:AgentNode};
|
|
117
|
-
|
|
118
|
-
function Dashboard(){
|
|
119
|
-
const [nodes,setNodes,onNodesChange]=useNodesState([]);
|
|
120
|
-
const [edges,setEdges,onEdgesChange]=useEdgesState([]);
|
|
121
|
-
const [events,setEvents]=useState([]);
|
|
122
|
-
const agentMap=useRef({});
|
|
123
|
-
const modelCounts=useRef({});
|
|
124
|
-
|
|
125
|
-
const processEvent=useCallback((ev)=>{
|
|
126
|
-
setEvents(prev=>{
|
|
127
|
-
const next=[ev,...prev];
|
|
128
|
-
return next.length>MAX_EVENTS?next.slice(0,MAX_EVENTS):next;
|
|
129
|
-
});
|
|
130
|
-
const m=ev.model||(ev.event_type==='session_start'&&ev.reasoning?ev.reasoning.match(/opus|sonnet|haiku/i):null);
|
|
131
|
-
const mName=m?(typeof m==='string'?m:m[0]).toLowerCase():null;
|
|
132
|
-
if(mName){modelCounts.current[mName]=(modelCounts.current[mName]||0)+1;}
|
|
133
|
-
if(!ev.agent_id)return;
|
|
134
|
-
const id=ev.agent_id;
|
|
135
|
-
const prev=agentMap.current[id]||{count:0,outcome:null,parent:null,nodeType:null,firstTs:null};
|
|
136
|
-
const nodeType=prev.nodeType||(ev.event_type==='session_start'?'session':ev.event_type==='subagent_spawn'?(ev.reasoning||'task'):null);
|
|
137
|
-
agentMap.current[id]={
|
|
138
|
-
count:prev.count+1,
|
|
139
|
-
outcome:ev.outcome||prev.outcome,
|
|
140
|
-
parent:ev.parent_agent_id||prev.parent||null,
|
|
141
|
-
nodeType,
|
|
142
|
-
firstTs:prev.firstTs||ev.ts||null
|
|
143
|
-
};
|
|
144
|
-
const newNodes=Object.entries(agentMap.current).map(([aid,d])=>{
|
|
145
|
-
const isSession=d.nodeType==='session';
|
|
146
|
-
const dateStr=d.firstTs?new Date(d.firstTs).toLocaleDateString('en',{month:'short',day:'numeric'}):'';
|
|
147
|
-
const lbl=isSession?`Session · ${dateStr?dateStr+' ':''}${aid.slice(0,7)}`:(d.nodeType||aid.slice(0,8)+'..'+aid.slice(-4));
|
|
148
|
-
return{id:aid,type:'agentNode',data:{label:lbl,count:d.count,outcome:d.outcome||'',nodeType:d.nodeType||''},position:{x:0,y:0}};
|
|
149
|
-
});
|
|
150
|
-
const newEdges=Object.entries(agentMap.current)
|
|
151
|
-
.filter(([,d])=>d.parent&&d.parent!=='')
|
|
152
|
-
.map(([aid,d])=>({id:`${d.parent}-${aid}`,source:d.parent,target:aid,
|
|
153
|
-
markerEnd:{type:MarkerType.ArrowClosed},style:{stroke:'#30363d'}}));
|
|
154
|
-
const laid=layoutGraph(newNodes,newEdges);
|
|
155
|
-
setNodes(laid);
|
|
156
|
-
setEdges(newEdges);
|
|
157
|
-
},[]);
|
|
158
|
-
|
|
159
|
-
useEffect(()=>{
|
|
160
|
-
let es;
|
|
161
|
-
function connect(){
|
|
162
|
-
es=new EventSource(`http://localhost:${PORT}/events`);
|
|
163
|
-
es.onopen=()=>{
|
|
164
|
-
document.getElementById('status').className='status live';
|
|
165
|
-
document.getElementById('status-txt').textContent='Live';
|
|
166
|
-
};
|
|
167
|
-
es.onmessage=(e)=>{
|
|
168
|
-
if(e.data&&e.data.trim()&&!e.data.startsWith(':')){
|
|
169
|
-
try{processEvent(JSON.parse(e.data));}catch(err){}
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
|
-
es.onerror=()=>{
|
|
173
|
-
document.getElementById('status').className='status wait';
|
|
174
|
-
document.getElementById('status-txt').textContent='Reconnecting...';
|
|
175
|
-
es.close();
|
|
176
|
-
setTimeout(connect,3000);
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
connect();
|
|
180
|
-
return()=>{if(es)es.close();};
|
|
181
|
-
},[processEvent]);
|
|
182
|
-
|
|
183
|
-
useEffect(()=>{
|
|
184
|
-
document.getElementById('ev-count').textContent=`${events.length} event${events.length!==1?'s':''}`;
|
|
185
|
-
const mp=document.getElementById('models'),mc=modelCounts.current,mEntries=Object.entries(mc).sort((a,b)=>b[1]-a[1]);
|
|
186
|
-
if(mEntries.length){const maxC=Math.max(...mEntries.map(e=>e[1]));
|
|
187
|
-
mp.innerHTML=mEntries.map(([n,c])=>`<div class="model-row"><span class="model-name">${n}</span><div class="model-bar-bg"><div class="model-bar ${n}" style="width:${Math.round((c/maxC)*100)}%"></div></div><span class="model-cnt">${c}</span></div>`).join('');}
|
|
188
|
-
const feed=document.getElementById('feed');
|
|
189
|
-
if(!events.length){feed.innerHTML='<div class="noevents">Waiting for events...</div>';return;}
|
|
190
|
-
feed.innerHTML=events.slice(0,50).map(ev=>{
|
|
191
|
-
const oc=outcomeClass(ev.outcome);
|
|
192
|
-
const ts=ev.ts?new Date(ev.ts).toLocaleTimeString():'';
|
|
193
|
-
const cmd=ev.command||ev.phase||'';
|
|
194
|
-
const evModel=ev.model||null;
|
|
195
|
-
return`<div class="ev${oc?' '+oc:''}">
|
|
196
|
-
<div class="ev-type">${ev.event_type||'event'}${evModel?` <span style="color:var(--muted);font-size:9px">[${evModel}]</span>`:''}${oc?`<span class="badge ${oc}">${ev.outcome}</span>`:''}</div>
|
|
197
|
-
${cmd?`<div class="ev-cmd">${cmd}</div>`:''}
|
|
198
|
-
${ev.reasoning?`<div class="ev-ts" style="color:var(--muted)">${ev.reasoning.slice(0,60)}</div>`:''}
|
|
199
|
-
<div class="ev-ts">${ts}</div>
|
|
200
|
-
</div>`;
|
|
201
|
-
}).join('');
|
|
202
|
-
},[events]);
|
|
203
|
-
|
|
204
|
-
if(!ReactFlow)return React.createElement('div',{style:{color:'#f85149',padding:'20px'}},'ReactFlow failed to load. Check CDN.');
|
|
205
|
-
return React.createElement(ReactFlow,{
|
|
206
|
-
nodes,edges,onNodesChange,onEdgesChange,nodeTypes,fitView:true,
|
|
207
|
-
style:{background:'var(--bg)'},
|
|
208
|
-
defaultEdgeOptions:{type:'smoothstep',animated:false}
|
|
209
|
-
},
|
|
210
|
-
React.createElement(Background,{color:'#30363d',gap:24,size:1}),
|
|
211
|
-
React.createElement(Controls,{style:{background:'var(--surface)',border:'1px solid var(--border)'}})
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
ReactDOM.render(React.createElement(Dashboard),document.getElementById('rf-root'));
|
|
216
|
-
|
|
217
|
-
// ── Metrics Panel ──────────────────────────────────────────────────────────
|
|
218
|
-
(function initMetrics(){
|
|
219
|
-
let trendChart=null;
|
|
220
|
-
function rateClass(r){return r>=0.8?'rate-good':r>=0.6?'rate-warn':'rate-bad';}
|
|
221
|
-
function fetchMetrics(){
|
|
222
|
-
fetch(`http://localhost:${PORT}/metrics`).then(r=>r.json()).then(data=>{
|
|
223
|
-
renderELO(data.rollups);renderTrend(data.rollups);renderHeatmap(data.rollups);
|
|
224
|
-
}).catch(()=>{});
|
|
225
|
-
}
|
|
226
|
-
function renderELO(rollups){
|
|
227
|
-
const el=document.getElementById('elo-display');
|
|
228
|
-
if(!rollups||!rollups.length){el.innerHTML='<span class="noevents">No metrics data</span>';return;}
|
|
229
|
-
const last=rollups[rollups.length-1];
|
|
230
|
-
const dc=last.elo_delta>=0?'up':'down';const arrow=last.elo_delta>=0?'+':'';
|
|
231
|
-
el.innerHTML=`<span class="elo-score">${Math.round(last.elo_after)}</span><span>ELO</span>`+
|
|
232
|
-
`<span class="elo-delta ${dc}">${arrow}${last.elo_delta.toFixed(1)}</span>`;
|
|
233
|
-
}
|
|
234
|
-
function renderTrend(rollups){
|
|
235
|
-
const ctx=document.getElementById('trend-chart');
|
|
236
|
-
if(!ctx||!rollups||rollups.length<1)return;
|
|
237
|
-
const labels=rollups.map(r=>r.milestone);
|
|
238
|
-
const rates=rollups.map(r=>(r.first_pass_rate*100).toFixed(1));
|
|
239
|
-
if(trendChart)trendChart.destroy();
|
|
240
|
-
trendChart=new Chart(ctx,{type:'line',data:{labels,datasets:[{label:'First-Pass %',
|
|
241
|
-
data:rates,borderColor:'#3fb950',backgroundColor:'rgba(63,185,80,0.1)',fill:true,tension:0.3}]},
|
|
242
|
-
options:{responsive:true,maintainAspectRatio:false,scales:{y:{min:0,max:100,
|
|
243
|
-
ticks:{color:'#7d8590',font:{size:9}}},x:{ticks:{color:'#7d8590',font:{size:9}}}},
|
|
244
|
-
plugins:{legend:{labels:{color:'#e6edf3',font:{size:10}}}}}});
|
|
245
|
-
}
|
|
246
|
-
function renderHeatmap(rollups){
|
|
247
|
-
const tbl=document.getElementById('domain-heat');const body=document.getElementById('domain-heat-body');
|
|
248
|
-
if(!rollups||!rollups.length||!tbl||!body){return;}
|
|
249
|
-
const last=rollups[rollups.length-1];
|
|
250
|
-
if(!last.domain_breakdown||!last.domain_breakdown.length)return;
|
|
251
|
-
tbl.style.display='table';
|
|
252
|
-
body.innerHTML=last.domain_breakdown.map(d=>{
|
|
253
|
-
const r=d.first_pass_rate;const cls=rateClass(r);
|
|
254
|
-
return `<tr><td>${d.domain}</td><td class="${cls}">${(r*100).toFixed(0)}%</td>`+
|
|
255
|
-
`<td>${d.avg_duration_s.toFixed(0)}</td><td>${d.tasks}</td></tr>`;
|
|
256
|
-
}).join('');
|
|
257
|
-
}
|
|
258
|
-
fetchMetrics();setInterval(fetchMetrics,30000);
|
|
259
|
-
})();
|
|
260
|
-
</script>
|
|
261
|
-
</body>
|
|
262
|
-
</html>
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>GSD-T Agent Dashboard</title>
|
|
6
|
+
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
|
|
7
|
+
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
|
|
8
|
+
<script src="https://unpkg.com/dagre@0.8.5/dist/dagre.min.js"></script>
|
|
9
|
+
<script src="https://unpkg.com/reactflow@11.11.4/dist/umd/index.js"></script>
|
|
10
|
+
<link rel="stylesheet" href="https://unpkg.com/reactflow@11.11.4/dist/style.css">
|
|
11
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
|
|
12
|
+
<style>
|
|
13
|
+
:root{--bg:#0d1117;--surface:#161b22;--border:#30363d;--text:#e6edf3;--muted:#7d8590;
|
|
14
|
+
--green:#3fb950;--green-bg:#1a3a1e;--red:#f85149;--red-bg:#3a1a1a;
|
|
15
|
+
--yellow:#d29922;--yellow-bg:#3a2c10;--blue:#388bfd;--blue-bg:#1f3a5f;
|
|
16
|
+
--font:'SF Mono','Fira Code',monospace;}
|
|
17
|
+
*{box-sizing:border-box;margin:0;padding:0;}
|
|
18
|
+
body{background:var(--bg);color:var(--text);font-family:var(--font);font-size:12px;
|
|
19
|
+
height:100vh;display:flex;flex-direction:column;overflow:hidden;}
|
|
20
|
+
.hdr{display:flex;align-items:center;gap:12px;padding:0 16px;background:var(--surface);
|
|
21
|
+
border-bottom:1px solid var(--border);height:40px;flex-shrink:0;}
|
|
22
|
+
.logo{color:var(--blue);font-weight:bold;font-size:13px;}
|
|
23
|
+
.status{display:flex;align-items:center;gap:5px;padding:2px 8px;border-radius:10px;
|
|
24
|
+
font-size:10px;border:1px solid;}
|
|
25
|
+
.status.live{background:var(--green-bg);border-color:var(--green);color:var(--green);}
|
|
26
|
+
.status.wait{background:var(--yellow-bg);border-color:var(--yellow);color:var(--yellow);}
|
|
27
|
+
.dot{width:6px;height:6px;border-radius:50%;background:currentColor;animation:pdot 1.5s ease-in-out infinite;}
|
|
28
|
+
@keyframes pdot{0%,100%{opacity:1}50%{opacity:.3}}
|
|
29
|
+
.hright{margin-left:auto;color:var(--muted);font-size:11px;}.main{display:flex;flex:1;overflow:hidden;}
|
|
30
|
+
.garea{flex:1;position:relative;background:var(--bg);}.rfwrap{width:100%;height:100%;}
|
|
31
|
+
.react-flow__background{background:var(--bg);}.react-flow__node{font-family:var(--font);font-size:11px;}
|
|
32
|
+
.agent-node{background:var(--surface);border:1px solid var(--border);border-radius:6px;
|
|
33
|
+
padding:8px 12px;min-width:120px;text-align:center;}
|
|
34
|
+
.agent-node.success{border-color:var(--green);background:var(--green-bg);}.agent-node.failure{border-color:var(--red);background:var(--red-bg);}
|
|
35
|
+
.agent-node.learning,.agent-node.deferred{border-color:var(--yellow);background:var(--yellow-bg);}.agent-node.session{border-color:var(--blue);background:var(--blue-bg);}
|
|
36
|
+
.node-id{color:var(--text);font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:140px;}.node-cnt{color:var(--muted);font-size:9px;margin-top:2px;}
|
|
37
|
+
.sidebar{width:300px;background:var(--surface);border-left:1px solid var(--border);
|
|
38
|
+
display:flex;flex-direction:column;overflow:hidden;}
|
|
39
|
+
.sb-hdr{padding:10px 12px;border-bottom:1px solid var(--border);color:var(--muted);
|
|
40
|
+
font-size:10px;text-transform:uppercase;letter-spacing:.8px;flex-shrink:0;}
|
|
41
|
+
.feed{flex:1;overflow-y:auto;padding:8px;}.feed::-webkit-scrollbar{width:4px;}.feed::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px;}
|
|
42
|
+
.ev{border-radius:4px;padding:6px 8px;margin-bottom:5px;border-left:3px solid var(--border);
|
|
43
|
+
background:#1c2128;}
|
|
44
|
+
.ev.success{border-left-color:var(--green);}.ev.failure{border-left-color:var(--red);}.ev.learning,.ev.deferred{border-left-color:var(--yellow);}
|
|
45
|
+
.ev-type{font-weight:500;color:var(--text);font-size:11px;}.ev-cmd{color:var(--blue);font-size:10px;margin-top:1px;}.ev-ts{color:var(--muted);font-size:9px;margin-top:2px;}
|
|
46
|
+
.badge{display:inline-block;font-size:9px;padding:1px 5px;border-radius:3px;font-weight:bold;margin-left:4px;}
|
|
47
|
+
.badge.success{background:var(--green-bg);color:var(--green);}.badge.failure{background:var(--red-bg);color:var(--red);}.badge.learning,.badge.deferred{background:var(--yellow-bg);color:var(--yellow);}
|
|
48
|
+
.noevents{color:var(--muted);font-size:11px;padding:12px;text-align:center;}
|
|
49
|
+
.models{padding:8px 12px;border-bottom:1px solid var(--border);flex-shrink:0;}
|
|
50
|
+
.model-row{display:flex;align-items:center;gap:8px;margin-bottom:4px;font-size:11px;}
|
|
51
|
+
.model-name{width:50px;color:var(--text);font-weight:500;}.model-bar-bg{flex:1;height:8px;background:var(--bg);border-radius:4px;overflow:hidden;}
|
|
52
|
+
.model-bar{height:100%;border-radius:4px;transition:width .3s ease;}.model-bar.opus{background:var(--blue);}.model-bar.sonnet{background:var(--green);}.model-bar.haiku{background:var(--yellow);}
|
|
53
|
+
.model-cnt{color:var(--muted);font-size:10px;min-width:20px;text-align:right;}
|
|
54
|
+
.metrics-panel{background:var(--surface);border-top:1px solid var(--border);padding:12px;flex-shrink:0;max-height:280px;overflow-y:auto;}
|
|
55
|
+
.metrics-panel .sb-hdr{padding:0 0 8px;border-bottom:none;}
|
|
56
|
+
.elo-display{display:flex;align-items:center;gap:8px;margin-bottom:8px;font-size:12px;}
|
|
57
|
+
.elo-score{font-size:18px;font-weight:bold;color:var(--blue);}
|
|
58
|
+
.elo-delta{font-size:12px;}.elo-delta.up{color:var(--green);}.elo-delta.down{color:var(--red);}
|
|
59
|
+
.metrics-chart{height:120px;margin-bottom:8px;}
|
|
60
|
+
.domain-heat{width:100%;border-collapse:collapse;font-size:10px;margin-top:4px;}
|
|
61
|
+
.domain-heat th{color:var(--muted);text-align:left;padding:2px 6px;border-bottom:1px solid var(--border);}
|
|
62
|
+
.domain-heat td{padding:2px 6px;}.rate-good{color:var(--green);}.rate-warn{color:var(--yellow);}.rate-bad{color:var(--red);}
|
|
63
|
+
</style>
|
|
64
|
+
</head>
|
|
65
|
+
<body>
|
|
66
|
+
<div class="hdr">
|
|
67
|
+
<span class="logo">GSD-T Agent Dashboard</span>
|
|
68
|
+
<div id="status" class="status wait"><span class="dot"></span><span id="status-txt">Connecting...</span></div>
|
|
69
|
+
<div class="hright"><span id="ev-count">0 events</span></div>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="main">
|
|
72
|
+
<div class="garea"><div id="rf-root" class="rfwrap"></div></div>
|
|
73
|
+
<div class="sidebar">
|
|
74
|
+
<div class="sb-hdr">Models Invoked</div>
|
|
75
|
+
<div id="models" class="models"><div class="noevents">No model data yet</div></div>
|
|
76
|
+
<div class="sb-hdr">Live Event Feed</div>
|
|
77
|
+
<div id="feed" class="feed"><div class="noevents">Waiting for events...</div></div>
|
|
78
|
+
<div class="metrics-panel">
|
|
79
|
+
<div class="sb-hdr">Process Metrics</div>
|
|
80
|
+
<div id="elo-display" class="elo-display"><span class="noevents">No metrics data</span></div>
|
|
81
|
+
<div class="metrics-chart"><canvas id="trend-chart"></canvas></div>
|
|
82
|
+
<table id="domain-heat" class="domain-heat" style="display:none">
|
|
83
|
+
<thead><tr><th>Domain</th><th>Pass%</th><th>Avg(s)</th><th>Tasks</th></tr></thead>
|
|
84
|
+
<tbody id="domain-heat-body"></tbody>
|
|
85
|
+
</table>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
<script>
|
|
90
|
+
const {useState,useEffect,useCallback,useRef}=React;
|
|
91
|
+
const {ReactFlow,useNodesState,useEdgesState,Background,Controls,MarkerType}=window.ReactFlow||{};
|
|
92
|
+
const dagre=window.dagre;
|
|
93
|
+
const params=new URLSearchParams(location.search);
|
|
94
|
+
const PORT=params.get('port')||'7433';
|
|
95
|
+
const MAX_EVENTS=200;
|
|
96
|
+
const GRAPH_W=160,GRAPH_H=60;
|
|
97
|
+
|
|
98
|
+
function outcomeClass(o){return o==='success'?'success':o==='failure'?'failure':o==='learning'||o==='deferred'?o:'';}
|
|
99
|
+
|
|
100
|
+
function layoutGraph(nodes,edges){
|
|
101
|
+
const g=new dagre.graphlib.Graph();
|
|
102
|
+
g.setGraph({rankdir:'TB',ranksep:60,nodesep:40});
|
|
103
|
+
g.setDefaultEdgeLabel(()=>({}));
|
|
104
|
+
nodes.forEach(n=>g.setNode(n.id,{width:GRAPH_W,height:GRAPH_H}));
|
|
105
|
+
edges.forEach(e=>g.setEdge(e.source,e.target));
|
|
106
|
+
dagre.layout(g);
|
|
107
|
+
return nodes.map(n=>{const p=g.node(n.id);return{...n,position:{x:p.x-GRAPH_W/2,y:p.y-GRAPH_H/2}};});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function AgentNode({data}){
|
|
111
|
+
return React.createElement('div',{className:`agent-node ${data.outcome} ${data.nodeType||''}`},
|
|
112
|
+
React.createElement('div',{className:'node-id',title:data.label},data.label),
|
|
113
|
+
React.createElement('div',{className:'node-cnt'},`${data.count} event${data.count!==1?'s':''}`));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const nodeTypes={agentNode:AgentNode};
|
|
117
|
+
|
|
118
|
+
function Dashboard(){
|
|
119
|
+
const [nodes,setNodes,onNodesChange]=useNodesState([]);
|
|
120
|
+
const [edges,setEdges,onEdgesChange]=useEdgesState([]);
|
|
121
|
+
const [events,setEvents]=useState([]);
|
|
122
|
+
const agentMap=useRef({});
|
|
123
|
+
const modelCounts=useRef({});
|
|
124
|
+
|
|
125
|
+
const processEvent=useCallback((ev)=>{
|
|
126
|
+
setEvents(prev=>{
|
|
127
|
+
const next=[ev,...prev];
|
|
128
|
+
return next.length>MAX_EVENTS?next.slice(0,MAX_EVENTS):next;
|
|
129
|
+
});
|
|
130
|
+
const m=ev.model||(ev.event_type==='session_start'&&ev.reasoning?ev.reasoning.match(/opus|sonnet|haiku/i):null);
|
|
131
|
+
const mName=m?(typeof m==='string'?m:m[0]).toLowerCase():null;
|
|
132
|
+
if(mName){modelCounts.current[mName]=(modelCounts.current[mName]||0)+1;}
|
|
133
|
+
if(!ev.agent_id)return;
|
|
134
|
+
const id=ev.agent_id;
|
|
135
|
+
const prev=agentMap.current[id]||{count:0,outcome:null,parent:null,nodeType:null,firstTs:null};
|
|
136
|
+
const nodeType=prev.nodeType||(ev.event_type==='session_start'?'session':ev.event_type==='subagent_spawn'?(ev.reasoning||'task'):null);
|
|
137
|
+
agentMap.current[id]={
|
|
138
|
+
count:prev.count+1,
|
|
139
|
+
outcome:ev.outcome||prev.outcome,
|
|
140
|
+
parent:ev.parent_agent_id||prev.parent||null,
|
|
141
|
+
nodeType,
|
|
142
|
+
firstTs:prev.firstTs||ev.ts||null
|
|
143
|
+
};
|
|
144
|
+
const newNodes=Object.entries(agentMap.current).map(([aid,d])=>{
|
|
145
|
+
const isSession=d.nodeType==='session';
|
|
146
|
+
const dateStr=d.firstTs?new Date(d.firstTs).toLocaleDateString('en',{month:'short',day:'numeric'}):'';
|
|
147
|
+
const lbl=isSession?`Session · ${dateStr?dateStr+' ':''}${aid.slice(0,7)}`:(d.nodeType||aid.slice(0,8)+'..'+aid.slice(-4));
|
|
148
|
+
return{id:aid,type:'agentNode',data:{label:lbl,count:d.count,outcome:d.outcome||'',nodeType:d.nodeType||''},position:{x:0,y:0}};
|
|
149
|
+
});
|
|
150
|
+
const newEdges=Object.entries(agentMap.current)
|
|
151
|
+
.filter(([,d])=>d.parent&&d.parent!=='')
|
|
152
|
+
.map(([aid,d])=>({id:`${d.parent}-${aid}`,source:d.parent,target:aid,
|
|
153
|
+
markerEnd:{type:MarkerType.ArrowClosed},style:{stroke:'#30363d'}}));
|
|
154
|
+
const laid=layoutGraph(newNodes,newEdges);
|
|
155
|
+
setNodes(laid);
|
|
156
|
+
setEdges(newEdges);
|
|
157
|
+
},[]);
|
|
158
|
+
|
|
159
|
+
useEffect(()=>{
|
|
160
|
+
let es;
|
|
161
|
+
function connect(){
|
|
162
|
+
es=new EventSource(`http://localhost:${PORT}/events`);
|
|
163
|
+
es.onopen=()=>{
|
|
164
|
+
document.getElementById('status').className='status live';
|
|
165
|
+
document.getElementById('status-txt').textContent='Live';
|
|
166
|
+
};
|
|
167
|
+
es.onmessage=(e)=>{
|
|
168
|
+
if(e.data&&e.data.trim()&&!e.data.startsWith(':')){
|
|
169
|
+
try{processEvent(JSON.parse(e.data));}catch(err){}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
es.onerror=()=>{
|
|
173
|
+
document.getElementById('status').className='status wait';
|
|
174
|
+
document.getElementById('status-txt').textContent='Reconnecting...';
|
|
175
|
+
es.close();
|
|
176
|
+
setTimeout(connect,3000);
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
connect();
|
|
180
|
+
return()=>{if(es)es.close();};
|
|
181
|
+
},[processEvent]);
|
|
182
|
+
|
|
183
|
+
useEffect(()=>{
|
|
184
|
+
document.getElementById('ev-count').textContent=`${events.length} event${events.length!==1?'s':''}`;
|
|
185
|
+
const mp=document.getElementById('models'),mc=modelCounts.current,mEntries=Object.entries(mc).sort((a,b)=>b[1]-a[1]);
|
|
186
|
+
if(mEntries.length){const maxC=Math.max(...mEntries.map(e=>e[1]));
|
|
187
|
+
mp.innerHTML=mEntries.map(([n,c])=>`<div class="model-row"><span class="model-name">${n}</span><div class="model-bar-bg"><div class="model-bar ${n}" style="width:${Math.round((c/maxC)*100)}%"></div></div><span class="model-cnt">${c}</span></div>`).join('');}
|
|
188
|
+
const feed=document.getElementById('feed');
|
|
189
|
+
if(!events.length){feed.innerHTML='<div class="noevents">Waiting for events...</div>';return;}
|
|
190
|
+
feed.innerHTML=events.slice(0,50).map(ev=>{
|
|
191
|
+
const oc=outcomeClass(ev.outcome);
|
|
192
|
+
const ts=ev.ts?new Date(ev.ts).toLocaleTimeString():'';
|
|
193
|
+
const cmd=ev.command||ev.phase||'';
|
|
194
|
+
const evModel=ev.model||null;
|
|
195
|
+
return`<div class="ev${oc?' '+oc:''}">
|
|
196
|
+
<div class="ev-type">${ev.event_type||'event'}${evModel?` <span style="color:var(--muted);font-size:9px">[${evModel}]</span>`:''}${oc?`<span class="badge ${oc}">${ev.outcome}</span>`:''}</div>
|
|
197
|
+
${cmd?`<div class="ev-cmd">${cmd}</div>`:''}
|
|
198
|
+
${ev.reasoning?`<div class="ev-ts" style="color:var(--muted)">${ev.reasoning.slice(0,60)}</div>`:''}
|
|
199
|
+
<div class="ev-ts">${ts}</div>
|
|
200
|
+
</div>`;
|
|
201
|
+
}).join('');
|
|
202
|
+
},[events]);
|
|
203
|
+
|
|
204
|
+
if(!ReactFlow)return React.createElement('div',{style:{color:'#f85149',padding:'20px'}},'ReactFlow failed to load. Check CDN.');
|
|
205
|
+
return React.createElement(ReactFlow,{
|
|
206
|
+
nodes,edges,onNodesChange,onEdgesChange,nodeTypes,fitView:true,
|
|
207
|
+
style:{background:'var(--bg)'},
|
|
208
|
+
defaultEdgeOptions:{type:'smoothstep',animated:false}
|
|
209
|
+
},
|
|
210
|
+
React.createElement(Background,{color:'#30363d',gap:24,size:1}),
|
|
211
|
+
React.createElement(Controls,{style:{background:'var(--surface)',border:'1px solid var(--border)'}})
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
ReactDOM.render(React.createElement(Dashboard),document.getElementById('rf-root'));
|
|
216
|
+
|
|
217
|
+
// ── Metrics Panel ──────────────────────────────────────────────────────────
|
|
218
|
+
(function initMetrics(){
|
|
219
|
+
let trendChart=null;
|
|
220
|
+
function rateClass(r){return r>=0.8?'rate-good':r>=0.6?'rate-warn':'rate-bad';}
|
|
221
|
+
function fetchMetrics(){
|
|
222
|
+
fetch(`http://localhost:${PORT}/metrics`).then(r=>r.json()).then(data=>{
|
|
223
|
+
renderELO(data.rollups);renderTrend(data.rollups);renderHeatmap(data.rollups);
|
|
224
|
+
}).catch(()=>{});
|
|
225
|
+
}
|
|
226
|
+
function renderELO(rollups){
|
|
227
|
+
const el=document.getElementById('elo-display');
|
|
228
|
+
if(!rollups||!rollups.length){el.innerHTML='<span class="noevents">No metrics data</span>';return;}
|
|
229
|
+
const last=rollups[rollups.length-1];
|
|
230
|
+
const dc=last.elo_delta>=0?'up':'down';const arrow=last.elo_delta>=0?'+':'';
|
|
231
|
+
el.innerHTML=`<span class="elo-score">${Math.round(last.elo_after)}</span><span>ELO</span>`+
|
|
232
|
+
`<span class="elo-delta ${dc}">${arrow}${last.elo_delta.toFixed(1)}</span>`;
|
|
233
|
+
}
|
|
234
|
+
function renderTrend(rollups){
|
|
235
|
+
const ctx=document.getElementById('trend-chart');
|
|
236
|
+
if(!ctx||!rollups||rollups.length<1)return;
|
|
237
|
+
const labels=rollups.map(r=>r.milestone);
|
|
238
|
+
const rates=rollups.map(r=>(r.first_pass_rate*100).toFixed(1));
|
|
239
|
+
if(trendChart)trendChart.destroy();
|
|
240
|
+
trendChart=new Chart(ctx,{type:'line',data:{labels,datasets:[{label:'First-Pass %',
|
|
241
|
+
data:rates,borderColor:'#3fb950',backgroundColor:'rgba(63,185,80,0.1)',fill:true,tension:0.3}]},
|
|
242
|
+
options:{responsive:true,maintainAspectRatio:false,scales:{y:{min:0,max:100,
|
|
243
|
+
ticks:{color:'#7d8590',font:{size:9}}},x:{ticks:{color:'#7d8590',font:{size:9}}}},
|
|
244
|
+
plugins:{legend:{labels:{color:'#e6edf3',font:{size:10}}}}}});
|
|
245
|
+
}
|
|
246
|
+
function renderHeatmap(rollups){
|
|
247
|
+
const tbl=document.getElementById('domain-heat');const body=document.getElementById('domain-heat-body');
|
|
248
|
+
if(!rollups||!rollups.length||!tbl||!body){return;}
|
|
249
|
+
const last=rollups[rollups.length-1];
|
|
250
|
+
if(!last.domain_breakdown||!last.domain_breakdown.length)return;
|
|
251
|
+
tbl.style.display='table';
|
|
252
|
+
body.innerHTML=last.domain_breakdown.map(d=>{
|
|
253
|
+
const r=d.first_pass_rate;const cls=rateClass(r);
|
|
254
|
+
return `<tr><td>${d.domain}</td><td class="${cls}">${(r*100).toFixed(0)}%</td>`+
|
|
255
|
+
`<td>${d.avg_duration_s.toFixed(0)}</td><td>${d.tasks}</td></tr>`;
|
|
256
|
+
}).join('');
|
|
257
|
+
}
|
|
258
|
+
fetchMetrics();setInterval(fetchMetrics,30000);
|
|
259
|
+
})();
|
|
260
|
+
</script>
|
|
261
|
+
</body>
|
|
262
|
+
</html>
|