@steadwing/openalerts 0.2.1 → 0.2.3

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.
@@ -13,22 +13,22 @@ export function getDashboardHtml() {
13
13
  <title>OpenAlerts Monitor</title>
14
14
  <style>
15
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}
16
+ body{font-family:'SF Mono','Cascadia Code','Consolas',monospace;background:#0d1117;color:#c9d1d9;font-size:14px;overflow:hidden;height:100vh}
17
17
  .grid{display:grid;grid-template-rows:auto auto 1fr;height:100vh}
18
18
 
19
19
  /* ── Top bar ──────────────────── */
20
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}
21
+ .topbar h1{font-size:16px;font-weight:600;color:#f0f6fc;letter-spacing:0.5px}
22
22
  .dot{width:7px;height:7px;border-radius:50%;display:inline-block;margin-right:4px}
23
23
  .dot.live{background:#3fb950;animation:pulse 2s infinite}
24
24
  .dot.dead{background:#f85149}
25
25
  @keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}}
26
- .stat{color:#8b949e;font-size:11px}
26
+ .stat{color:#8b949e;font-size:12px}
27
27
  .stat b{color:#c9d1d9;font-weight:500}
28
28
 
29
29
  /* ── Tabs ──────────────────── */
30
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}
31
+ .tab{padding:7px 18px;font-size:13px;font-weight:600;color:#8b949e;cursor:pointer;border-bottom:2px solid transparent;transition:all 0.15s}
32
32
  .tab:hover{color:#c9d1d9;background:#1c2129}
33
33
  .tab.active{color:#58a6ff;border-bottom-color:#58a6ff}
34
34
  .tab-content{display:none;overflow:hidden;flex:1}
@@ -39,7 +39,7 @@ export function getDashboardHtml() {
39
39
  .activity-panels{display:grid;grid-template-columns:1fr 280px;gap:0;overflow:hidden;flex:1}
40
40
  @media(max-width:900px){.activity-panels{grid-template-columns:1fr}}
41
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}
42
+ .panel-header{background:#161b22;padding:6px 12px;font-size:12px;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
43
  .panel:first-child{border-right:1px solid #30363d}
44
44
  .scroll{flex:1;overflow-y:auto}
45
45
  .scroll::-webkit-scrollbar{width:5px}
@@ -65,16 +65,16 @@ export function getDashboardHtml() {
65
65
  .flow-body.shut{max-height:0!important;overflow:hidden}
66
66
 
67
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}
68
+ .row{padding:3px 10px 3px 24px;border-top:1px solid #0d1117;font-size:12px;line-height:1.5;animation:fi 0.15s ease}
69
69
  .row:hover{background:#0d1117}
70
70
  .row.standalone{padding-left:10px;border-bottom:1px solid #21262d;border-top:none}
71
71
  .row.deep{padding-left:38px}
72
72
 
73
73
  /* OpenAlerts event row */
74
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}
75
+ .r-time{color:#484f58;font-size:11px;min-width:55px;flex-shrink:0}
76
+ .r-icon{width:14px;text-align:center;flex-shrink:0;font-size:12px}
77
+ .r-type{font-weight:600;min-width:110px;font-size:11px;flex-shrink:0}
78
78
  .r-type.llm{color:#58a6ff} .r-type.tool{color:#bc8cff} .r-type.agent{color:#3fb950}
79
79
  .r-type.session{color:#d29922} .r-type.infra{color:#f85149} .r-type.custom{color:#8b949e}
80
80
  .r-type.watchdog{color:#6e7681}
@@ -83,10 +83,10 @@ export function getDashboardHtml() {
83
83
  .r-oc.error{background:#3d1a1a;color:#f85149}
84
84
  .r-oc.timeout{background:#3d2e1a;color:#d29922}
85
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}
86
+ .p{font-size:10px;background:#21262d;padding:0 5px;border-radius:3px;white-space:nowrap}
87
87
  .p.t{color:#bc8cff;background:#2a1f3d} .p.d{color:#d29922} .p.tk{color:#58a6ff}
88
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}
89
+ .r-det{padding:1px 0 1px 70px;color:#6e7681;font-size:11px}
90
90
  .r-det .err{color:#f85149} .r-det .dim{color:#484f58} .r-det .sc{color:#d29922}
91
91
 
92
92
  /* OpenClaw log row */
@@ -101,25 +101,25 @@ export function getDashboardHtml() {
101
101
  @keyframes fi{from{opacity:0;transform:translateY(-2px)}to{opacity:1;transform:none}}
102
102
 
103
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}
104
+ .al{padding:6px 10px;border-bottom:1px solid #21262d;font-size:12px;animation:fi 0.2s}
105
+ .al-sev{font-weight:700;text-transform:uppercase;font-size:10px;letter-spacing:0.4px}
106
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}
107
+ .al-title{color:#c9d1d9;margin-top:1px;font-size:12px}
108
+ .al-detail{color:#8b949e;margin-top:1px;font-size:11px}
109
+ .al-time{color:#484f58;font-size:11px;margin-top:1px}
110
110
 
111
111
  /* ── Rules ──────────────────── */
112
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}
113
+ .rules h3{font-size:12px;color:#8b949e;text-transform:uppercase;letter-spacing:0.7px;margin-bottom:4px}
114
+ .rl{display:flex;align-items:center;gap:5px;font-size:12px;padding:1px 0}
115
115
  .rl-d{width:5px;height:5px;border-radius:50%;flex-shrink:0}
116
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}
117
+ .rl-s{color:#8b949e;margin-left:auto;font-size:11px}
118
118
 
119
119
  /* ── Logs tab ──────────────────── */
120
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}
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:12px}
122
+ .log-bar select,.log-bar input{background:#0d1117;border:1px solid #30363d;color:#c9d1d9;font-family:inherit;font-size:11px;padding:3px 6px;border-radius:3px}
123
123
  .log-bar input[type="text"]{min-width:180px}
124
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
125
  .log-bar button:hover{background:#30363d;border-color:#484f58}
@@ -131,31 +131,31 @@ export function getDashboardHtml() {
131
131
  .log-filters label{font-size:10px}
132
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
133
  .log-truncate.show{display:block}
134
- .log-list{flex:1;overflow-y:auto;font-size:11px}
134
+ .log-list{flex:1;overflow-y:auto;font-size:12px}
135
135
  .log-e{padding:2px 12px;border-bottom:1px solid #161b22;display:flex;gap:6px;align-items:baseline;position:relative}
136
136
  .log-e:hover{background:#161b22}
137
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}
138
+ .log-ts{color:#484f58;font-size:11px;min-width:70px;flex-shrink:0}
139
+ .log-lv{font-size:10px;font-weight:700;min-width:42px;flex-shrink:0}
140
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}
141
+ .log-su{color:#bc8cff;font-size:11px;min-width:110px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
142
+ .log-mg{color:#c9d1d9;font-size:12px;word-break:break-all;flex:1}
143
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
144
  .log-copy:hover{color:#c9d1d9;border-color:#484f58}
145
145
 
146
146
  /* ── Health tab ──────────────────── */
147
147
  .health-t{flex:1;overflow-y:auto;padding:14px}
148
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}
149
+ .h-sec h3{font-size:13px;color:#8b949e;text-transform:uppercase;letter-spacing:0.7px;margin-bottom:6px;padding-bottom:3px;border-bottom:1px solid #21262d}
150
150
  .h-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:6px}
151
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}
152
+ .h-card .lb{color:#8b949e;font-size:11px;margin-bottom:2px}
153
+ .h-card .vl{color:#c9d1d9;font-size:15px;font-weight:600}
154
154
  .h-card .vl.ok{color:#3fb950} .h-card .vl.bad{color:#f85149}
155
155
  .h-tbl{width:100%;border-collapse:collapse}
156
- .h-tbl td{padding:3px 8px;font-size:11px;border-bottom:1px solid #161b22}
156
+ .h-tbl td{padding:4px 8px;font-size:13px;border-bottom:1px solid #161b22}
157
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}
158
+ .h-tbl th{padding:4px 8px;font-size:12px;color:#8b949e;text-align:left;border-bottom:1px solid #30363d;font-weight:600}
159
159
 
160
160
  /* ── Expandable event detail ──────────────────── */
161
161
  .row.expandable{cursor:pointer}
@@ -245,9 +245,10 @@ export function getDashboardHtml() {
245
245
  </div>
246
246
  <button id="lR" title="Refresh logs (Ctrl+R)">↻ Refresh</button>
247
247
  <button id="lE" title="Export filtered logs">⬇ Export</button>
248
+ <label title="Fetch all logs (not just recent) when searching or filtering"><input type="checkbox" id="lAll"> All logs</label>
248
249
  <label style="margin-left:auto" title="Auto-scroll to new logs"><input type="checkbox" id="lA" checked> Auto-follow</label>
249
250
  </div>
250
- <div class="log-truncate" id="lT">⚠ Logs truncated - showing most recent entries only</div>
251
+ <div class="log-truncate" id="lT">⚠ Logs truncated showing most recent entries only. Check "All logs" to search across the full log file.</div>
251
252
  <div class="log-list" id="logList"><div class="empty-msg">Loading...</div></div>
252
253
  </div>
253
254
  </div>
@@ -271,14 +272,6 @@ export function getDashboardHtml() {
271
272
  <h3>Alert Rules Status</h3>
272
273
  <div id="dbRules"></div>
273
274
  </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
275
  </div>
283
276
  </div>
284
277
  </div>
@@ -293,6 +286,30 @@ export function getDashboardHtml() {
293
286
  function esc(s){var d=document.createElement('div');d.textContent=s;return d.innerHTML}
294
287
  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
288
  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'})}
289
+
290
+ // ─── Human-readable event labels ──────────────────────
291
+ var EVT_LABELS={
292
+ 'llm.call':['\\u{1F916}','LLM Called'],
293
+ 'llm.error':['\\u26A0','LLM Failed'],
294
+ 'llm.token_usage':['\\u{1F4CA}','Token Usage'],
295
+ 'tool.call':['\\u{1F527}','Tool Executed'],
296
+ 'tool.error':['\\u{1F527}','Tool Failed'],
297
+ 'agent.start':['\\u25B6','Agent Started'],
298
+ 'agent.end':['\\u23F9','Agent Finished'],
299
+ 'agent.error':['\\u{1F6A8}','Agent Error'],
300
+ 'agent.stuck':['\\u23F3','Agent Stuck'],
301
+ 'session.start':['\\u{1F4AC}','Session Started'],
302
+ 'session.end':['\\u{1F3C1}','Session Ended'],
303
+ 'session.stuck':['\\u23F3','Session Stuck'],
304
+ 'infra.error':['\\u{1F6A8}','Infra Error'],
305
+ 'infra.heartbeat':['\\u2764','Heartbeat'],
306
+ 'infra.queue_depth':['\\u{1F4E6}','Queue Depth'],
307
+ 'watchdog.tick':['\\u{1F440}','Health Check'],
308
+ 'msg.delivered':['\\u2709','Message Sent'],
309
+ 'msg.in':['\\u{1F4E5}','Message Received'],
310
+ 'msg.out':['\\u{1F4E4}','Message Sending']
311
+ };
312
+ function friendlyLabel(ft){return EVT_LABELS[ft]||['\\u2022',ft]}
296
313
  function fISO(ts){if(!ts)return'';return new Date(typeof ts==='number'?ts:Date.parse(ts)).toISOString()}
297
314
  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
315
  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'}
@@ -317,7 +334,7 @@ export function getDashboardHtml() {
317
334
  var c=document.createElement('div');c.className='flow active';
318
335
  var hdr=document.createElement('div');hdr.className='flow-hdr';
319
336
  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>';
337
+ 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">Running</span><span class="flow-info" data-r="info">'+fT(ev.ts||ev.tsMs)+'</span>';
321
338
  var summary=document.createElement('div');summary.className='flow-summary';summary.setAttribute('data-r','summary');
322
339
  var body=document.createElement('div');body.className='flow-body';
323
340
  hdr.addEventListener('click',function(){
@@ -337,26 +354,27 @@ export function getDashboardHtml() {
337
354
  function updFlow(f,st){
338
355
  f.st=st;f.el.className='flow '+st;
339
356
  var sEl=f.hdr.querySelector('[data-r="st"]');
340
- if(sEl){sEl.className='flow-badge '+st;sEl.textContent=st}
357
+ var stLabels={active:'Running',done:'Completed',error:'Failed'};
358
+ if(sEl){sEl.className='flow-badge '+st;sEl.textContent=stLabels[st]||st}
341
359
  var iEl=f.hdr.querySelector('[data-r="info"]');
342
360
  if(iEl){
343
361
  var ps=[f.n+' events'];
344
362
  if(f.dur>0)ps.push(fD(f.dur));
345
- if(f.tok>0)ps.push(f.tok+' tok');
363
+ if(f.tok>0)ps.push(f.tok+' tokens');
346
364
  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');
365
+ if(f.tools>0)ps.push(f.tools+(f.tools===1?' tool':' tools'));
366
+ if(f.llms>0)ps.push(f.llms+(f.llms===1?' LLM call':' LLM calls'));
349
367
  iEl.textContent=ps.join(' \\u00B7 ');
350
368
  }
351
369
  // Update summary bar
352
370
  var sh='';
353
371
  sh+='<span>Events: <span class="fs-v">'+f.n+'</span></span>';
354
372
  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>';
373
+ if(f.tok>0)sh+='<span>Tokens used: <span class="fs-v">'+f.tok+'</span></span>';
356
374
  if(f.cost>0)sh+='<span>Cost: <span class="fs-v">$'+f.cost.toFixed(4)+'</span></span>';
357
375
  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>';
376
+ if(tn.length)sh+='<span>Tools used: <span class="fs-tools">'+tn.map(esc).join(', ')+'</span></span>';
377
+ if(f.errCount>0)sh+='<span>\\u26A0 Errors: <span class="fs-err">'+f.errCount+'</span></span>';
360
378
  if(f.agentId)sh+='<span>Agent: <span class="fs-agent">'+esc(f.agentId)+'</span></span>';
361
379
  f.summary.innerHTML=sh;
362
380
  }
@@ -423,29 +441,35 @@ export function getDashboardHtml() {
423
441
  var ft=ev.type||'?';
424
442
  if(ft==='custom'&&m.openclawEventType==='session.state')ft='session.'+(m.sessionState||'state');
425
443
  if(ft==='custom'&&m.openclawEventType==='message_sent')ft='msg.delivered';
444
+ if(ft==='custom'&&m.openclawHook==='message_received')ft='msg.in';
445
+ if(ft==='custom'&&m.openclawHook==='message_sending')ft='msg.out';
446
+
447
+ var fl=friendlyLabel(ft);
426
448
 
427
449
  var h='<div class="r-main">';
428
450
  h+='<span class="r-time">'+fT(ev.ts)+'</span>';
451
+ h+='<span class="r-icon">'+fl[0]+'</span>';
429
452
  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>';
453
+ h+='<span class="r-type '+c+'">'+esc(fl[1])+'</span>';
454
+ if(oc)h+='<span class="r-oc '+oc+'">'+(oc==='success'?'\\u2713 OK':oc==='error'?'\\u2717 Failed':oc==='timeout'?'\\u23F1 Timeout':'\\u25CB '+oc)+'</span>';
432
455
  h+='<span class="r-pills">';
433
456
  if(m.toolName)h+='<span class="p t">'+esc(String(m.toolName))+'</span>';
434
457
  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>';
458
+ if(ev.tokenCount!=null)h+='<span class="p tk">'+ev.tokenCount+' tokens</span>';
436
459
  if(ev.costUsd!=null)h+='<span class="p cost">$'+ev.costUsd.toFixed(4)+'</span>';
437
460
  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>';
461
+ if(ev.queueDepth!=null)h+='<span class="p q">Queue: '+ev.queueDepth+'</span>';
439
462
  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>';
463
+ if(ev.channel)h+='<span class="p ch">via '+esc(ev.channel)+'</span>';
441
464
  if(m.messageCount!=null)h+='<span class="p">'+m.messageCount+' msgs</span>';
465
+ if(m.content){var preview=String(m.content);if(preview.length>60)preview=preview.slice(0,57)+'...';h+='<span class="p">'+esc(preview)+'</span>'}
442
466
  if(m.source&&String(m.source)!=='simulate')h+='<span class="p s">'+esc(String(m.source))+'</span>';
443
467
  h+='</span></div>';
444
468
 
445
469
  var ds=[];
446
470
  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>');
471
+ if(ev.ageMs!=null)ds.push('idle for '+fD(ev.ageMs));
472
+ if(m.sessionState)ds.push('<span class="sc">'+esc(String(m.previousState||'?'))+' \\u2192 '+esc(String(m.sessionState))+'</span>');
449
473
  if(m.provider)ds.push('<span class="dim">provider: '+esc(String(m.provider))+'</span>');
450
474
  if(m.to)ds.push('<span class="dim">to: '+esc(String(m.to))+'</span>');
451
475
  if(ev.sessionKey&&depth===0)ds.push('<span class="dim">session: '+esc(ev.sessionKey.slice(0,12))+'</span>');
@@ -572,7 +596,7 @@ export function getDashboardHtml() {
572
596
  function addAlert(a){
573
597
  alEmpty.style.display='none';
574
598
  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>';
599
+ var sv=a.severity||'error';d.innerHTML='<div class="al-sev '+esc(sv)+'">['+esc(sv.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
600
  alList.insertBefore(d,alList.firstChild);
577
601
  while(alList.children.length>MAX_ALERTS+1)alList.removeChild(alList.lastChild);
578
602
  }
@@ -596,17 +620,21 @@ export function getDashboardHtml() {
596
620
  // ─── SSE (OpenAlerts events + OpenClaw log tailing) ──────────────────────
597
621
  function connectSSE(){
598
622
  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...'};
623
+ try{
624
+ evSrc=new EventSource('/openalerts/events');
625
+ evSrc.addEventListener('openalerts',function(e){try{addEvent(JSON.parse(e.data))}catch(_){}});
626
+ evSrc.addEventListener('history',function(e){try{var evs=JSON.parse(e.data);for(var i=0;i<evs.length;i++)addEvent(evs[i])}catch(_){}});
627
+ evSrc.addEventListener('oclog',function(e){try{addLogEntry(JSON.parse(e.data))}catch(_){}});
628
+ evSrc.onopen=function(){$('sDot').className='dot live';$('sConn').textContent='live'};
629
+ evSrc.onerror=function(e){$('sDot').className='dot dead';$('sConn').textContent='err:'+evSrc.readyState};
630
+ }catch(e){$('sConn').textContent='SSE fail:'+e.message}
604
631
  }
605
632
 
606
633
  // ─── State polling ──────────────────────
607
634
  var prevAl={};
608
635
  function pollState(){
609
- fetch('/openalerts/state').then(function(r){return r.json()}).then(function(s){
636
+ fetch('/openalerts/state').then(function(r){if(!r.ok)throw new Error('HTTP '+r.status);return r.json()}).catch(function(e){$('sUp').textContent='fetch err: '+e.message;return null}).then(function(s){
637
+ if(!s)return;
610
638
  if(s.stats){
611
639
  $('sMsgs').textContent=s.stats.messagesProcessed||0;
612
640
  $('sErr').textContent=(s.stats.messageErrors||0)+(s.stats.webhookErrors||0)+(s.stats.toolErrors||0);
@@ -687,8 +715,19 @@ export function getDashboardHtml() {
687
715
  if($('lA').checked)list.scrollTop=list.scrollHeight;
688
716
  }
689
717
 
718
+ function hasActiveFilter(){
719
+ var fSub=$('lF').value;
720
+ var fSrch=$('lS').value;
721
+ var allLevels=['TRACE','DEBUG','INFO','WARN','ERROR','FATAL'];
722
+ var anyUnchecked=false;
723
+ for(var li=0;li<allLevels.length;li++){var cb=$('lv-'+allLevels[li]);if(cb&&!cb.checked){anyUnchecked=true;break}}
724
+ return !!(fSub||fSrch||anyUnchecked);
725
+ }
726
+
690
727
  function refreshLogs(){
691
- fetch('/openalerts/logs?limit=500').then(function(r){return r.json()}).then(function(data){
728
+ var fetchAll=$('lAll').checked||hasActiveFilter();
729
+ var limit=fetchAll?0:500;
730
+ fetch('/openalerts/logs?limit='+limit).then(function(r){return r.json()}).then(function(data){
692
731
  var list=$('logList');
693
732
  var entries=data.entries||[];
694
733
  var fSub=$('lF').value, fSrch=$('lS').value.toLowerCase();
@@ -713,15 +752,16 @@ export function getDashboardHtml() {
713
752
  }
714
753
 
715
754
  list.innerHTML='';
716
- if(!entries.length){list.innerHTML='<div class="empty-msg">No logs found.</div>';return}
717
-
755
+ var shown=0,total=entries.length;
718
756
  for(var i=0;i<entries.length;i++){
719
757
  var e=entries[i];
720
758
  if(fSub&&e.subsystem.indexOf(fSub)<0)continue;
721
759
  if(!isLevelEnabled(e.level))continue;
722
760
  if(fSrch&&e.message.toLowerCase().indexOf(fSrch)<0&&e.subsystem.toLowerCase().indexOf(fSrch)<0)continue;
723
761
  list.appendChild(buildLogTabRow(e));
762
+ shown++;
724
763
  }
764
+ if(!shown){list.innerHTML='<div class="empty-msg">No logs match your filters. '+(truncated?'Try checking "All logs" to search the full log file.':'')+'</div>';return}
725
765
  if($('lA').checked)list.scrollTop=list.scrollHeight;
726
766
  }).catch(function(){$('logList').innerHTML='<div class="empty-msg">Failed to load.</div>'});
727
767
  }
@@ -736,7 +776,7 @@ export function getDashboardHtml() {
736
776
  var btn=row.querySelector('.log-copy');
737
777
  if(btn)lines.push(btn.getAttribute('data-raw'));
738
778
  });
739
- var blob=new Blob([lines.join('\n')],{type:'text/plain'});
779
+ var blob=new Blob([lines.join('\\n')],{type:'text/plain'});
740
780
  var url=URL.createObjectURL(blob);
741
781
  var a=document.createElement('a');
742
782
  a.href=url;a.download='openalerts-logs-'+Date.now()+'.txt';
@@ -748,6 +788,7 @@ export function getDashboardHtml() {
748
788
  $('lR').addEventListener('click',refreshLogs);
749
789
  $('lE').addEventListener('click',exportLogs);
750
790
  $('lF').addEventListener('change',refreshLogs);
791
+ $('lAll').addEventListener('change',refreshLogs);
751
792
  var sDb;$('lS').addEventListener('input',function(){clearTimeout(sDb);sDb=setTimeout(refreshLogs,300)});
752
793
 
753
794
  // Level filter checkboxes
@@ -814,7 +855,7 @@ export function getDashboardHtml() {
814
855
  var cds=s.cooldowns||{};
815
856
  if(s.rules)for(var i=0;i<s.rules.length;i++){
816
857
  var r=s.rules[i];
817
- var cdTs=cds[r.id];
858
+ var cdTs=findLastFired(cds,r.id);
818
859
  var lastFired=cdTs?fAgo(cdTs):'--';
819
860
  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
861
  }
@@ -826,34 +867,12 @@ export function getDashboardHtml() {
826
867
  html+='</table></div>';
827
868
  }
828
869
 
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
870
  hEl.innerHTML=html;
854
871
  }
855
872
  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
873
  function hTr(l,v){return'<tr><td>'+esc(l)+'</td><td><b>'+esc(String(v))+'</b></td></tr>'}
874
+ /** Find the most recent cooldown timestamp for a rule ID (handles contextual fingerprints like "llm-errors:telegram"). */
875
+ function findLastFired(cds,ruleId){var latest=0;for(var k in cds){if(k===ruleId||k.indexOf(ruleId+':')===0){if(cds[k]>latest)latest=cds[k]}}return latest||null}
857
876
 
858
877
  // ─── Debug tab ──────────────────────
859
878
  function refreshDebug(){
@@ -900,18 +919,13 @@ export function getDashboardHtml() {
900
919
  var cds=s.cooldowns||{};
901
920
  if(s.rules)for(var i=0;i<s.rules.length;i++){
902
921
  var r=s.rules[i];
903
- var cdTs=cds[r.id];
922
+ var cdTs=findLastFired(cds,r.id);
904
923
  var lastFired=cdTs?fAgo(cdTs):'never';
905
924
  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
925
  }
907
926
  rulesHtml+='</table>';
908
927
  $('dbRules').innerHTML=rulesHtml;
909
928
 
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
929
  }
916
930
  $('dbRefresh').addEventListener('click',refreshDebug);
917
931
 
@@ -1,5 +1,6 @@
1
1
  import { readFileSync, existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { DEFAULTS } from "../core/index.js";
3
4
  import { getDashboardHtml } from "./dashboard-html.js";
4
5
  // ─── SSE connection tracking ─────────────────────────────────────────────────
5
6
  const sseConnections = new Set();
@@ -26,14 +27,35 @@ const RULE_IDS = [
26
27
  "heartbeat-fail",
27
28
  "queue-depth",
28
29
  "high-error-rate",
30
+ "tool-errors",
29
31
  "gateway-down",
30
32
  ];
31
33
  function getRuleStatuses(engine) {
32
34
  const state = engine.state;
33
35
  const now = Date.now();
36
+ const cooldownWindow = 15 * 60 * 1000;
34
37
  return RULE_IDS.map((id) => {
35
- const cooldownTs = state.cooldowns.get(id);
36
- const fired = cooldownTs != null && cooldownTs > now - 15 * 60 * 1000;
38
+ // For gateway-down, reflect current condition: if heartbeats have resumed,
39
+ // show OK even if the rule fired recently.
40
+ if (id === "gateway-down") {
41
+ const silenceMs = state.lastHeartbeatTs > 0
42
+ ? now - state.lastHeartbeatTs
43
+ : 0;
44
+ const isCurrentlyDown = state.lastHeartbeatTs > 0 &&
45
+ silenceMs >= DEFAULTS.gatewayDownThresholdMs;
46
+ return { id, status: isCurrentlyDown ? "fired" : "ok" };
47
+ }
48
+ // Cooldown keys are fingerprints like "llm-errors:unknown", not bare rule IDs.
49
+ // Check if ANY cooldown key starting with this rule ID has fired recently.
50
+ let fired = false;
51
+ for (const [key, ts] of state.cooldowns) {
52
+ if (key === id || key.startsWith(id + ":")) {
53
+ if (ts > now - cooldownWindow) {
54
+ fired = true;
55
+ break;
56
+ }
57
+ }
58
+ }
37
59
  return { id, status: fired ? "fired" : "ok" };
38
60
  });
39
61
  }
@@ -115,8 +137,8 @@ function readOpenClawLogs(maxEntries, afterTs) {
115
137
  entries.push(parsed);
116
138
  subsystemSet.add(parsed.subsystem);
117
139
  }
118
- const truncated = entries.length > maxEntries;
119
- const sliced = entries.slice(-maxEntries);
140
+ const truncated = maxEntries > 0 && entries.length > maxEntries;
141
+ const sliced = maxEntries > 0 ? entries.slice(-maxEntries) : entries;
120
142
  const subsystems = Array.from(subsystemSet).sort();
121
143
  return { entries: sliced, truncated, subsystems };
122
144
  }
@@ -208,6 +230,20 @@ export function createDashboardHandler(getEngine) {
208
230
  "Access-Control-Allow-Origin": "*",
209
231
  });
210
232
  res.flushHeaders();
233
+ // Send initial connection event so the browser knows the stream is live
234
+ res.write(`:ok\n\n`);
235
+ // Send current state snapshot as initial event
236
+ const state = engine.state;
237
+ res.write(`event: state\ndata: ${JSON.stringify({
238
+ uptimeMs: Date.now() - state.startedAt,
239
+ stats: state.stats,
240
+ rules: getRuleStatuses(engine),
241
+ })}\n\n`);
242
+ // Send event history so dashboard survives refreshes
243
+ const history = engine.getRecentLiveEvents(200);
244
+ if (history.length > 0) {
245
+ res.write(`event: history\ndata: ${JSON.stringify(history)}\n\n`);
246
+ }
211
247
  // Subscribe to engine events
212
248
  const unsub = engine.bus.on((event) => {
213
249
  try {
@@ -274,7 +310,9 @@ export function createDashboardHandler(getEngine) {
274
310
  // ── GET /openalerts/logs → OpenClaw log entries (for Logs tab) ────
275
311
  if (url.startsWith("/openalerts/logs") && req.method === "GET") {
276
312
  const urlObj = new URL(url, "http://localhost");
277
- const limit = Math.min(parseInt(urlObj.searchParams.get("limit") || "200", 10), 1000);
313
+ const rawLimit = urlObj.searchParams.get("limit") || "200";
314
+ // limit=0 means "no limit" — return all log entries
315
+ const limit = rawLimit === "0" ? 0 : Math.min(parseInt(rawLimit, 10), 50000);
278
316
  const afterTs = urlObj.searchParams.get("after") || undefined;
279
317
  const result = readOpenClawLogs(limit, afterTs);
280
318
  res.writeHead(200, {
@@ -272,6 +272,30 @@ export function createLogBridge(engine) {
272
272
  },
273
273
  });
274
274
  }
275
+ // ── Lane task error (diagnostic) ────────────────────────────────────────────
276
+ // Safety net: catches lane-level errors from diagnostic logs.
277
+ // The agent_end hook already covers agent errors → llm-errors rule.
278
+ // This emits as infra.error to avoid double-counting in the llm-errors window
279
+ // while still ensuring infra-errors fires if the hook path fails.
280
+ // Format: "lane task error: lane=main durationMs=1 error="Error: ...""
281
+ function handleLaneTaskError(rec) {
282
+ const { lane, error: errorMsg } = rec.kvs;
283
+ const dedupeKey = `lane-error:${lane}:${rec.ts}`;
284
+ if (dedupeSet.has(dedupeKey))
285
+ return;
286
+ dedupeSet.add(dedupeKey);
287
+ ingest({
288
+ type: "infra.error",
289
+ ts: rec.ts,
290
+ outcome: "error",
291
+ error: errorMsg,
292
+ meta: {
293
+ lane,
294
+ source: "log-bridge",
295
+ openclawLog: "lane_task_error",
296
+ },
297
+ });
298
+ }
275
299
  // ── Exec command (exec) ────────────────────────────────────────────────────
276
300
  function handleExecCommand(rec) {
277
301
  pendingCommand = rec.message;
@@ -312,6 +336,9 @@ export function createLogBridge(engine) {
312
336
  if (msg.startsWith("session state:")) {
313
337
  handleSessionState(rec);
314
338
  }
339
+ else if (msg.startsWith("lane task error:")) {
340
+ handleLaneTaskError(rec);
341
+ }
315
342
  }
316
343
  else if (rec.subsystem === "exec") {
317
344
  if (msg.startsWith("elevated command")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steadwing/openalerts",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "description": "OpenAlerts — An alerting layer for agentic frameworks",
6
6
  "author": "Steadwing",
@@ -39,5 +39,8 @@
39
39
  "keywords": ["openalerts", "openclaw", "monitoring", "alerting", "plugin"],
40
40
  "engines": {
41
41
  "node": ">=18"
42
+ },
43
+ "overrides": {
44
+ "tar": "^7.5.7"
42
45
  }
43
46
  }