Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions clawmetry/static/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -6906,6 +6906,7 @@ function startLogStream() {
appendLogLine('ov-logs', data.line);
appendLogLine('logs-full', data.line);
processFlowEvent(data.line);
processStuckEvent(data.line);
document.getElementById('refresh-time').textContent = 'Live \u2022 ' + new Date().toLocaleTimeString();
};
logStream.onerror = function() {
Expand All @@ -6915,6 +6916,68 @@ function startLogStream() {
};
}

var _stuckSessions = {};

function processStuckEvent(line) {
try {
var obj = JSON.parse(line);
var event = obj.event || obj.type || obj.name || '';
var sessionId = obj.sessionId || obj.session_id || obj.key || '';
if (!sessionId) return;
if (event === 'session.stuck') {
var ageMs = obj.ageMs || obj.age_ms || obj.duration || 0;
_stuckSessions[sessionId] = {id: sessionId, ageMs: ageMs};
_updateStuckBanner();
} else if (event === 'session.state') {
var terminalStates = ['completed', 'error', 'failed', 'cancelled', 'terminated'];
var state = (obj.state || obj.status || '').toLowerCase();
if (terminalStates.indexOf(state) !== -1 && _stuckSessions[sessionId]) {
delete _stuckSessions[sessionId];
_updateStuckBanner();
}
}
} catch(e) {
if (line.indexOf('session.stuck') !== -1) {
var m = line.match(/session[._](?:id|key)["\s:=]+([^",\s}]+)/);
var sid = m ? m[1] : 'unknown';
_stuckSessions[sid] = {id: sid, ageMs: 0};
_updateStuckBanner();
}
}
}

function _updateStuckBanner() {
var banner = document.getElementById('stuck-banner');
var msgEl = document.getElementById('stuck-banner-msg');
var keys = Object.keys(_stuckSessions);
var count = keys.length;
_updateStuckBadge(count);
if (count === 0) {
if (banner) banner.style.display = 'none';
return;
}
if (!banner || !msgEl) return;
var ids = keys.slice(0, 3).map(function(id) {
return id.length > 14 ? id.slice(0, 14) + '…' : id;
}).join(', ');
msgEl.textContent = count === 1
? '⏱ Session stuck: ' + ids
: '⏱ ' + count + ' sessions stuck: ' + ids;
banner.style.display = 'flex';
}

function _updateStuckBadge(count) {
var badge = document.getElementById('nav-stuck-badge');
if (!badge) return;
badge.textContent = count;
badge.style.display = count > 0 ? 'inline' : 'none';
}

function dismissStuckBanner() {
var banner = document.getElementById('stuck-banner');
if (banner) banner.style.display = 'none';
}

function parseLogLine(line) {
try {
var obj = JSON.parse(line);
Expand Down
35 changes: 35 additions & 0 deletions clawmetry/templates/partials/banners.html
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,38 @@
font-weight:600;
">Dismiss</button>
</div>

<!-- Stuck Session Banner -->
<div id="stuck-banner" style="
display:none;
padding:10px 16px;
border-bottom:2px solid #a855f7;
font-size:13px;
font-weight:600;
align-items:center;
gap:10px;
background:#3b0764;
color:#e9d5ff;
">
<span style="font-size:18px;">&#9201;</span>
<span id="stuck-banner-msg" style="flex:1;"></span>
<button onclick="switchTab('overview')" style="
background:#7e22ce;
color:#fff;
border:none;
border-radius:6px;
padding:4px 12px;
font-size:12px;
cursor:pointer;
font-weight:600;
">View Sessions</button>
<button onclick="dismissStuckBanner()" style="
background:transparent;
color:#e9d5ff;
border:1px solid #7e22ce80;
border-radius:6px;
padding:4px 10px;
font-size:11px;
cursor:pointer;
">Dismiss</button>
</div>
4 changes: 2 additions & 2 deletions dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -3303,7 +3303,7 @@ def get_local_ip():
<div class="nav-tabs">
<div class="nav-tab" onclick="switchTab('flow')">Flow</div>
<div class="nav-tab" onclick="switchTab('brain')">Brain</div>
<div class="nav-tab active" onclick="switchTab('overview')">Overview</div>
<div class="nav-tab active" onclick="switchTab('overview')">Overview <span id="nav-stuck-badge" style="display:none;background:#a855f7;color:#fff;border-radius:10px;padding:1px 6px;font-size:10px;font-weight:700;margin-left:4px;">0</span></div>
<div class="nav-tab" onclick="switchTab('approvals')" title="Cloud-mediated approval queue">Approvals <span id="nav-approvals-badge" style="display:none;background:#ef4444;color:#fff;border-radius:10px;padding:1px 6px;font-size:10px;font-weight:700;margin-left:4px;">0</span></div>
<div class="nav-tab" onclick="switchTab('alerts')" title="Get notified when something goes wrong">Alerts</div>
<div class="nav-tab" onclick="switchTab('notifications')" title="Slack / Email / PagerDuty / Telegram channels">Notifications</div>
Expand Down Expand Up @@ -8588,7 +8588,7 @@ def get_local_ip():
<div class="nav-tabs">
<div class="nav-tab" onclick="switchTab('flow')">Flow</div>
<div class="nav-tab" onclick="switchTab('brain')">Brain</div>
<div class="nav-tab active" onclick="switchTab('overview')">Overview</div>
<div class="nav-tab active" onclick="switchTab('overview')">Overview <span id="nav-stuck-badge" style="display:none;background:#a855f7;color:#fff;border-radius:10px;padding:1px 6px;font-size:10px;font-weight:700;margin-left:4px;">0</span></div>
<div class="nav-tab" onclick="switchTab('approvals')" title="Cloud-mediated approval queue">Approvals <span id="nav-approvals-badge" style="display:none;background:#ef4444;color:#fff;border-radius:10px;padding:1px 6px;font-size:10px;font-weight:700;margin-left:4px;">0</span></div>
<div class="nav-tab" onclick="switchTab('alerts')" title="Get notified when something goes wrong">Alerts</div>
<div class="nav-tab" onclick="switchTab('notifications')" title="Slack / Email / PagerDuty / Telegram channels">Notifications</div>
Expand Down
Loading