fix: /live dashboard JS — state field names, memory field, alert severity
Browse filesThree bugs prevented the service grid and alerts from rendering:
1. state.observation → state.current_observation
State model serialises the observation under current_observation,
not observation. Services and alerts were never rendered.
2. s.memory_utilization → s.memory_percent
ServiceStatus model field is memory_percent (0-100 float).
All MEM values showed as NaN.
3. a.severity === 'CRITICAL' → a.severity === 'critical'
Alert model severity is lowercase ("critical"/"warning"/"info").
Alert severity colours never applied; critical flash never fired.
Also fixed:
- services rendered via Array.slice().sort() instead of Object.entries()
(services is a JSON array, not a dict)
- max_steps is now dynamic from observation, not hardcoded 15
- rollback_service/block_ip → rollback/block_ip_range (correct action names)
- "WS DISCONNECTED" initial label → "API DISCONNECTED" (REST polling, not WS)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- server/app.py +36 -33
|
@@ -467,7 +467,7 @@ async def live_dashboard():
|
|
| 467 |
<div id="bottom-bar">
|
| 468 |
<div class="ws-status mono">
|
| 469 |
<div class="status-dot dot-grey" id="btm-dot"></div>
|
| 470 |
-
<span id="btm-text" style="color: var(--text-dim)">○
|
| 471 |
</div>
|
| 472 |
<div class="tip-text" id="tip-text">ⓘ Agents must read_logs before acting — blind remediation triggers -0.10 penalty</div>
|
| 473 |
<div class="footer-right mono">ARIA v2.0 · OpenEnv Compliant 🤗 Arijit-07</div>
|
|
@@ -608,7 +608,11 @@ async def live_dashboard():
|
|
| 608 |
|
| 609 |
function handleState(state) {{
|
| 610 |
if (!state.episode_id) return;
|
| 611 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 612 |
if (state.episode_id !== currentEpisodeId) {{
|
| 613 |
currentEpisodeId = state.episode_id;
|
| 614 |
lastStep = -1;
|
|
@@ -624,57 +628,55 @@ async def live_dashboard():
|
|
| 624 |
for (let i = Math.max(0, lastStep); i < state.action_history.length; i++) {{
|
| 625 |
const hist = state.action_history[i];
|
| 626 |
const act = hist.action;
|
| 627 |
-
|
| 628 |
if(act.action_type === 'diagnose') addLog('DIAGNOSE', act.root_cause);
|
| 629 |
-
else if(act.action_type === 'restart_service' || act.action_type === '
|
| 630 |
-
addLog('FIX', act.action_type, act.service || act.
|
| 631 |
else addLog('ACTION', act);
|
| 632 |
-
|
| 633 |
if(hist.reward !== undefined) {{
|
| 634 |
rewardHistory.push(hist.reward);
|
| 635 |
addLog('REWARD', hist.reward);
|
| 636 |
}}
|
| 637 |
}}
|
| 638 |
-
|
| 639 |
lastStep = state.step;
|
| 640 |
totalScore = state.total_reward;
|
| 641 |
-
|
| 642 |
-
document.getElementById('top-step').textContent = `${{state.step.toString().padStart(2,'0')}} /
|
| 643 |
-
document.getElementById('stat-step').textContent = `STEP ${{state.step}}/
|
| 644 |
updateScoreDisplay();
|
| 645 |
updateSparkline();
|
| 646 |
-
|
| 647 |
-
if (state.
|
| 648 |
addLog('EPISODE_END', state.total_reward, state.step);
|
| 649 |
-
// Ensure we don't duplicate end logs
|
| 650 |
lastStep = 99999;
|
| 651 |
}}
|
| 652 |
}}
|
| 653 |
|
| 654 |
-
if (
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
const svcs =
|
| 658 |
-
svcs.sort((a, b) => {{
|
| 659 |
const val = (st) => st === 'down' ? 0 : (st === 'degraded' ? 1 : 2);
|
| 660 |
return val(a.status) - val(b.status);
|
| 661 |
}});
|
| 662 |
-
|
| 663 |
const list = document.getElementById('service-list');
|
| 664 |
list.innerHTML = '';
|
| 665 |
document.getElementById('svc-count').textContent = svcs.length;
|
| 666 |
-
|
| 667 |
svcs.forEach(s => {{
|
| 668 |
let bcol = 'var(--border)', bgcol = 'var(--surface)', tcol = 'var(--text-dim)', stxt = '○ UNKNOWN';
|
| 669 |
if(s.status === 'down') {{ bcol = 'var(--red)'; bgcol = 'var(--red-dim)'; tcol = 'var(--red)'; stxt = '● DOWN'; }}
|
| 670 |
else if(s.status === 'degraded') {{ bcol = 'var(--yellow)'; bgcol = 'var(--yellow-dim)'; tcol = 'var(--yellow)'; stxt = '◐ DEGRADED'; }}
|
| 671 |
else if(s.status === 'healthy') {{ bcol = 'var(--green)'; bgcol = 'var(--green-dim)'; tcol = 'var(--green)'; stxt = '○ HEALTHY'; }}
|
| 672 |
-
|
| 673 |
let errRate = (s.error_rate * 100).toFixed(1);
|
| 674 |
-
let
|
| 675 |
let errCol = s.error_rate > 0.3 ? 'var(--red)' : (s.error_rate > 0.1 ? 'var(--yellow)' : 'var(--green)');
|
| 676 |
-
let memCol = s.
|
| 677 |
-
|
| 678 |
list.innerHTML += `
|
| 679 |
<div class="service-item mono" style="border-left: 3px solid ${{bcol}}; background: ${{bgcol}}">
|
| 680 |
<div>
|
|
@@ -683,33 +685,34 @@ async def live_dashboard():
|
|
| 683 |
</div>
|
| 684 |
<div class="svc-stats">
|
| 685 |
<div class="svc-stat-line" style="color:${{errCol}}">ERR ${{errRate}}%</div>
|
| 686 |
-
<div class="svc-stat-line" style="color:${{memCol}}">MEM ${{
|
| 687 |
</div>
|
| 688 |
</div>
|
| 689 |
`;
|
| 690 |
}});
|
| 691 |
}}
|
| 692 |
-
|
| 693 |
if (obs.active_alerts) {{
|
| 694 |
const alist = document.getElementById('alerts-list');
|
| 695 |
alist.innerHTML = '';
|
| 696 |
document.getElementById('alert-count').textContent = obs.active_alerts.length;
|
| 697 |
document.getElementById('alert-count').style.background = obs.active_alerts.length > 0 ? 'var(--red)' : 'var(--surface2)';
|
| 698 |
-
|
| 699 |
if(obs.active_alerts.length === 0) {{
|
| 700 |
alist.innerHTML = '<div class="no-alerts mono">◎ ALL SYSTEMS NOMINAL</div>';
|
| 701 |
}} else {{
|
| 702 |
let critFound = false;
|
| 703 |
obs.active_alerts.slice(0, 5).forEach(a => {{
|
| 704 |
let bg = 'var(--surface)', border = 'var(--border)', txtCol = '#000';
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
else if(a.severity === '
|
| 708 |
-
else {{ border = 'var(--
|
| 709 |
-
|
|
|
|
| 710 |
alist.innerHTML += `
|
| 711 |
<div class="alert-strip mono" style="border-left: 3px solid ${{border}}; background: ${{bg}}20">
|
| 712 |
-
<div class="alert-badge" style="background:${{bg}}; color:${{txtCol}}">${{a.severity}}</div>
|
| 713 |
<div class="alert-text">[${{a.service}}] ${{a.message}}</div>
|
| 714 |
</div>
|
| 715 |
`;
|
|
|
|
| 467 |
<div id="bottom-bar">
|
| 468 |
<div class="ws-status mono">
|
| 469 |
<div class="status-dot dot-grey" id="btm-dot"></div>
|
| 470 |
+
<span id="btm-text" style="color: var(--text-dim)">○ API DISCONNECTED</span>
|
| 471 |
</div>
|
| 472 |
<div class="tip-text" id="tip-text">ⓘ Agents must read_logs before acting — blind remediation triggers -0.10 penalty</div>
|
| 473 |
<div class="footer-right mono">ARIA v2.0 · OpenEnv Compliant 🤗 Arijit-07</div>
|
|
|
|
| 608 |
|
| 609 |
function handleState(state) {{
|
| 610 |
if (!state.episode_id) return;
|
| 611 |
+
|
| 612 |
+
// API returns current_observation not observation
|
| 613 |
+
const obs = state.current_observation || state.observation;
|
| 614 |
+
const maxSteps = (obs && obs.max_steps) || 15;
|
| 615 |
+
|
| 616 |
if (state.episode_id !== currentEpisodeId) {{
|
| 617 |
currentEpisodeId = state.episode_id;
|
| 618 |
lastStep = -1;
|
|
|
|
| 628 |
for (let i = Math.max(0, lastStep); i < state.action_history.length; i++) {{
|
| 629 |
const hist = state.action_history[i];
|
| 630 |
const act = hist.action;
|
| 631 |
+
|
| 632 |
if(act.action_type === 'diagnose') addLog('DIAGNOSE', act.root_cause);
|
| 633 |
+
else if(act.action_type === 'restart_service' || act.action_type === 'rollback' || act.action_type === 'block_ip_range')
|
| 634 |
+
addLog('FIX', act.action_type, act.service || act.ip_range);
|
| 635 |
else addLog('ACTION', act);
|
| 636 |
+
|
| 637 |
if(hist.reward !== undefined) {{
|
| 638 |
rewardHistory.push(hist.reward);
|
| 639 |
addLog('REWARD', hist.reward);
|
| 640 |
}}
|
| 641 |
}}
|
| 642 |
+
|
| 643 |
lastStep = state.step;
|
| 644 |
totalScore = state.total_reward;
|
| 645 |
+
|
| 646 |
+
document.getElementById('top-step').textContent = `${{state.step.toString().padStart(2,'0')}} / ${{maxSteps}}`;
|
| 647 |
+
document.getElementById('stat-step').textContent = `STEP ${{state.step}}/${{maxSteps}}`;
|
| 648 |
updateScoreDisplay();
|
| 649 |
updateSparkline();
|
| 650 |
+
|
| 651 |
+
if (state.incident_resolved) {{
|
| 652 |
addLog('EPISODE_END', state.total_reward, state.step);
|
|
|
|
| 653 |
lastStep = 99999;
|
| 654 |
}}
|
| 655 |
}}
|
| 656 |
|
| 657 |
+
if (obs) {{
|
| 658 |
+
if (obs.services && obs.services.length > 0) {{
|
| 659 |
+
// services is a JSON array — sort in place
|
| 660 |
+
const svcs = obs.services.slice().sort((a, b) => {{
|
|
|
|
| 661 |
const val = (st) => st === 'down' ? 0 : (st === 'degraded' ? 1 : 2);
|
| 662 |
return val(a.status) - val(b.status);
|
| 663 |
}});
|
| 664 |
+
|
| 665 |
const list = document.getElementById('service-list');
|
| 666 |
list.innerHTML = '';
|
| 667 |
document.getElementById('svc-count').textContent = svcs.length;
|
| 668 |
+
|
| 669 |
svcs.forEach(s => {{
|
| 670 |
let bcol = 'var(--border)', bgcol = 'var(--surface)', tcol = 'var(--text-dim)', stxt = '○ UNKNOWN';
|
| 671 |
if(s.status === 'down') {{ bcol = 'var(--red)'; bgcol = 'var(--red-dim)'; tcol = 'var(--red)'; stxt = '● DOWN'; }}
|
| 672 |
else if(s.status === 'degraded') {{ bcol = 'var(--yellow)'; bgcol = 'var(--yellow-dim)'; tcol = 'var(--yellow)'; stxt = '◐ DEGRADED'; }}
|
| 673 |
else if(s.status === 'healthy') {{ bcol = 'var(--green)'; bgcol = 'var(--green-dim)'; tcol = 'var(--green)'; stxt = '○ HEALTHY'; }}
|
| 674 |
+
|
| 675 |
let errRate = (s.error_rate * 100).toFixed(1);
|
| 676 |
+
let memPct = (s.memory_percent || 0).toFixed(1);
|
| 677 |
let errCol = s.error_rate > 0.3 ? 'var(--red)' : (s.error_rate > 0.1 ? 'var(--yellow)' : 'var(--green)');
|
| 678 |
+
let memCol = (s.memory_percent || 0) > 90 ? 'var(--red)' : ((s.memory_percent || 0) > 70 ? 'var(--yellow)' : 'var(--green)');
|
| 679 |
+
|
| 680 |
list.innerHTML += `
|
| 681 |
<div class="service-item mono" style="border-left: 3px solid ${{bcol}}; background: ${{bgcol}}">
|
| 682 |
<div>
|
|
|
|
| 685 |
</div>
|
| 686 |
<div class="svc-stats">
|
| 687 |
<div class="svc-stat-line" style="color:${{errCol}}">ERR ${{errRate}}%</div>
|
| 688 |
+
<div class="svc-stat-line" style="color:${{memCol}}">MEM ${{memPct}}%</div>
|
| 689 |
</div>
|
| 690 |
</div>
|
| 691 |
`;
|
| 692 |
}});
|
| 693 |
}}
|
| 694 |
+
|
| 695 |
if (obs.active_alerts) {{
|
| 696 |
const alist = document.getElementById('alerts-list');
|
| 697 |
alist.innerHTML = '';
|
| 698 |
document.getElementById('alert-count').textContent = obs.active_alerts.length;
|
| 699 |
document.getElementById('alert-count').style.background = obs.active_alerts.length > 0 ? 'var(--red)' : 'var(--surface2)';
|
| 700 |
+
|
| 701 |
if(obs.active_alerts.length === 0) {{
|
| 702 |
alist.innerHTML = '<div class="no-alerts mono">◎ ALL SYSTEMS NOMINAL</div>';
|
| 703 |
}} else {{
|
| 704 |
let critFound = false;
|
| 705 |
obs.active_alerts.slice(0, 5).forEach(a => {{
|
| 706 |
let bg = 'var(--surface)', border = 'var(--border)', txtCol = '#000';
|
| 707 |
+
// Alert severity is lowercase: "critical", "warning", "info"
|
| 708 |
+
if(a.severity === 'critical') {{ border = 'var(--red)'; bg = 'var(--red)'; critFound = true; }}
|
| 709 |
+
else if(a.severity === 'high') {{ border = '#ff6600'; bg = '#ff6600'; }}
|
| 710 |
+
else if(a.severity === 'warning') {{ border = 'var(--yellow)'; bg = 'var(--yellow)'; }}
|
| 711 |
+
else {{ border = 'var(--blue)'; bg = 'var(--blue)'; txtCol = '#fff'; }}
|
| 712 |
+
|
| 713 |
alist.innerHTML += `
|
| 714 |
<div class="alert-strip mono" style="border-left: 3px solid ${{border}}; background: ${{bg}}20">
|
| 715 |
+
<div class="alert-badge" style="background:${{bg}}; color:${{txtCol}}">${{a.severity.toUpperCase()}}</div>
|
| 716 |
<div class="alert-text">[${{a.service}}] ${{a.message}}</div>
|
| 717 |
</div>
|
| 718 |
`;
|