Skip to content

Commit 930d058

Browse files
committed
feat(v0.11): Live Log Tail tab — dedicated Logs page with SSE stream, filtering, and level badges (closes #27)
1 parent 77973a3 commit 930d058

1 file changed

Lines changed: 82 additions & 3 deletions

File tree

dashboard.py

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10110,6 +10110,34 @@ def get_local_ip():
1011010110
</div><!-- end page-nemoclaw (theme 2) -->
1011110111

1011210112

10113+
<div class="page" id="page-logs">
10114+
<div class="refresh-bar" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
10115+
<button class="refresh-btn" onclick="loadLogs()">&#8635; Refresh</button>
10116+
<label style="font-size:12px;color:var(--text-secondary);">Lines:
10117+
<input id="log-lines" type="number" value="200" min="10" max="2000" step="10"
10118+
style="width:60px;margin-left:4px;padding:3px 6px;border:1px solid var(--border-primary);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary);font-size:12px;">
10119+
</label>
10120+
<input id="log-filter" type="text" placeholder="Filter logs…" oninput="filterLogLines()"
10121+
style="padding:5px 10px;border-radius:7px;border:1px solid var(--border-primary);background:var(--bg-secondary);color:var(--text-primary);font-size:12px;width:180px;">
10122+
<div style="display:flex;gap:4px;flex-wrap:wrap;">
10123+
<button class="time-btn active" id="log-filter-all" onclick="setLogLevel('all',this)">All</button>
10124+
<button class="time-btn" id="log-filter-info" onclick="setLogLevel('info',this)">Info</button>
10125+
<button class="time-btn" id="log-filter-warn" onclick="setLogLevel('warn',this)">Warn</button>
10126+
<button class="time-btn" id="log-filter-error" onclick="setLogLevel('error',this)">Error</button>
10127+
</div>
10128+
<label style="display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-secondary);cursor:pointer;margin-left:auto;">
10129+
<input type="checkbox" id="log-autoscroll" checked> Auto-scroll
10130+
</label>
10131+
<span id="log-stream-status" style="font-size:11px;color:var(--text-muted);">&#9679; Connecting…</span>
10132+
</div>
10133+
<div class="card" style="padding:0;overflow:hidden;margin-top:8px;">
10134+
<div id="logs-full"
10135+
style="font-family:monospace;font-size:12px;padding:12px 16px;max-height:calc(100vh - 180px);overflow-y:auto;background:var(--bg-secondary);border-radius:8px;">
10136+
<div style="color:var(--text-muted);text-align:center;padding:24px;">Loading logs…</div>
10137+
</div>
10138+
</div>
10139+
</div>
10140+
1011310141
<script>
1011410142
// === Budget & Alert Functions ===
1011510143
function openBudgetModal() {
@@ -14574,17 +14602,25 @@ def get_local_ip():
1457414602
if (window.CLOUD_MODE) return;
1457514603
if (logStream) logStream.close();
1457614604
streamBuffer = [];
14605+
var statusEl = document.getElementById('log-stream-status');
14606+
if (statusEl) statusEl.textContent = '\u25cf Connecting\u2026';
1457714607
logStream = new EventSource('/api/logs-stream' + (localStorage.getItem('clawmetry-token') ? '?token=' + encodeURIComponent(localStorage.getItem('clawmetry-token')) : ''));
14608+
logStream.onopen = function() {
14609+
var s = document.getElementById('log-stream-status');
14610+
if (s) { s.textContent = '\u25cf Live'; s.style.color = '#22c55e'; }
14611+
};
1457814612
logStream.onmessage = function(e) {
1457914613
var data = JSON.parse(e.data);
1458014614
streamBuffer.push(data.line);
1458114615
if (streamBuffer.length > MAX_STREAM_LINES) streamBuffer.shift();
1458214616
appendLogLine('ov-logs', data.line);
1458314617
appendLogLine('logs-full', data.line);
1458414618
processFlowEvent(data.line);
14585-
document.getElementById('refresh-time').textContent = 'Live ' + new Date().toLocaleTimeString();
14619+
document.getElementById('refresh-time').textContent = 'Live \u2022 ' + new Date().toLocaleTimeString();
1458614620
};
1458714621
logStream.onerror = function() {
14622+
var s = document.getElementById('log-stream-status');
14623+
if (s) { s.textContent = '\u25cf Reconnecting\u2026'; s.style.color = '#f59e0b'; }
1458814624
setTimeout(startLogStream, 5000);
1458914625
};
1459014626
}
@@ -14632,11 +14668,54 @@ def get_local_ip():
1463214668
div.innerHTML = '<span class="' + parsed.cls + '">' + parsed.html + '</span>';
1463314669
el.appendChild(div);
1463414670
while (el.children.length > MAX_STREAM_LINES) el.removeChild(el.firstChild);
14635-
if (el.scrollHeight - el.scrollTop - el.clientHeight < 150) {
14636-
el.scrollTop = el.scrollHeight;
14671+
// Apply live filter for logs-full
14672+
if (elId === 'logs-full') {
14673+
var span = el.lastElementChild ? el.lastElementChild.querySelector('span') : null;
14674+
var cls = span ? span.className : '';
14675+
var text = el.lastElementChild ? el.lastElementChild.textContent.toLowerCase() : '';
14676+
var query = (document.getElementById('log-filter') ? document.getElementById('log-filter').value : '').toLowerCase();
14677+
var levelOk = _logLevelFilter === 'all'
14678+
|| (_logLevelFilter === 'error' && cls === 'err')
14679+
|| (_logLevelFilter === 'warn' && (cls === 'warn' || cls === 'err'))
14680+
|| (_logLevelFilter === 'info' && (cls === 'info' || cls === 'warn' || cls === 'err'));
14681+
if (!levelOk || (query && !text.includes(query))) el.lastElementChild.style.display = 'none';
14682+
}
14683+
var autoScroll = document.getElementById('log-autoscroll');
14684+
if (!autoScroll || autoScroll.checked) {
14685+
if (el.scrollHeight - el.scrollTop - el.clientHeight < 150) {
14686+
el.scrollTop = el.scrollHeight;
14687+
}
1463714688
}
1463814689
}
1463914690

14691+
14692+
// ===== Log Tab Helpers =====
14693+
var _logLevelFilter = 'all';
14694+
14695+
function setLogLevel(level, btn) {
14696+
_logLevelFilter = level;
14697+
document.querySelectorAll('#page-logs .time-btn').forEach(function(b) { b.classList.remove('active'); });
14698+
if (btn) btn.classList.add('active');
14699+
filterLogLines();
14700+
}
14701+
14702+
function filterLogLines() {
14703+
var el = document.getElementById('logs-full');
14704+
if (!el) return;
14705+
var query = (document.getElementById('log-filter') ? document.getElementById('log-filter').value : '').toLowerCase();
14706+
Array.from(el.children).forEach(function(div) {
14707+
var span = div.querySelector('span');
14708+
var cls = span ? span.className : '';
14709+
var text = div.textContent.toLowerCase();
14710+
var levelOk = _logLevelFilter === 'all'
14711+
|| (_logLevelFilter === 'error' && cls === 'err')
14712+
|| (_logLevelFilter === 'warn' && (cls === 'warn' || cls === 'err'))
14713+
|| (_logLevelFilter === 'info' && (cls === 'info' || cls === 'warn' || cls === 'err'));
14714+
var queryOk = !query || text.includes(query);
14715+
div.style.display = levelOk && queryOk ? '' : 'none';
14716+
});
14717+
}
14718+
1464014719
// ===== Flow Visualization Engine =====
1464114720
var flowStats = { messages: 0, events: 0, activeTools: {}, msgTimestamps: [] };
1464214721
var flowInitDone = false;

0 commit comments

Comments
 (0)