From 9041a9135cf2005a2b66fdcb2a9328df4c87c7f1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 7 May 2026 22:11:44 +0000 Subject: [PATCH] fix: surface session.stuck events as banner + nav badge When OpenClaw emits a session.stuck event in the log stream, detect it in startLogStream() and show a persistent purple banner with the stuck session IDs. A matching badge appears on the Overview nav tab (where active sessions are shown). Both clear automatically when a terminal session.state event arrives for that session. Dismissable without affecting the badge count. Closes #29 Co-Authored-By: Claude --- clawmetry/static/js/app.js | 63 +++++++++++++++++++++++ clawmetry/templates/partials/banners.html | 35 +++++++++++++ dashboard.py | 4 +- 3 files changed, 100 insertions(+), 2 deletions(-) diff --git a/clawmetry/static/js/app.js b/clawmetry/static/js/app.js index a5e903b9..539db938 100644 --- a/clawmetry/static/js/app.js +++ b/clawmetry/static/js/app.js @@ -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() { @@ -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); diff --git a/clawmetry/templates/partials/banners.html b/clawmetry/templates/partials/banners.html index 34efdfb6..884390f1 100644 --- a/clawmetry/templates/partials/banners.html +++ b/clawmetry/templates/partials/banners.html @@ -148,3 +148,38 @@ font-weight:600; ">Dismiss + + +
+ + + + +
diff --git a/dashboard.py b/dashboard.py index b26b67c6..cc20a246 100755 --- a/dashboard.py +++ b/dashboard.py @@ -3303,7 +3303,7 @@ def get_local_ip():