@steadwing/openalerts 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,924 @@
1
+ /**
2
+ * OpenAlerts real-time monitoring dashboard.
3
+ * Tabs: Activity (unified event + log timeline), System Logs, Health.
4
+ * Activity shows both OpenAlerts engine events AND OpenClaw internal logs
5
+ * grouped by sessionId for a complete picture of what's happening.
6
+ */
7
+ export function getDashboardHtml() {
8
+ return `<!DOCTYPE html>
9
+ <html lang="en">
10
+ <head>
11
+ <meta charset="utf-8">
12
+ <meta name="viewport" content="width=device-width,initial-scale=1">
13
+ <title>OpenAlerts Monitor</title>
14
+ <style>
15
+ *{margin:0;padding:0;box-sizing:border-box}
16
+ body{font-family:'SF Mono','Cascadia Code','Consolas',monospace;background:#0d1117;color:#c9d1d9;font-size:13px;overflow:hidden;height:100vh}
17
+ .grid{display:grid;grid-template-rows:auto auto 1fr;height:100vh}
18
+
19
+ /* ── Top bar ──────────────────── */
20
+ .topbar{background:#161b22;border-bottom:1px solid #30363d;padding:8px 16px;display:flex;align-items:center;gap:16px;flex-wrap:wrap}
21
+ .topbar h1{font-size:14px;font-weight:600;color:#f0f6fc;letter-spacing:0.5px}
22
+ .dot{width:7px;height:7px;border-radius:50%;display:inline-block;margin-right:4px}
23
+ .dot.live{background:#3fb950;animation:pulse 2s infinite}
24
+ .dot.dead{background:#f85149}
25
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}}
26
+ .stat{color:#8b949e;font-size:11px}
27
+ .stat b{color:#c9d1d9;font-weight:500}
28
+
29
+ /* ── Tabs ──────────────────── */
30
+ .tabbar{background:#161b22;border-bottom:1px solid #30363d;display:flex}
31
+ .tab{padding:7px 18px;font-size:12px;font-weight:600;color:#8b949e;cursor:pointer;border-bottom:2px solid transparent;transition:all 0.15s}
32
+ .tab:hover{color:#c9d1d9;background:#1c2129}
33
+ .tab.active{color:#58a6ff;border-bottom-color:#58a6ff}
34
+ .tab-content{display:none;overflow:hidden;flex:1}
35
+ .tab-content.active{display:flex;overflow:hidden}
36
+ .content{display:flex;flex-direction:column;overflow:hidden}
37
+
38
+ /* ── Activity layout ──────────────────── */
39
+ .activity-panels{display:grid;grid-template-columns:1fr 280px;gap:0;overflow:hidden;flex:1}
40
+ @media(max-width:900px){.activity-panels{grid-template-columns:1fr}}
41
+ .panel{display:flex;flex-direction:column;overflow:hidden}
42
+ .panel-header{background:#161b22;padding:6px 12px;font-size:11px;font-weight:600;color:#8b949e;text-transform:uppercase;letter-spacing:0.8px;border-bottom:1px solid #30363d;flex-shrink:0;display:flex;align-items:center;justify-content:space-between}
43
+ .panel:first-child{border-right:1px solid #30363d}
44
+ .scroll{flex:1;overflow-y:auto}
45
+ .scroll::-webkit-scrollbar{width:5px}
46
+ .scroll::-webkit-scrollbar-thumb{background:#30363d;border-radius:3px}
47
+ .empty-msg{color:#484f58;padding:30px 14px;text-align:center;font-style:italic;font-size:12px}
48
+
49
+ /* ── Session flow (collapsible group) ──────────────────── */
50
+ .flow{border-bottom:1px solid #21262d}
51
+ .flow-hdr{padding:6px 10px;display:flex;align-items:center;gap:6px;cursor:pointer;font-size:11px;background:#161b22;border-left:3px solid #30363d;transition:all 0.12s}
52
+ .flow-hdr:hover{background:#1c2129}
53
+ .flow.active .flow-hdr{border-left-color:#58a6ff}
54
+ .flow.done .flow-hdr{border-left-color:#3fb950}
55
+ .flow.error .flow-hdr{border-left-color:#f85149}
56
+ .flow-arr{color:#484f58;font-size:9px;width:12px;text-align:center;transition:transform 0.12s;flex-shrink:0}
57
+ .flow-arr.shut{transform:rotate(-90deg)}
58
+ .flow-lbl{font-weight:600;color:#c9d1d9;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:260px}
59
+ .flow-badge{font-size:9px;padding:1px 5px;border-radius:3px;font-weight:700;flex-shrink:0}
60
+ .flow-badge.active{background:#1f3a5f;color:#58a6ff}
61
+ .flow-badge.done{background:#1a3a2a;color:#3fb950}
62
+ .flow-badge.error{background:#3d1a1a;color:#f85149}
63
+ .flow-info{color:#484f58;font-size:10px;margin-left:auto;white-space:nowrap;flex-shrink:0}
64
+ .flow-body{overflow:hidden;transition:max-height 0.2s ease-out}
65
+ .flow-body.shut{max-height:0!important;overflow:hidden}
66
+
67
+ /* ── Event/Log row ──────────────────── */
68
+ .row{padding:3px 10px 3px 24px;border-top:1px solid #0d1117;font-size:11px;line-height:1.5;animation:fi 0.15s ease}
69
+ .row:hover{background:#0d1117}
70
+ .row.standalone{padding-left:10px;border-bottom:1px solid #21262d;border-top:none}
71
+ .row.deep{padding-left:38px}
72
+
73
+ /* OpenAlerts event row */
74
+ .row .r-main{display:flex;align-items:center;gap:5px}
75
+ .r-time{color:#484f58;font-size:10px;min-width:55px;flex-shrink:0}
76
+ .r-icon{width:14px;text-align:center;flex-shrink:0;font-size:11px}
77
+ .r-type{font-weight:600;min-width:90px;font-size:10px;flex-shrink:0}
78
+ .r-type.llm{color:#58a6ff} .r-type.tool{color:#bc8cff} .r-type.agent{color:#3fb950}
79
+ .r-type.session{color:#d29922} .r-type.infra{color:#f85149} .r-type.custom{color:#8b949e}
80
+ .r-type.watchdog{color:#6e7681}
81
+ .r-oc{font-size:9px;padding:0 4px;border-radius:3px;font-weight:700;flex-shrink:0}
82
+ .r-oc.success{background:#1a3a2a;color:#3fb950}
83
+ .r-oc.error{background:#3d1a1a;color:#f85149}
84
+ .r-oc.timeout{background:#3d2e1a;color:#d29922}
85
+ .r-pills{display:flex;gap:4px;flex-wrap:wrap;margin-left:auto;align-items:center}
86
+ .p{font-size:9px;background:#21262d;padding:0 5px;border-radius:3px;white-space:nowrap}
87
+ .p.t{color:#bc8cff;background:#2a1f3d} .p.d{color:#d29922} .p.tk{color:#58a6ff}
88
+ .p.q{color:#f0883e} .p.m{color:#8b949e} .p.ch{color:#d2a8ff} .p.s{color:#6e7681;font-size:8px}
89
+ .r-det{padding:1px 0 1px 70px;color:#6e7681;font-size:10px}
90
+ .r-det .err{color:#f85149} .r-det .dim{color:#484f58} .r-det .sc{color:#d29922}
91
+
92
+ /* OpenClaw log row */
93
+ .row.log .r-main{display:flex;align-items:baseline;gap:5px}
94
+ .r-lvl{font-size:9px;font-weight:700;min-width:38px;flex-shrink:0}
95
+ .r-lvl.DEBUG{color:#6e7681} .r-lvl.INFO{color:#58a6ff} .r-lvl.WARN{color:#d29922} .r-lvl.ERROR{color:#f85149}
96
+ .r-sub{color:#bc8cff;font-size:10px;min-width:100px;max-width:140px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
97
+ .r-msg{color:#c9d1d9;font-size:11px;word-break:break-word}
98
+ .r-kvs{color:#484f58;font-size:10px;padding-left:70px}
99
+ .r-kvs span{margin-right:8px}
100
+
101
+ @keyframes fi{from{opacity:0;transform:translateY(-2px)}to{opacity:1;transform:none}}
102
+
103
+ /* ── Alerts panel ──────────────────── */
104
+ .al{padding:6px 10px;border-bottom:1px solid #21262d;font-size:11px;animation:fi 0.2s}
105
+ .al-sev{font-weight:700;text-transform:uppercase;font-size:9px;letter-spacing:0.4px}
106
+ .al-sev.error{color:#f85149} .al-sev.warn{color:#d29922} .al-sev.critical{color:#ff7b72} .al-sev.info{color:#58a6ff}
107
+ .al-title{color:#c9d1d9;margin-top:1px;font-size:11px}
108
+ .al-detail{color:#8b949e;margin-top:1px;font-size:10px}
109
+ .al-time{color:#484f58;font-size:10px;margin-top:1px}
110
+
111
+ /* ── Rules ──────────────────── */
112
+ .rules{border-top:1px solid #30363d;padding:6px 10px;flex-shrink:0}
113
+ .rules h3{font-size:10px;color:#8b949e;text-transform:uppercase;letter-spacing:0.7px;margin-bottom:4px}
114
+ .rl{display:flex;align-items:center;gap:5px;font-size:11px;padding:1px 0}
115
+ .rl-d{width:5px;height:5px;border-radius:50%;flex-shrink:0}
116
+ .rl-d.ok{background:#3fb950} .rl-d.fired{background:#f85149;animation:pulse 1s infinite}
117
+ .rl-s{color:#8b949e;margin-left:auto;font-size:10px}
118
+
119
+ /* ── Logs tab ──────────────────── */
120
+ .logs-t{flex:1;display:flex;flex-direction:column;overflow:hidden}
121
+ .log-bar{background:#161b22;padding:8px 12px;border-bottom:1px solid #30363d;display:flex;align-items:center;gap:10px;flex-wrap:wrap;flex-shrink:0;font-size:11px}
122
+ .log-bar select,.log-bar input{background:#0d1117;border:1px solid #30363d;color:#c9d1d9;font-family:inherit;font-size:10px;padding:3px 6px;border-radius:3px}
123
+ .log-bar input[type="text"]{min-width:180px}
124
+ .log-bar button{background:#21262d;border:1px solid #30363d;color:#c9d1d9;font-family:inherit;font-size:10px;padding:3px 10px;border-radius:3px;cursor:pointer;transition:all 0.12s}
125
+ .log-bar button:hover{background:#30363d;border-color:#484f58}
126
+ .log-bar button:active{background:#0d1117}
127
+ .log-bar label{color:#8b949e;display:flex;align-items:center;gap:4px;cursor:pointer}
128
+ .log-bar label:hover{color:#c9d1d9}
129
+ .log-bar input[type="checkbox"]{cursor:pointer}
130
+ .log-filters{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
131
+ .log-filters label{font-size:10px}
132
+ .log-truncate{background:#3d2e1a;border:1px solid #d29922;color:#d29922;padding:4px 10px;font-size:10px;border-radius:3px;margin:8px 12px;display:none}
133
+ .log-truncate.show{display:block}
134
+ .log-list{flex:1;overflow-y:auto;font-size:11px}
135
+ .log-e{padding:2px 12px;border-bottom:1px solid #161b22;display:flex;gap:6px;align-items:baseline;position:relative}
136
+ .log-e:hover{background:#161b22}
137
+ .log-e:hover .log-copy{opacity:1}
138
+ .log-ts{color:#484f58;font-size:10px;min-width:70px;flex-shrink:0}
139
+ .log-lv{font-size:9px;font-weight:700;min-width:42px;flex-shrink:0}
140
+ .log-lv.TRACE{color:#6e7681} .log-lv.DEBUG{color:#8b949e} .log-lv.INFO{color:#58a6ff} .log-lv.WARN{color:#d29922} .log-lv.ERROR{color:#f85149} .log-lv.FATAL{color:#ff7b72;background:#3d1a1a;padding:1px 3px;border-radius:2px}
141
+ .log-su{color:#bc8cff;font-size:10px;min-width:110px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
142
+ .log-mg{color:#c9d1d9;word-break:break-all;flex:1}
143
+ .log-copy{position:absolute;right:8px;top:4px;font-size:9px;color:#484f58;cursor:pointer;border:1px solid #30363d;background:#161b22;padding:1px 4px;border-radius:2px;font-family:inherit;opacity:0;transition:opacity 0.12s}
144
+ .log-copy:hover{color:#c9d1d9;border-color:#484f58}
145
+
146
+ /* ── Health tab ──────────────────── */
147
+ .health-t{flex:1;overflow-y:auto;padding:14px}
148
+ .h-sec{margin-bottom:16px}
149
+ .h-sec h3{font-size:11px;color:#8b949e;text-transform:uppercase;letter-spacing:0.7px;margin-bottom:6px;padding-bottom:3px;border-bottom:1px solid #21262d}
150
+ .h-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:6px}
151
+ .h-card{background:#161b22;border:1px solid #21262d;border-radius:5px;padding:8px 12px}
152
+ .h-card .lb{color:#8b949e;font-size:10px;margin-bottom:2px}
153
+ .h-card .vl{color:#c9d1d9;font-size:13px;font-weight:600}
154
+ .h-card .vl.ok{color:#3fb950} .h-card .vl.bad{color:#f85149}
155
+ .h-tbl{width:100%;border-collapse:collapse}
156
+ .h-tbl td{padding:3px 8px;font-size:11px;border-bottom:1px solid #161b22}
157
+ .h-tbl td:first-child{color:#8b949e;width:140px}
158
+ .h-tbl th{padding:3px 8px;font-size:10px;color:#8b949e;text-align:left;border-bottom:1px solid #30363d;font-weight:600}
159
+
160
+ /* ── Expandable event detail ──────────────────── */
161
+ .row.expandable{cursor:pointer}
162
+ .row.expandable:hover{background:#161b22}
163
+ .ev-detail{background:#0d1117;border:1px solid #21262d;border-radius:4px;margin:4px 10px 6px 24px;padding:8px 10px;display:none;animation:fi 0.15s ease}
164
+ .ev-detail.open{display:block}
165
+ .ev-detail h4{font-size:10px;color:#58a6ff;text-transform:uppercase;letter-spacing:0.6px;margin:6px 0 3px;font-weight:600}
166
+ .ev-detail h4:first-child{margin-top:0}
167
+ .ev-detail .dv{display:flex;gap:4px;align-items:baseline;font-size:11px;padding:1px 0}
168
+ .ev-detail .dk{color:#8b949e;min-width:90px;flex-shrink:0}
169
+ .ev-detail .dd{color:#c9d1d9;word-break:break-all}
170
+ .ev-detail .dd .cp-btn{font-size:9px;color:#484f58;cursor:pointer;margin-left:4px;border:1px solid #30363d;background:#161b22;padding:0 3px;border-radius:2px;font-family:inherit}
171
+ .ev-detail .dd .cp-btn:hover{color:#c9d1d9;border-color:#484f58}
172
+ .ev-detail .err-block{background:#1a0a0a;border:1px solid #3d1a1a;border-radius:3px;padding:6px 8px;color:#f85149;font-size:11px;max-height:120px;overflow-y:auto;white-space:pre-wrap;word-break:break-all}
173
+ .ev-detail .meta-grid{display:grid;grid-template-columns:auto 1fr;gap:2px 8px;font-size:11px}
174
+ .ev-detail .meta-grid .mk{color:#8b949e;text-align:right}
175
+ .ev-detail .meta-grid .mv{color:#c9d1d9;word-break:break-all}
176
+
177
+ /* Inline row indicators */
178
+ .sev-dot{width:6px;height:6px;border-radius:50%;display:inline-block;flex-shrink:0}
179
+ .sev-dot.info{background:#58a6ff} .sev-dot.warn{background:#d29922} .sev-dot.error{background:#f85149} .sev-dot.critical{background:#ff7b72}
180
+ .p.cost{color:#3fb950;background:#1a3a2a} .p.agent{color:#d2a8ff;background:#2a1f3d}
181
+
182
+ /* ── Flow summary bar ──────────────────── */
183
+ .flow-summary{background:#0d1117;padding:4px 10px;font-size:10px;color:#8b949e;display:flex;flex-wrap:wrap;gap:4px 12px;border-bottom:1px solid #21262d;align-items:center}
184
+ .flow-summary .fs-v{color:#c9d1d9;font-weight:600}
185
+ .flow-summary .fs-err{color:#f85149;font-weight:600}
186
+ .flow-summary .fs-tools{color:#bc8cff}
187
+ .flow-summary .fs-agent{color:#d2a8ff}
188
+
189
+ /* ── Expandable log row ──────────────────── */
190
+ .log-e{cursor:pointer;transition:background 0.1s}
191
+ .log-detail{display:none;background:#0d1117;border:1px solid #21262d;border-radius:3px;margin:2px 12px 4px 12px;padding:6px 8px;animation:fi 0.12s ease}
192
+ .log-detail.open{display:block}
193
+ .log-detail .ld-grid{display:grid;grid-template-columns:auto 1fr;gap:1px 8px;font-size:10px}
194
+ .log-detail .ld-grid .lk{color:#8b949e;text-align:right}
195
+ .log-detail .ld-grid .lv{color:#c9d1d9;word-break:break-all}
196
+ .log-detail .ld-file{color:#484f58;font-size:10px;margin-top:3px}
197
+ </style>
198
+ </head>
199
+ <body>
200
+ <div class="grid">
201
+ <div class="topbar">
202
+ <h1><span class="dot dead" id="sDot"></span> OPENALERTS</h1>
203
+ <span class="stat" id="sConn">connecting...</span>
204
+ <span class="stat">up: <b id="sUp">--</b></span>
205
+ <span class="stat">msgs: <b id="sMsgs">0</b></span>
206
+ <span class="stat">err: <b id="sErr">0</b></span>
207
+ <span class="stat">tools: <b id="sTools">0</b></span>
208
+ <span class="stat">agents: <b id="sAgents">0</b></span>
209
+ </div>
210
+ <div class="tabbar">
211
+ <div class="tab active" data-tab="activity">Activity</div>
212
+ <div class="tab" data-tab="logs">System Logs</div>
213
+ <div class="tab" data-tab="health">Health</div>
214
+ <div class="tab" data-tab="debug">Debug</div>
215
+ </div>
216
+ <div class="content">
217
+ <!-- Activity -->
218
+ <div class="tab-content active" id="tab-activity">
219
+ <div class="activity-panels">
220
+ <div class="panel">
221
+ <div class="panel-header"><span>Live Timeline</span><span style="color:#484f58;font-weight:400" id="evCnt">0</span></div>
222
+ <div class="scroll" id="evList"><div class="empty-msg" id="emptyMsg">Waiting for events... send a message to your bot.</div></div>
223
+ </div>
224
+ <div class="panel">
225
+ <div class="panel-header">Alerts</div>
226
+ <div class="scroll" id="alList"><div class="empty-msg" id="alEmpty">No alerts.</div></div>
227
+ <div class="rules" id="rulesEl"><h3>Rules</h3></div>
228
+ </div>
229
+ </div>
230
+ </div>
231
+ <!-- System Logs -->
232
+ <div class="tab-content" id="tab-logs">
233
+ <div class="logs-t">
234
+ <div class="log-bar">
235
+ <label>Subsystem:</label>
236
+ <select id="lF"><option value="">All</option></select>
237
+ <input type="text" id="lS" placeholder="Search logs..." title="Ctrl+F to focus">
238
+ <div class="log-filters">
239
+ <label title="Show TRACE logs"><input type="checkbox" id="lv-TRACE" checked> TRACE</label>
240
+ <label title="Show DEBUG logs"><input type="checkbox" id="lv-DEBUG" checked> DEBUG</label>
241
+ <label title="Show INFO logs"><input type="checkbox" id="lv-INFO" checked> INFO</label>
242
+ <label title="Show WARN logs"><input type="checkbox" id="lv-WARN" checked> WARN</label>
243
+ <label title="Show ERROR logs"><input type="checkbox" id="lv-ERROR" checked> ERROR</label>
244
+ <label title="Show FATAL logs"><input type="checkbox" id="lv-FATAL" checked> FATAL</label>
245
+ </div>
246
+ <button id="lR" title="Refresh logs (Ctrl+R)">↻ Refresh</button>
247
+ <button id="lE" title="Export filtered logs">⬇ Export</button>
248
+ <label style="margin-left:auto" title="Auto-scroll to new logs"><input type="checkbox" id="lA" checked> Auto-follow</label>
249
+ </div>
250
+ <div class="log-truncate" id="lT">⚠ Logs truncated - showing most recent entries only</div>
251
+ <div class="log-list" id="logList"><div class="empty-msg">Loading...</div></div>
252
+ </div>
253
+ </div>
254
+ <!-- Health -->
255
+ <div class="tab-content" id="tab-health">
256
+ <div class="health-t" id="hC"><div class="empty-msg">Loading...</div></div>
257
+ </div>
258
+ <!-- Debug -->
259
+ <div class="tab-content" id="tab-debug">
260
+ <div class="health-t">
261
+ <div class="h-sec">
262
+ <h3>OpenAlerts State Snapshot</h3>
263
+ <button id="dbRefresh" style="margin-left:10px;padding:4px 12px;font-size:11px">↻ Refresh</button>
264
+ <div class="h-grid" id="dbState" style="margin-top:12px"></div>
265
+ </div>
266
+ <div class="h-sec">
267
+ <h3>Recent Events (Last 20)</h3>
268
+ <div id="dbEvents" style="font-size:11px;max-height:400px;overflow-y:auto"></div>
269
+ </div>
270
+ <div class="h-sec">
271
+ <h3>Alert Rules Status</h3>
272
+ <div id="dbRules"></div>
273
+ </div>
274
+ <div class="h-sec">
275
+ <h3>Circuit Breakers</h3>
276
+ <div id="dbCircuit" style="font-size:11px"></div>
277
+ </div>
278
+ <div class="h-sec">
279
+ <h3>Task Timeouts</h3>
280
+ <div id="dbTasks" style="font-size:11px"></div>
281
+ </div>
282
+ </div>
283
+ </div>
284
+ </div>
285
+ </div>
286
+ <script>
287
+ (function(){
288
+ var MAX_FLOWS=150, MAX_STANDALONE=300, MAX_ALERTS=50, STALE_MS=120000;
289
+ var total=0, paused=false, evSrc=null;
290
+ var $=function(i){return document.getElementById(i)};
291
+ var evList=$('evList'), emptyMsg=$('emptyMsg'), alList=$('alList'), alEmpty=$('alEmpty'), evCnt=$('evCnt');
292
+
293
+ function esc(s){var d=document.createElement('div');d.textContent=s;return d.innerHTML}
294
+ function cat(t){if(!t)return'custom';var p=t.split('.')[0];return['llm','tool','agent','session','infra','watchdog'].indexOf(p)>=0?p:'custom'}
295
+ function fT(ts){if(!ts)return'';var d=typeof ts==='number'?new Date(ts):new Date(ts);return d.toLocaleTimeString('en-US',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'})}
296
+ function fISO(ts){if(!ts)return'';return new Date(typeof ts==='number'?ts:Date.parse(ts)).toISOString()}
297
+ function fD(ms){if(ms==null)return'';if(ms<1000)return ms+'ms';if(ms<60000)return(ms/1000).toFixed(1)+'s';return Math.floor(ms/60000)+'m '+Math.round((ms%60000)/1000)+'s'}
298
+ function fU(ms){var s=Math.floor(ms/1000),m=Math.floor(s/60),h=Math.floor(m/60);return h>0?h+'h '+m%60+'m':m+'m '+s%60+'s'}
299
+ function fAgo(ts){if(!ts)return'never';var d=Date.now()-ts;if(d<0)d=0;if(d<1000)return'just now';if(d<60000)return Math.floor(d/1000)+'s ago';if(d<3600000)return Math.floor(d/60000)+'m ago';return Math.floor(d/3600000)+'h ago'}
300
+
301
+ /** Copy text to clipboard with visual feedback */
302
+ function cpToClip(text,btn){
303
+ navigator.clipboard.writeText(text).then(function(){
304
+ var orig=btn.textContent;btn.textContent='\\u2713';setTimeout(function(){btn.textContent=orig},800);
305
+ }).catch(function(){});
306
+ }
307
+
308
+ // ─── Subsystem label helpers ──────────────────────
309
+ var subIcons={'diagnostic':'\\u2139','plugins':'\\u2699','agent/embedded':'\\u25B6','gateway':'\\u2302','gateway/ws':'\\u21C4','heartbeat':'\\u2764','canvas':'\\u25A1'};
310
+ function subIcon(s){for(var k in subIcons)if(s.indexOf(k)>=0)return subIcons[k];return'\\u2022'}
311
+
312
+ // ─── Flow tracker ──────────────────────
313
+ var flows={}, flowOrd=[];
314
+
315
+ function getFlow(sid,ev){
316
+ if(flows[sid])return flows[sid];
317
+ var c=document.createElement('div');c.className='flow active';
318
+ var hdr=document.createElement('div');hdr.className='flow-hdr';
319
+ var short=sid.length>20?sid.slice(0,8)+'..'+sid.slice(-4):sid;
320
+ hdr.innerHTML='<span class="flow-arr">\\u25BC</span><span class="flow-lbl" title="'+esc(sid)+'">Session '+esc(short)+'</span><span class="flow-badge active" data-r="st">active</span><span class="flow-info" data-r="info">'+fT(ev.ts||ev.tsMs)+'</span>';
321
+ var summary=document.createElement('div');summary.className='flow-summary';summary.setAttribute('data-r','summary');
322
+ var body=document.createElement('div');body.className='flow-body';
323
+ hdr.addEventListener('click',function(){
324
+ var shut=!body.classList.contains('shut');
325
+ body.classList.toggle('shut',shut);
326
+ summary.style.display=shut?'none':'flex';
327
+ hdr.querySelector('.flow-arr').classList.toggle('shut',shut);
328
+ });
329
+ c.appendChild(hdr);c.appendChild(summary);c.appendChild(body);
330
+ var f={el:c,body:body,hdr:hdr,summary:summary,st:'active',n:0,startTs:Date.now(),sid:sid,err:false,errCount:0,dur:0,tok:0,cost:0,tools:0,llms:0,toolNames:{},agentId:''};
331
+ flows[sid]=f;flowOrd.push(sid);
332
+ emptyMsg.style.display='none';
333
+ evList.insertBefore(c,evList.firstChild);
334
+ return f;
335
+ }
336
+
337
+ function updFlow(f,st){
338
+ f.st=st;f.el.className='flow '+st;
339
+ var sEl=f.hdr.querySelector('[data-r="st"]');
340
+ if(sEl){sEl.className='flow-badge '+st;sEl.textContent=st}
341
+ var iEl=f.hdr.querySelector('[data-r="info"]');
342
+ if(iEl){
343
+ var ps=[f.n+' events'];
344
+ if(f.dur>0)ps.push(fD(f.dur));
345
+ if(f.tok>0)ps.push(f.tok+' tok');
346
+ if(f.cost>0)ps.push('$'+f.cost.toFixed(4));
347
+ if(f.tools>0)ps.push(f.tools+' tools');
348
+ if(f.llms>0)ps.push(f.llms+' llm');
349
+ iEl.textContent=ps.join(' \\u00B7 ');
350
+ }
351
+ // Update summary bar
352
+ var sh='';
353
+ sh+='<span>Events: <span class="fs-v">'+f.n+'</span></span>';
354
+ if(f.dur>0)sh+='<span>Duration: <span class="fs-v">'+fD(f.dur)+'</span></span>';
355
+ if(f.tok>0)sh+='<span>Tokens: <span class="fs-v">'+f.tok+'</span></span>';
356
+ if(f.cost>0)sh+='<span>Cost: <span class="fs-v">$'+f.cost.toFixed(4)+'</span></span>';
357
+ var tn=Object.keys(f.toolNames);
358
+ if(tn.length)sh+='<span>Tools: <span class="fs-tools">'+tn.map(esc).join(', ')+'</span></span>';
359
+ if(f.errCount>0)sh+='<span>Errors: <span class="fs-err">'+f.errCount+'</span></span>';
360
+ if(f.agentId)sh+='<span>Agent: <span class="fs-agent">'+esc(f.agentId)+'</span></span>';
361
+ f.summary.innerHTML=sh;
362
+ }
363
+
364
+ // ─── Build expandable event detail panel ──────────────────────
365
+ function buildEvDetail(ev){
366
+ var d=document.createElement('div');d.className='ev-detail';
367
+ var m=ev.meta||{};
368
+ var h='<h4>Identity</h4>';
369
+ h+=dvRow('Type',ev.type||'?');
370
+ h+=dvRow('Timestamp',fISO(ev.ts));
371
+ if(ev.sessionKey)h+=dvRowCopy('Session Key',ev.sessionKey);
372
+ if(ev.agentId)h+=dvRowCopy('Agent ID',ev.agentId);
373
+ if(ev.channel)h+=dvRow('Channel',ev.channel);
374
+ if(ev.outcome)h+=dvRow('Outcome',ev.outcome);
375
+ if(ev.severity)h+=dvRow('Severity',ev.severity);
376
+
377
+ var hasMetrics=ev.durationMs!=null||ev.tokenCount!=null||ev.costUsd!=null||ev.queueDepth!=null||ev.ageMs!=null;
378
+ if(hasMetrics){
379
+ h+='<h4>Metrics</h4>';
380
+ if(ev.durationMs!=null)h+=dvRow('Duration',fD(ev.durationMs)+' ('+ev.durationMs+'ms)');
381
+ if(ev.tokenCount!=null)h+=dvRow('Tokens',String(ev.tokenCount));
382
+ if(ev.costUsd!=null)h+=dvRow('Cost','$'+ev.costUsd.toFixed(6));
383
+ if(ev.queueDepth!=null)h+=dvRow('Queue Depth',String(ev.queueDepth));
384
+ if(ev.ageMs!=null)h+=dvRow('Age',fD(ev.ageMs)+' ('+ev.ageMs+'ms)');
385
+ }
386
+
387
+ if(ev.error){
388
+ h+='<h4>Error</h4>';
389
+ h+='<div class="err-block">'+esc(ev.error)+'</div>';
390
+ }
391
+
392
+ var mKeys=Object.keys(m);
393
+ if(mKeys.length){
394
+ h+='<h4>Meta ('+mKeys.length+' fields)</h4>';
395
+ h+='<div class="meta-grid">';
396
+ for(var i=0;i<mKeys.length;i++){
397
+ var k=mKeys[i],v=m[k];
398
+ var vs=typeof v==='object'?JSON.stringify(v):String(v!=null?v:'');
399
+ h+='<span class="mk">'+esc(k)+'</span><span class="mv">'+esc(vs)+'</span>';
400
+ }
401
+ h+='</div>';
402
+ }
403
+
404
+ d.innerHTML=h;
405
+ // Wire up copy buttons after inserting
406
+ setTimeout(function(){
407
+ d.querySelectorAll('.cp-btn').forEach(function(btn){
408
+ btn.addEventListener('click',function(e){e.stopPropagation();cpToClip(btn.getAttribute('data-cp'),btn)});
409
+ });
410
+ },0);
411
+ return d;
412
+ }
413
+
414
+ function dvRow(label,val){return'<div class="dv"><span class="dk">'+esc(label)+'</span><span class="dd">'+esc(String(val))+'</span></div>'}
415
+ function dvRowCopy(label,val){return'<div class="dv"><span class="dk">'+esc(label)+'</span><span class="dd">'+esc(String(val))+'<button class="cp-btn" data-cp="'+esc(String(val))+'">copy</button></span></div>'}
416
+
417
+ // ─── Build an OpenAlerts event row ──────────────────────
418
+ function buildEvRow(ev,depth){
419
+ var wrap=document.createElement('div');
420
+ var div=document.createElement('div');
421
+ div.className='row expandable'+(depth===0?' standalone':depth>1?' deep':'');
422
+ var c=cat(ev.type),oc=ev.outcome||'',m=ev.meta||{};
423
+ var ft=ev.type||'?';
424
+ if(ft==='custom'&&m.openclawEventType==='session.state')ft='session.'+(m.sessionState||'state');
425
+ if(ft==='custom'&&m.openclawEventType==='message_sent')ft='msg.delivered';
426
+
427
+ var h='<div class="r-main">';
428
+ h+='<span class="r-time">'+fT(ev.ts)+'</span>';
429
+ if(ev.severity)h+='<span class="sev-dot '+(ev.severity||'')+'" title="'+esc(ev.severity)+'"></span>';
430
+ h+='<span class="r-type '+c+'">'+esc(ft)+'</span>';
431
+ if(oc)h+='<span class="r-oc '+oc+'">'+(oc==='success'?'\\u2713':oc==='error'?'\\u2717':'\\u25CB')+' '+oc+'</span>';
432
+ h+='<span class="r-pills">';
433
+ if(m.toolName)h+='<span class="p t">'+esc(String(m.toolName))+'</span>';
434
+ if(ev.durationMs!=null)h+='<span class="p d">'+fD(ev.durationMs)+'</span>';
435
+ if(ev.tokenCount!=null)h+='<span class="p tk">'+ev.tokenCount+' tok</span>';
436
+ if(ev.costUsd!=null)h+='<span class="p cost">$'+ev.costUsd.toFixed(4)+'</span>';
437
+ if(ev.agentId)h+='<span class="p agent">'+esc(ev.agentId.length>12?ev.agentId.slice(0,8)+'..':ev.agentId)+'</span>';
438
+ if(ev.queueDepth!=null)h+='<span class="p q">q='+ev.queueDepth+'</span>';
439
+ if(m.model)h+='<span class="p m">'+esc(String(m.model))+'</span>';
440
+ if(ev.channel)h+='<span class="p ch">'+esc(ev.channel)+'</span>';
441
+ if(m.messageCount!=null)h+='<span class="p">'+m.messageCount+' msgs</span>';
442
+ if(m.source&&String(m.source)!=='simulate')h+='<span class="p s">'+esc(String(m.source))+'</span>';
443
+ h+='</span></div>';
444
+
445
+ var ds=[];
446
+ if(ev.error)ds.push('<span class="err">'+esc(ev.error.length>120?ev.error.slice(0,120)+'...':ev.error)+'</span>');
447
+ if(ev.ageMs!=null)ds.push('stuck '+fD(ev.ageMs));
448
+ if(m.sessionState)ds.push('<span class="sc">'+(m.previousState||'?')+' \\u2192 '+m.sessionState+'</span>');
449
+ if(m.provider)ds.push('<span class="dim">provider: '+esc(String(m.provider))+'</span>');
450
+ if(m.to)ds.push('<span class="dim">to: '+esc(String(m.to))+'</span>');
451
+ if(ev.sessionKey&&depth===0)ds.push('<span class="dim">session: '+esc(ev.sessionKey.slice(0,12))+'</span>');
452
+ if(ds.length)h+='<div class="r-det">'+ds.join(' \\u00B7 ')+'</div>';
453
+
454
+ div.innerHTML=h;
455
+ wrap.appendChild(div);
456
+
457
+ // Click to expand/collapse detail panel
458
+ var detail=buildEvDetail(ev);
459
+ wrap.appendChild(detail);
460
+ div.addEventListener('click',function(e){
461
+ if(e.target.classList.contains('cp-btn'))return;
462
+ detail.classList.toggle('open');
463
+ });
464
+
465
+ return wrap;
466
+ }
467
+
468
+ // ─── Build an OpenClaw log row (Activity tab) ──────────────────────
469
+ function buildLogRow(entry,depth){
470
+ var div=document.createElement('div');
471
+ div.className='row log'+(depth===0?' standalone':depth>1?' deep':'');
472
+ var h='<div class="r-main">';
473
+ h+='<span class="r-time">'+fT(entry.tsMs||entry.ts)+'</span>';
474
+ h+='<span class="r-lvl '+esc(entry.level)+'">'+esc(entry.level)+'</span>';
475
+ h+='<span class="r-sub" title="'+esc(entry.subsystem)+'">'+subIcon(entry.subsystem)+' '+esc(entry.subsystem)+'</span>';
476
+ h+='<span class="r-msg">'+esc(entry.message)+'</span>';
477
+ h+='</div>';
478
+
479
+ // Show ALL parsed key=value pairs
480
+ var kvs=entry.extra||{};
481
+ var kvKeys=Object.keys(kvs);
482
+ if(kvKeys.length){
483
+ var kvParts=[];
484
+ for(var i=0;i<kvKeys.length;i++){
485
+ var k=kvKeys[i],v=kvs[k];
486
+ if(k==='sessionId'||k==='runId')kvParts.push('<span>'+esc(k)+'='+esc(v.length>16?v.slice(0,12)+'..':v)+'</span>');
487
+ else if(k==='durationMs')kvParts.push('<span>duration='+fD(parseInt(v))+'</span>');
488
+ else kvParts.push('<span>'+esc(k)+'='+esc(v)+'</span>');
489
+ }
490
+ if(kvParts.length)h+='<div class="r-kvs">'+kvParts.join('')+'</div>';
491
+ }
492
+
493
+ div.innerHTML=h;return div;
494
+ }
495
+
496
+ // ─── Ingest OpenAlerts event ──────────────────────
497
+ function addEvent(ev){
498
+ total++;evCnt.textContent=total;emptyMsg.style.display='none';
499
+ if(paused)return;
500
+ var sid=ev.sessionKey||ev.agentId;
501
+ if(sid&&(flows[sid]||ev.type==='agent.start'||ev.type==='session.start'||ev.type==='custom')){
502
+ var f=getFlow(sid,ev);f.n++;
503
+ if(ev.tokenCount)f.tok+=ev.tokenCount;
504
+ if(ev.costUsd)f.cost+=ev.costUsd;
505
+ if(ev.agentId&&!f.agentId)f.agentId=ev.agentId;
506
+ if(ev.type==='tool.call'||ev.type==='tool.error'){f.tools++;if(ev.meta&&ev.meta.toolName)f.toolNames[String(ev.meta.toolName)]=true}
507
+ if(ev.type==='llm.call')f.llms++;
508
+ if(ev.outcome==='error'){f.err=true;f.errCount++}
509
+ var depth=1;
510
+ if(ev.type==='tool.call'||ev.type==='tool.error'||ev.type==='llm.call'||ev.type==='llm.token_usage')depth=2;
511
+ f.body.appendChild(buildEvRow(ev,depth));
512
+ if(evList.firstChild!==f.el)evList.insertBefore(f.el,evList.firstChild);
513
+ if(ev.type==='agent.end'||ev.type==='session.end'){if(ev.durationMs)f.dur=ev.durationMs;updFlow(f,f.err?'error':'done')}
514
+ else if(ev.type==='agent.error'){f.err=true;f.errCount++;if(ev.durationMs)f.dur=ev.durationMs;updFlow(f,'error')}
515
+ else updFlow(f,f.st);
516
+ f.el.scrollIntoView({block:'nearest',behavior:'smooth'});
517
+ return;
518
+ }
519
+ var row=buildEvRow(ev,0);
520
+ if(evList.firstChild&&evList.firstChild!==emptyMsg)evList.insertBefore(row,evList.firstChild);
521
+ else evList.appendChild(row);
522
+ trimStandalone();
523
+ }
524
+
525
+ // ─── Ingest OpenClaw log entry (from log tailer) ──────────────────────
526
+ function addLogEntry(entry){
527
+ total++;evCnt.textContent=total;emptyMsg.style.display='none';
528
+ if(paused)return;
529
+ // Also stream to logs tab if active and auto-refresh is on
530
+ if($('tab-logs').classList.contains('active')&&$('lA').checked){
531
+ appendLogToTab(entry);
532
+ }
533
+ var sid=entry.sessionId;
534
+ if(sid&&flows[sid]){
535
+ var f=flows[sid];f.n++;
536
+ f.body.appendChild(buildLogRow(entry,1));
537
+ if(evList.firstChild!==f.el)evList.insertBefore(f.el,evList.firstChild);
538
+ updFlow(f,f.st);
539
+ f.el.scrollIntoView({block:'nearest',behavior:'smooth'});
540
+ return;
541
+ }
542
+ if(sid){
543
+ var f=getFlow(sid,entry);f.n++;
544
+ f.body.appendChild(buildLogRow(entry,1));
545
+ updFlow(f,f.st);
546
+ f.el.scrollIntoView({block:'nearest',behavior:'smooth'});
547
+ return;
548
+ }
549
+ var row=buildLogRow(entry,0);
550
+ if(evList.firstChild&&evList.firstChild!==emptyMsg)evList.insertBefore(row,evList.firstChild);
551
+ else evList.appendChild(row);
552
+ trimStandalone();
553
+ }
554
+
555
+ function trimStandalone(){
556
+ var all=evList.querySelectorAll('.row.standalone');
557
+ while(all.length>MAX_STANDALONE){all[all.length-1].remove();all=evList.querySelectorAll('.row.standalone')}
558
+ }
559
+
560
+ // ─── Stale flow cleanup ──────────────────────
561
+ setInterval(function(){
562
+ var now=Date.now();
563
+ for(var k in flows){
564
+ var f=flows[k];
565
+ if(f.st==='active'&&now-f.startTs>STALE_MS)updFlow(f,'done');
566
+ if(f.st!=='active'&&now-f.startTs>600000){f.el.remove();delete flows[k];var i=flowOrd.indexOf(k);if(i>=0)flowOrd.splice(i,1)}
567
+ }
568
+ while(flowOrd.length>MAX_FLOWS){var old=flowOrd.shift();if(flows[old]){flows[old].el.remove();delete flows[old]}}
569
+ },30000);
570
+
571
+ // ─── Alerts ──────────────────────
572
+ function addAlert(a){
573
+ alEmpty.style.display='none';
574
+ var d=document.createElement('div');d.className='al';
575
+ d.innerHTML='<div class="al-sev '+(a.severity||'error')+'">['+((a.severity||'ERROR').toUpperCase())+'] '+esc(a.ruleId||'')+'</div><div class="al-title">'+esc(a.title||'')+'</div><div class="al-detail">'+esc(a.detail||'')+'</div><div class="al-time">'+fT(a.ts)+'</div>';
576
+ alList.insertBefore(d,alList.firstChild);
577
+ while(alList.children.length>MAX_ALERTS+1)alList.removeChild(alList.lastChild);
578
+ }
579
+
580
+ evList.addEventListener('mouseenter',function(){paused=true});
581
+ evList.addEventListener('mouseleave',function(){paused=false});
582
+
583
+ // ─── Tabs ──────────────────────
584
+ document.querySelectorAll('.tab').forEach(function(t){
585
+ t.addEventListener('click',function(){
586
+ document.querySelectorAll('.tab').forEach(function(x){x.classList.remove('active')});
587
+ document.querySelectorAll('.tab-content').forEach(function(x){x.classList.remove('active')});
588
+ t.classList.add('active');
589
+ var tgt=$('tab-'+t.dataset.tab);if(tgt)tgt.classList.add('active');
590
+ if(t.dataset.tab==='logs')refreshLogs();
591
+ if(t.dataset.tab==='health')refreshHealth();
592
+ if(t.dataset.tab==='debug')refreshDebug();
593
+ });
594
+ });
595
+
596
+ // ─── SSE (OpenAlerts events + OpenClaw log tailing) ──────────────────────
597
+ function connectSSE(){
598
+ if(evSrc)evSrc.close();
599
+ evSrc=new EventSource('/openalerts/events');
600
+ evSrc.addEventListener('openalerts',function(e){try{addEvent(JSON.parse(e.data))}catch(_){}});
601
+ evSrc.addEventListener('oclog',function(e){try{addLogEntry(JSON.parse(e.data))}catch(_){}});
602
+ evSrc.onopen=function(){$('sDot').className='dot live';$('sConn').textContent='live'};
603
+ evSrc.onerror=function(){$('sDot').className='dot dead';$('sConn').textContent='reconnecting...'};
604
+ }
605
+
606
+ // ─── State polling ──────────────────────
607
+ var prevAl={};
608
+ function pollState(){
609
+ fetch('/openalerts/state').then(function(r){return r.json()}).then(function(s){
610
+ if(s.stats){
611
+ $('sMsgs').textContent=s.stats.messagesProcessed||0;
612
+ $('sErr').textContent=(s.stats.messageErrors||0)+(s.stats.webhookErrors||0)+(s.stats.toolErrors||0);
613
+ $('sTools').textContent=s.stats.toolCalls||0;
614
+ $('sAgents').textContent=s.stats.agentStarts||0;
615
+ }
616
+ if(s.uptimeMs!=null)$('sUp').textContent=fU(s.uptimeMs);
617
+ if(s.recentAlerts){
618
+ var nids={};
619
+ for(var i=0;i<s.recentAlerts.length;i++){var a=s.recentAlerts[i];nids[a.id]=true;if(!prevAl[a.id])addAlert(a)}
620
+ prevAl=nids;
621
+ }
622
+ if(s.rules){
623
+ var sec=$('rulesEl'),rh='<h3>Rules</h3>';
624
+ for(var j=0;j<s.rules.length;j++){var r=s.rules[j];var rf=r.status==='fired';rh+='<div class="rl"><span class="rl-d '+(rf?'fired':'ok')+'"></span><span>'+esc(r.id)+'</span><span class="rl-s">'+(rf?'FIRING':'OK')+'</span></div>'}
625
+ sec.innerHTML=rh;
626
+ }
627
+ window._ss=s;
628
+ }).catch(function(){});
629
+ }
630
+
631
+ // ─── Logs tab ──────────────────────
632
+ var logSubsPopulated=false;
633
+
634
+ /** Build an expandable log row for the Logs tab */
635
+ function buildLogTabRow(e){
636
+ var container=document.createElement('div');
637
+ var row=document.createElement('div');row.className='log-e';
638
+ var rawLine=fISO(e.tsMs||e.ts)+' ['+e.level+'] '+e.subsystem+': '+e.message;
639
+ row.innerHTML='<span class="log-ts">'+fT(e.tsMs||e.ts)+'</span><span class="log-lv '+esc(e.level)+'">'+esc(e.level)+'</span><span class="log-su">'+esc(e.subsystem)+'</span><span class="log-mg">'+esc(e.message)+'</span><button class="log-copy" data-raw="'+esc(rawLine)+'" title="Copy log line">copy</button>';
640
+ container.appendChild(row);
641
+
642
+ // Build expandable detail
643
+ var detail=document.createElement('div');detail.className='log-detail';
644
+ var dh='<div class="ld-grid">';
645
+ dh+='<span class="lk">Time</span><span class="lv">'+fISO(e.tsMs||e.ts)+'</span>';
646
+ dh+='<span class="lk">Level</span><span class="lv">'+esc(e.level)+'</span>';
647
+ dh+='<span class="lk">Subsystem</span><span class="lv">'+esc(e.subsystem)+'</span>';
648
+ // ALL key-value pairs from extra
649
+ var kvs=e.extra||{};
650
+ var kvKeys=Object.keys(kvs);
651
+ for(var i=0;i<kvKeys.length;i++){
652
+ dh+='<span class="lk">'+esc(kvKeys[i])+'</span><span class="lv">'+esc(kvs[kvKeys[i]])+'</span>';
653
+ }
654
+ dh+='</div>';
655
+ // File path + method
656
+ if(e.filePath||e.method){
657
+ dh+='<div class="ld-file">';
658
+ if(e.filePath)dh+='\\u{1F4C4} '+esc(e.filePath);
659
+ if(e.method)dh+=' ('+esc(e.method)+')';
660
+ if(e.hostname)dh+=' @ '+esc(e.hostname);
661
+ dh+='</div>';
662
+ }
663
+ detail.innerHTML=dh;
664
+ container.appendChild(detail);
665
+
666
+ row.addEventListener('click',function(){detail.classList.toggle('open')});
667
+ return container;
668
+ }
669
+
670
+ /** Check if log level is enabled */
671
+ function isLevelEnabled(level){
672
+ var cb=$('lv-'+level);
673
+ return cb?cb.checked:true;
674
+ }
675
+
676
+ /** Append a single SSE-streamed log entry to the Logs tab */
677
+ function appendLogToTab(entry){
678
+ var list=$('logList');
679
+ var fSub=$('lF').value, fSrch=$('lS').value.toLowerCase();
680
+ if(fSub&&entry.subsystem.indexOf(fSub)<0)return;
681
+ if(!isLevelEnabled(entry.level))return;
682
+ if(fSrch&&entry.message.toLowerCase().indexOf(fSrch)<0&&entry.subsystem.toLowerCase().indexOf(fSrch)<0)return;
683
+ // Remove empty msg if present
684
+ var em=list.querySelector('.empty-msg');if(em)em.remove();
685
+ var row=buildLogTabRow(entry);
686
+ list.appendChild(row);
687
+ if($('lA').checked)list.scrollTop=list.scrollHeight;
688
+ }
689
+
690
+ function refreshLogs(){
691
+ fetch('/openalerts/logs?limit=500').then(function(r){return r.json()}).then(function(data){
692
+ var list=$('logList');
693
+ var entries=data.entries||[];
694
+ var fSub=$('lF').value, fSrch=$('lS').value.toLowerCase();
695
+ var truncated=data.truncated||false;
696
+ $('lT').classList.toggle('show',truncated);
697
+
698
+ // Populate subsystem dropdown dynamically
699
+ if(data.subsystems&&data.subsystems.length){
700
+ var sel=$('lF');
701
+ var cur=sel.value;
702
+ var existing={};
703
+ for(var oi=0;oi<sel.options.length;oi++)existing[sel.options[oi].value]=true;
704
+ var changed=false;
705
+ for(var si=0;si<data.subsystems.length;si++){
706
+ if(!existing[data.subsystems[si]]){
707
+ var opt=document.createElement('option');opt.value=data.subsystems[si];opt.textContent=data.subsystems[si];
708
+ sel.appendChild(opt);changed=true;
709
+ }
710
+ }
711
+ if(changed)sel.value=cur;
712
+ logSubsPopulated=true;
713
+ }
714
+
715
+ list.innerHTML='';
716
+ if(!entries.length){list.innerHTML='<div class="empty-msg">No logs found.</div>';return}
717
+
718
+ for(var i=0;i<entries.length;i++){
719
+ var e=entries[i];
720
+ if(fSub&&e.subsystem.indexOf(fSub)<0)continue;
721
+ if(!isLevelEnabled(e.level))continue;
722
+ if(fSrch&&e.message.toLowerCase().indexOf(fSrch)<0&&e.subsystem.toLowerCase().indexOf(fSrch)<0)continue;
723
+ list.appendChild(buildLogTabRow(e));
724
+ }
725
+ if($('lA').checked)list.scrollTop=list.scrollHeight;
726
+ }).catch(function(){$('logList').innerHTML='<div class="empty-msg">Failed to load.</div>'});
727
+ }
728
+
729
+ // Export filtered logs
730
+ function exportLogs(){
731
+ var list=$('logList');
732
+ var rows=list.querySelectorAll('.log-e');
733
+ if(!rows.length){alert('No logs to export');return}
734
+ var lines=[];
735
+ rows.forEach(function(row){
736
+ var btn=row.querySelector('.log-copy');
737
+ if(btn)lines.push(btn.getAttribute('data-raw'));
738
+ });
739
+ var blob=new Blob([lines.join('\n')],{type:'text/plain'});
740
+ var url=URL.createObjectURL(blob);
741
+ var a=document.createElement('a');
742
+ a.href=url;a.download='openalerts-logs-'+Date.now()+'.txt';
743
+ a.click();
744
+ URL.revokeObjectURL(url);
745
+ }
746
+
747
+ // Wire up event listeners
748
+ $('lR').addEventListener('click',refreshLogs);
749
+ $('lE').addEventListener('click',exportLogs);
750
+ $('lF').addEventListener('change',refreshLogs);
751
+ var sDb;$('lS').addEventListener('input',function(){clearTimeout(sDb);sDb=setTimeout(refreshLogs,300)});
752
+
753
+ // Level filter checkboxes
754
+ ['TRACE','DEBUG','INFO','WARN','ERROR','FATAL'].forEach(function(lv){
755
+ var cb=$('lv-'+lv);
756
+ if(cb)cb.addEventListener('change',refreshLogs);
757
+ });
758
+
759
+ // Keyboard shortcuts
760
+ document.addEventListener('keydown',function(e){
761
+ if(e.ctrlKey&&e.key==='f'&&$('tab-logs').classList.contains('active')){
762
+ e.preventDefault();$('lS').focus();
763
+ }
764
+ if(e.ctrlKey&&e.key==='r'&&$('tab-logs').classList.contains('active')){
765
+ e.preventDefault();refreshLogs();
766
+ }
767
+ });
768
+
769
+ // Copy button handler (event delegation)
770
+ document.addEventListener('click',function(e){
771
+ if(e.target.classList.contains('log-copy')){
772
+ e.stopPropagation();
773
+ var raw=e.target.getAttribute('data-raw');
774
+ if(raw)cpToClip(raw,e.target);
775
+ }
776
+ });
777
+
778
+ // Fallback polling every 3s (SSE handles real-time now)
779
+ setInterval(function(){if($('tab-logs').classList.contains('active')&&$('lA').checked&&!evSrc)refreshLogs()},3000);
780
+
781
+ // ─── Health tab ──────────────────────
782
+ function refreshHealth(){
783
+ var s=window._ss;if(!s){pollState();setTimeout(refreshHealth,1000);return}
784
+ var hEl=$('hC'),st=s.stats||{},up=s.uptimeMs||0;
785
+ var html='';
786
+ html+='<div class="h-sec"><h3>System</h3><div class="h-grid">';
787
+ html+=hCard('Uptime',fU(up),'ok');
788
+ html+=hCard('Started At',s.startedAt?new Date(s.startedAt).toLocaleString():'--','');
789
+ html+=hCard('SSE Listeners',s.busListeners||0,'ok');
790
+ var te=(st.messageErrors||0)+(st.webhookErrors||0)+(st.toolErrors||0)+(st.agentErrors||0);
791
+ html+=hCard('Total Errors',te,te>0?'bad':'ok');
792
+ html+=hCard('Platform',s.platformConnected?'Connected':'Off',s.platformConnected?'ok':'');
793
+ // New cards
794
+ var stuckN=s.stuckSessions!=null?s.stuckSessions:(st.stuckSessions||0);
795
+ html+=hCard('Stuck Sessions',stuckN,stuckN>0?'bad':'ok');
796
+ var hbAgo=s.lastHeartbeatTs?fAgo(s.lastHeartbeatTs):'never';
797
+ var hbOk=s.lastHeartbeatTs&&(Date.now()-s.lastHeartbeatTs)<90000;
798
+ html+=hCard('Last Heartbeat',hbAgo,hbOk?'ok':'bad');
799
+ if(s.hourlyAlerts){
800
+ html+=hCard('Hourly Alert Cap',s.hourlyAlerts.count+'/5'+(s.hourlyAlerts.resetAt?' (reset '+fAgo(s.hourlyAlerts.resetAt)+')':''),'');
801
+ }
802
+ if(s.lastResetTs){
803
+ html+=hCard('Stats Reset',new Date(s.lastResetTs).toLocaleString(),'');
804
+ }
805
+ html+='</div></div>';
806
+
807
+ html+='<div class="h-sec"><h3>Stats</h3><table class="h-tbl">';
808
+ html+=hTr('Messages Processed',st.messagesProcessed||0)+hTr('Message Errors',st.messageErrors||0)+hTr('Webhook Errors',st.webhookErrors||0);
809
+ html+=hTr('Tool Calls',st.toolCalls||0)+hTr('Tool Errors',st.toolErrors||0)+hTr('Agent Starts',st.agentStarts||0)+hTr('Agent Errors',st.agentErrors||0)+hTr('Sessions',st.sessionsStarted||0);
810
+ html+=hTr('Stuck Sessions',stuckN);
811
+ html+='</table></div>';
812
+
813
+ html+='<div class="h-sec"><h3>Rules</h3><table class="h-tbl"><tr><th>Rule</th><th>Status</th><th>Last Fired</th></tr>';
814
+ var cds=s.cooldowns||{};
815
+ if(s.rules)for(var i=0;i<s.rules.length;i++){
816
+ var r=s.rules[i];
817
+ var cdTs=cds[r.id];
818
+ var lastFired=cdTs?fAgo(cdTs):'--';
819
+ html+='<tr><td>'+esc(r.id)+'</td><td>'+(r.status==='fired'?'<span style="color:#f85149;font-weight:700">FIRING</span>':'<span style="color:#3fb950">OK</span>')+'</td><td>'+lastFired+'</td></tr>';
820
+ }
821
+ html+='</table></div>';
822
+
823
+ if(s.recentAlerts&&s.recentAlerts.length){
824
+ html+='<div class="h-sec"><h3>Recent Alerts ('+s.recentAlerts.length+')</h3><table class="h-tbl">';
825
+ for(var j=0;j<s.recentAlerts.length;j++){var a=s.recentAlerts[j];html+='<tr><td style="color:'+(a.severity==='critical'?'#ff7b72':a.severity==='warn'?'#d29922':'#f85149')+'">['+((a.severity||'?').toUpperCase())+'] '+esc(a.ruleId||'')+'</td><td>'+esc(a.title||'')+' \\u2014 '+esc(a.detail||'')+' ('+fT(a.ts)+')</td></tr>'}
826
+ html+='</table></div>';
827
+ }
828
+
829
+ // Circuit Breakers (when backend exposes this)
830
+ if(s.circuitBreakers&&s.circuitBreakers.length){
831
+ html+='<div class="h-sec"><h3>Circuit Breakers</h3><table class="h-tbl"><tr><th>Category</th><th>Name</th><th>State</th><th>Failures</th><th>Last Change</th></tr>';
832
+ for(var k=0;k<s.circuitBreakers.length;k++){
833
+ var cb=s.circuitBreakers[k];
834
+ var stColor=cb.state==='CLOSED'?'#3fb950':cb.state==='OPEN'?'#f85149':'#d29922';
835
+ html+='<tr><td>'+esc(cb.category)+'</td><td>'+esc(cb.name)+'</td><td style="color:'+stColor+';font-weight:700">'+cb.state+'</td><td>'+cb.failures+'</td><td>'+fAgo(cb.lastStateChange)+'</td></tr>';
836
+ }
837
+ html+='</table></div>';
838
+ }
839
+
840
+ // Task Timeouts (when backend exposes this)
841
+ if(s.taskTimeouts&&s.taskTimeouts.length){
842
+ html+='<div class="h-sec"><h3>Running Tasks</h3><table class="h-tbl"><tr><th>Type</th><th>ID</th><th>Duration</th><th>Timeout</th><th>Status</th></tr>';
843
+ for(var l=0;l<s.taskTimeouts.length;l++){
844
+ var tt=s.taskTimeouts[l];
845
+ var dur=Date.now()-tt.startedAt;
846
+ var pct=Math.floor((dur/tt.timeoutMs)*100);
847
+ var warn=pct>80;
848
+ html+='<tr><td>'+esc(tt.type)+'</td><td>'+esc(tt.id.slice(0,12))+'</td><td>'+fD(dur)+'</td><td>'+fD(tt.timeoutMs)+'</td><td style="color:'+(warn?'#d29922':'#3fb950')+'">'+pct+'%</td></tr>';
849
+ }
850
+ html+='</table></div>';
851
+ }
852
+
853
+ hEl.innerHTML=html;
854
+ }
855
+ function hCard(l,v,c){return'<div class="h-card"><div class="lb">'+esc(l)+'</div><div class="vl '+(c||'')+'">'+esc(String(v))+'</div></div>'}
856
+ function hTr(l,v){return'<tr><td>'+esc(l)+'</td><td><b>'+esc(String(v))+'</b></td></tr>'}
857
+
858
+ // ─── Debug tab ──────────────────────
859
+ function refreshDebug(){
860
+ var s=window._ss;if(!s){pollState();setTimeout(refreshDebug,1000);return}
861
+
862
+ // State snapshot
863
+ var stHtml='';
864
+ stHtml+=hCard('Uptime',fU(s.uptimeMs||0),'ok');
865
+ stHtml+=hCard('Started At',s.startedAt?new Date(s.startedAt).toLocaleString():'--','');
866
+ stHtml+=hCard('SSE Listeners',s.busListeners||0,'ok');
867
+ stHtml+=hCard('Platform',s.platformConnected?'Connected':'Off',s.platformConnected?'ok':'');
868
+ stHtml+=hCard('Event Count',total,'ok');
869
+ $('dbState').innerHTML=stHtml;
870
+
871
+ // Recent events (last 20 from flows)
872
+ var evHtml='<div style="font-family:monospace;font-size:10px;line-height:1.6">';
873
+ var recentEvs=[];
874
+ for(var k in flows){
875
+ var f=flows[k];
876
+ if(f.body&&f.body.children){
877
+ for(var i=0;i<f.body.children.length;i++){
878
+ var child=f.body.children[i];
879
+ if(child.querySelector&&child.querySelector('.r-time')){
880
+ var timeEl=child.querySelector('.r-time');
881
+ var typeEl=child.querySelector('.r-type');
882
+ if(timeEl&&typeEl){
883
+ recentEvs.push({time:timeEl.textContent,type:typeEl.textContent,sid:k});
884
+ }
885
+ }
886
+ }
887
+ }
888
+ }
889
+ recentEvs=recentEvs.slice(-20);
890
+ if(recentEvs.length===0)evHtml+='<div style="color:#8b949e">No recent events</div>';
891
+ else for(var j=0;j<recentEvs.length;j++){
892
+ var ev=recentEvs[j];
893
+ evHtml+='<div><span style="color:#484f58">'+esc(ev.time)+'</span> <span style="color:#58a6ff">'+esc(ev.type)+'</span> <span style="color:#8b949e">'+esc(ev.sid.slice(0,12))+'</span></div>';
894
+ }
895
+ evHtml+='</div>';
896
+ $('dbEvents').innerHTML=evHtml;
897
+
898
+ // Rules status
899
+ var rulesHtml='<table class="h-tbl"><tr><th>Rule</th><th>Status</th><th>Last Fired</th></tr>';
900
+ var cds=s.cooldowns||{};
901
+ if(s.rules)for(var i=0;i<s.rules.length;i++){
902
+ var r=s.rules[i];
903
+ var cdTs=cds[r.id];
904
+ var lastFired=cdTs?fAgo(cdTs):'never';
905
+ rulesHtml+='<tr><td>'+esc(r.id)+'</td><td>'+(r.status==='fired'?'<span style="color:#f85149;font-weight:700">FIRING</span>':'<span style="color:#3fb950">OK</span>')+'</td><td>'+lastFired+'</td></tr>';
906
+ }
907
+ rulesHtml+='</table>';
908
+ $('dbRules').innerHTML=rulesHtml;
909
+
910
+ // Circuit breakers (placeholder - will be populated when backend exposes this)
911
+ $('dbCircuit').innerHTML='<div style="color:#8b949e;padding:8px">Circuit breaker state not yet exposed by backend</div>';
912
+
913
+ // Task timeouts (placeholder - will be populated when backend exposes this)
914
+ $('dbTasks').innerHTML='<div style="color:#8b949e;padding:8px">Task timeout state not yet exposed by backend</div>';
915
+ }
916
+ $('dbRefresh').addEventListener('click',refreshDebug);
917
+
918
+ // ─── Boot ──────────────────────
919
+ connectSSE();pollState();setInterval(pollState,4000);
920
+ })();
921
+ </script>
922
+ </body>
923
+ </html>`;
924
+ }