saltylab-firmware/ui/event_log_panel.html
sl-webui 44691742c8 feat: WebUI event log panel (Issue #576)
Standalone 3-file filterable real-time event log (no build step).

Files:
  ui/event_log_panel.html  — layout, toolbar, empty state
  ui/event_log_panel.js    — rosbridge subscriptions, ring buffer, render
  ui/event_log_panel.css   — dark-theme, responsive grid layout

Features:
- 1000-entry ring buffer (oldest dropped when full, FIFO)
- Subscribes /rosout (rcl_interfaces/msg/Log) + /saltybot/events (std_msgs/String JSON)
- Severity filter buttons: DEBUG / INFO / WARN / ERROR / FATAL / EVENT (toggle on/off)
- Node name filter: select dropdown populated from seen nodes
- Live text search with <mark> highlight, Ctrl+F shortcut, Esc to clear
- Auto-scroll to latest entry; pauses on mouse hover (messages still buffered)
- Manual pause/resume button; detects user scroll-up and stops auto-scroll
- CSV export of current filtered view with timestamp (filename includes ISO date)
- Clear all entries button
- Color-coded by severity: left border stripe + text color per level
- Entry columns: timestamp (ms precision) | severity | node | message
- [system] entries for connect/disconnect events
- WS URL persisted in localStorage
- Responsive: node column hidden on narrow screens

ROS topics:
  SUB /rosout               rcl_interfaces/msg/Log  (all nodes)
  SUB /saltybot/events      std_msgs/String (JSON: {level,node,msg})

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 11:54:13 -04:00

91 lines
2.7 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<title>Saltybot — Event Log</title>
<link rel="stylesheet" href="event_log_panel.css">
<script src="https://cdn.jsdelivr.net/npm/roslib@1.3.0/build/roslib.min.js"></script>
</head>
<body>
<!-- ── Header ── -->
<div id="header">
<div class="logo">⚡ SALTYBOT — EVENT LOG</div>
<div id="conn-dot"></div>
<input id="ws-input" type="text" value="ws://localhost:9090" placeholder="ws://robot-ip:9090" />
<button id="btn-connect" class="hbtn">CONNECT</button>
<span id="conn-label" style="color:#4b5563;font-size:10px">Not connected</span>
<span id="paused-indicator">⏸ PAUSED</span>
<span id="count-badge">0 / 0</span>
</div>
<!-- ── Toolbar ── -->
<div id="toolbar">
<!-- Severity filters -->
<button class="sev-btn sev-debug" data-level="DEBUG">DEBUG</button>
<button class="sev-btn sev-info" data-level="INFO" >INFO</button>
<button class="sev-btn sev-warn" data-level="WARN" >WARN</button>
<button class="sev-btn sev-error" data-level="ERROR">ERROR</button>
<button class="sev-btn sev-fatal" data-level="FATAL">FATAL</button>
<button class="sev-btn sev-event" data-level="EVENT">EVENT</button>
<div class="toolbar-sep"></div>
<!-- Node filter -->
<select id="node-filter">
<option value="">All nodes</option>
</select>
<!-- Text search -->
<input id="search-input" type="text" placeholder="Search… (Ctrl+F)" />
<div class="toolbar-sep"></div>
<!-- Actions -->
<button id="btn-pause" class="hbtn active">⏸ PAUSE</button>
<button id="btn-clear" class="hbtn">CLEAR</button>
<button id="btn-export" class="hbtn">↓ CSV</button>
<!-- Ring buffer info -->
<span style="font-size:9px;color:#374151;margin-left:auto">
ring: 1000 entries · /rosout + /saltybot/events
</span>
</div>
<!-- ── Main ── -->
<div id="main">
<div id="log-feed">
<!-- Empty state -->
<div id="empty-state">
<div class="icon">📋</div>
<div>No events — connect to rosbridge</div>
<div style="font-size:10px;color:#374151">
Subscribing to /rosout and /saltybot/events
</div>
</div>
</div>
</div>
<!-- ── Footer ── -->
<div id="footer">
<span>rosbridge: <code id="footer-ws">ws://localhost:9090</code></span>
<span>topics: /rosout (rcl_interfaces/Log) · /saltybot/events (std_msgs/String)</span>
<span>event log — issue #576</span>
</div>
<script src="event_log_panel.js"></script>
<script>
// Sync footer WS URL
document.getElementById('ws-input').addEventListener('input', (e) => {
document.getElementById('footer-ws').textContent = e.target.value;
});
</script>
</body>
</html>