openclaw-observability 1.0.0 → 1.0.2

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.
package/dist/web/ui.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * Audit panel SPA — single-page HTML application
3
+ * Observability panel SPA — single-page HTML application
4
4
  * Contains Dashboard overview + Session Trace detail views
5
5
  * Uses hash routing: #/ = Dashboard, #/trace/{sessionId} = Trace
6
6
  *
@@ -14,7 +14,7 @@ function getAppHtml() {
14
14
  '<head>\n' +
15
15
  '<meta charset="UTF-8">\n' +
16
16
  '<meta name="viewport" content="width=device-width, initial-scale=1.0">\n' +
17
- '<title>OpenClaw Audit Traces</title>\n' +
17
+ '<title>OpenClaw Observability</title>\n' +
18
18
  '<meta name="color-scheme" content="dark light">\n' +
19
19
  '<style>\n' +
20
20
  CSS +
@@ -293,6 +293,78 @@ body.resizing *{cursor:row-resize!important}
293
293
  .trace-alert-banner:hover{filter:brightness(1.1)}
294
294
  .trace-alert-banner .count{font-weight:700}
295
295
 
296
+ /* ---- Analytics ---- */
297
+ .an-grid{display:grid;gap:20px;margin-bottom:24px}
298
+ .an-grid-2{grid-template-columns:1fr 1fr}
299
+ .an-grid-3{grid-template-columns:1fr 1fr 1fr}
300
+ @media(max-width:900px){.an-grid-2,.an-grid-3{grid-template-columns:1fr}}
301
+ .an-card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:20px 24px;box-shadow:inset 0 1px 0 var(--card-highlight)}
302
+ .an-card h3{font-size:14px;font-weight:600;color:var(--text-strong);margin-bottom:16px;display:flex;align-items:center;gap:8px;letter-spacing:-.02em}
303
+ .an-card h3 .icon{font-size:16px}
304
+ .an-card .sub{font-size:11px;color:var(--muted);font-weight:400;margin-left:auto}
305
+
306
+ /* Bar chart */
307
+ .chart-bars{display:flex;align-items:flex-end;gap:3px;height:160px;padding:0 4px;justify-content:center}
308
+ .chart-bar-col{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;min-width:0;max-width:60px;position:relative;height:100%}
309
+ .chart-bar{width:100%;border-radius:3px 3px 0 0;min-height:2px;transition:opacity var(--duration-fast);cursor:pointer;position:relative}
310
+ .chart-bar:hover{opacity:.85}
311
+ .chart-bar-stack{width:100%;display:flex;flex-direction:column-reverse}
312
+ .chart-bar-seg{width:100%;min-height:0;transition:opacity var(--duration-fast)}
313
+ .chart-bar-seg:last-child{border-radius:3px 3px 0 0}
314
+ .chart-x-labels{display:flex;gap:3px;padding:6px 4px 0;border-top:1px solid var(--border);justify-content:center}
315
+ .chart-x-labels span{flex:1;text-align:center;font-size:9px;color:var(--muted);font-family:var(--mono);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:60px}
316
+ .chart-y-axis{display:flex;flex-direction:column;justify-content:space-between;align-items:flex-end;height:160px;padding-right:8px;min-width:40px}
317
+ .chart-y-axis span{font-size:10px;color:var(--muted);font-family:var(--mono)}
318
+ .chart-container{display:flex}
319
+ .chart-main{flex:1;min-width:0}
320
+ .chart-tooltip{position:absolute;bottom:100%;left:50%;transform:translateX(-50%);background:var(--card);border:1px solid var(--border);border-radius:var(--radius-sm);padding:4px 8px;font-size:11px;color:var(--text);white-space:nowrap;pointer-events:none;z-index:20;box-shadow:var(--shadow-md);display:none}
321
+ .chart-bar-col:hover .chart-tooltip{display:block}
322
+ .chart-legend{display:flex;gap:16px;justify-content:center;margin-top:12px;flex-wrap:wrap}
323
+ .chart-legend-item{display:flex;align-items:center;gap:5px;font-size:11px;color:var(--muted)}
324
+ .chart-legend-dot{width:10px;height:10px;border-radius:2px;flex-shrink:0}
325
+
326
+ /* Horizontal bar chart */
327
+ .hbar-list{display:flex;flex-direction:column;gap:10px}
328
+ .hbar-row{display:flex;align-items:center;gap:10px}
329
+ .hbar-label{font-size:12px;color:var(--text);min-width:0;flex:0 0 140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--mono);text-align:right}
330
+ .hbar-track{flex:1;height:22px;background:var(--secondary);border-radius:4px;overflow:hidden;display:flex;position:relative}
331
+ .hbar-fill{height:100%;border-radius:4px 0 0 4px;transition:width .3s var(--ease-out);min-width:2px}
332
+ .hbar-fill:last-child{border-radius:0 4px 4px 0}
333
+ .hbar-value{font-size:11px;color:var(--muted);min-width:60px;font-family:var(--mono);text-align:right}
334
+
335
+ /* Donut/ring chart */
336
+ .donut-wrap{display:flex;align-items:center;gap:24px;justify-content:center}
337
+ .donut-svg{width:140px;height:140px;transform:rotate(-90deg)}
338
+ .donut-circle{fill:none;stroke-linecap:round;transition:stroke-dashoffset .4s var(--ease-out)}
339
+ .donut-center{font-size:18px;font-weight:700;color:var(--text-strong)}
340
+ .donut-legend{display:flex;flex-direction:column;gap:8px}
341
+ .donut-legend-item{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--text)}
342
+ .donut-legend-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
343
+ .donut-legend-val{margin-left:auto;font-family:var(--mono);color:var(--muted);font-size:11px;min-width:50px;text-align:right}
344
+
345
+ /* Data table */
346
+ .an-table{width:100%;border-collapse:collapse}
347
+ .an-table th{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;font-weight:600;text-align:left;padding:8px 12px;border-bottom:1px solid var(--border)}
348
+ .an-table th:last-child,.an-table td:last-child{text-align:right}
349
+ .an-table td{font-size:12px;color:var(--text);padding:8px 12px;border-bottom:1px solid var(--border)}
350
+ .an-table tr:last-child td{border-bottom:none}
351
+ .an-table tr:hover td{background:var(--bg-hover)}
352
+ .an-table .mono{font-family:var(--mono);color:var(--text-strong)}
353
+
354
+ /* Token split bar */
355
+ .token-split{display:flex;height:8px;border-radius:4px;overflow:hidden;margin-top:4px}
356
+ .token-split .inp{background:#8b5cf6}
357
+ .token-split .outp{background:#f59e0b}
358
+ .kpi-sub{font-size:11px;color:var(--muted);margin-top:4px;font-family:var(--mono)}
359
+ .kpi-sub .inp-color{color:#8b5cf6}
360
+ .kpi-sub .outp-color{color:#f59e0b}
361
+
362
+ /* Metric tab switcher */
363
+ .metric-tabs{display:flex;gap:4px;margin-bottom:12px}
364
+ .metric-tab{padding:4px 12px;border-radius:var(--radius-sm);font-size:11px;font-weight:500;color:var(--muted);cursor:pointer;transition:all var(--duration-fast);border:1px solid transparent;background:none}
365
+ .metric-tab:hover{color:var(--text);background:var(--bg-hover)}
366
+ .metric-tab.active{color:var(--accent-foreground);background:var(--accent);border-color:var(--accent)}
367
+
296
368
  /* ---- Empty / Loading ---- */
297
369
  .empty{text-align:center;padding:48px 24px;color:var(--muted)}
298
370
  .empty .icon{font-size:36px;margin-bottom:12px}
@@ -337,7 +409,7 @@ var TYPE_LABELS = {
337
409
  var app = document.getElementById('app');
338
410
  var currentPage = 1;
339
411
  var filterSearch = '';
340
- var filterTimeRange = ''; // '' = All time, or preset key
412
+ var filterTimeRange = '24h'; // default to 24h for better performance
341
413
 
342
414
  var TIME_PRESETS = [
343
415
  { key:'30m', label:'30 min', ms: 30*60*1000 },
@@ -367,7 +439,7 @@ function getTimeLabel() {
367
439
 
368
440
  /* ---------- theme ---------- */
369
441
  function getTheme() {
370
- var saved = localStorage.getItem('oc-audit-theme');
442
+ var saved = localStorage.getItem('oc-observability-theme');
371
443
  if (saved) return saved;
372
444
  return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
373
445
  }
@@ -377,7 +449,7 @@ function applyTheme(t) {
377
449
  } else {
378
450
  document.documentElement.removeAttribute('data-theme');
379
451
  }
380
- localStorage.setItem('oc-audit-theme', t);
452
+ localStorage.setItem('oc-observability-theme', t);
381
453
  }
382
454
  function toggleTheme() {
383
455
  var cur = getTheme();
@@ -494,6 +566,8 @@ function router() {
494
566
  var sid = decodeURIComponent(qIdx >= 0 ? raw.substring(0, qIdx) : raw);
495
567
  var params = parseHashParams(hash);
496
568
  renderTrace(sid, params.action, params.t);
569
+ } else if (hash.indexOf('#/analytics') === 0) {
570
+ renderAnalytics();
497
571
  } else if (hash.indexOf('#/security') === 0) {
498
572
  renderSecurity();
499
573
  } else {
@@ -515,11 +589,12 @@ function renderLayout(active, content) {
515
589
  '<div class="brand-logo">' + ICON_ACTIVITY + '</div>' +
516
590
  '<div class="brand-text">' +
517
591
  '<div class="brand-title">OpenClaw</div>' +
518
- '<div class="brand-sub">Audit Traces</div>' +
592
+ '<div class="brand-sub">Observability</div>' +
519
593
  '</div>' +
520
594
  '</div>' +
521
595
  '<div class="topbar-nav">' +
522
596
  '<a href="#/" class="' + (active==='dashboard'?'active':'') + '">Dashboard</a>' +
597
+ '<a href="#/analytics" class="' + (active==='analytics'?'active':'') + '">Analytics</a>' +
523
598
  '<a href="#/security" class="' + (active==='security'?'active':'') + '">Security' + (window.__alertCount > 0 ? '<span class="nav-badge">' + window.__alertCount + '</span>' : '') + '</a>' +
524
599
  '</div>' +
525
600
  '</div>' +
@@ -678,8 +753,367 @@ window.selectTimeRange = function(key) {
678
753
  document.addEventListener('click', function() {
679
754
  var menu = document.getElementById('time-menu');
680
755
  if (menu) menu.classList.remove('open');
756
+ // Close analytics dropdowns too
757
+ var anMenu = document.getElementById('an-time-menu');
758
+ if (anMenu) anMenu.classList.remove('open');
681
759
  });
682
760
 
761
+ /* ================================================================ */
762
+ /* Analytics tab */
763
+ /* ================================================================ */
764
+
765
+ var anTimeRange = '24h';
766
+ var anMetricTab = 'sessions'; // sessions | tokens
767
+
768
+ function anGetTimeFromISO() {
769
+ if (!anTimeRange) return '';
770
+ var preset = TIME_PRESETS.find(function(p){ return p.key === anTimeRange; });
771
+ if (!preset || !preset.ms) return '';
772
+ return new Date(Date.now() - preset.ms).toISOString();
773
+ }
774
+
775
+ function anGetTimeLabel() {
776
+ if (!anTimeRange) return 'All time';
777
+ var preset = TIME_PRESETS.find(function(p){ return p.key === anTimeRange; });
778
+ return preset ? preset.label : 'All time';
779
+ }
780
+
781
+ async function renderAnalytics() {
782
+ app.innerHTML = renderLayout('analytics', '<div class="loading">Loading analytics...</div>');
783
+
784
+ try {
785
+ var qs = '';
786
+ var tf = anGetTimeFromISO();
787
+ if (tf) qs = '?timeFrom=' + encodeURIComponent(tf);
788
+
789
+ var data = await fetchApi('/analytics' + qs);
790
+
791
+ var ov = data.overview;
792
+ var ts = data.timeSeries || [];
793
+ var mu = data.modelUsage || [];
794
+ var ad = data.actionDistribution || {};
795
+ var ta = data.topAgents || [];
796
+ var tbm = data.tokensByModel || [];
797
+
798
+ var html = '';
799
+
800
+ // --- Time range filter ---
801
+ html += '<div class="filter-bar" style="margin-bottom:20px">';
802
+ html += '<div class="time-dropdown" id="an-time-dropdown">';
803
+ html += '<button class="time-btn" onclick="anToggleTimeMenu(event)">';
804
+ html += '<svg viewBox="0 0 24 24" style="width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:1.5"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg> ';
805
+ html += esc(anGetTimeLabel());
806
+ html += ' <svg viewBox="0 0 24 24" style="width:12px;height:12px"><polyline points="6 9 12 15 18 9"/></svg>';
807
+ html += '</button>';
808
+ html += '<div class="time-menu" id="an-time-menu">';
809
+ TIME_PRESETS.forEach(function(p) {
810
+ var cls = (p.key === anTimeRange) ? ' active' : '';
811
+ html += '<div class="time-menu-item' + cls + '" onclick="anSelectTime(\\'' + p.key + '\\')">';
812
+ html += '<span class="check">' + (p.key === anTimeRange ? '✓' : '') + '</span>';
813
+ html += esc(p.label);
814
+ html += '</div>';
815
+ });
816
+ html += '</div></div>';
817
+ html += '</div>';
818
+
819
+ // --- KPI stat cards ---
820
+ var inpPct = ov.totalTokens > 0 ? Math.round(ov.inputTokens / ov.totalTokens * 100) : 50;
821
+ html += '<div class="stat-grid" style="grid-template-columns:repeat(auto-fit,minmax(140px,1fr))">';
822
+ html += statCard('Sessions', fmtNum(ov.totalSessions));
823
+
824
+ // Tokens with input/output split
825
+ html += '<div class="stat"><div class="stat-label">Total Tokens</div>';
826
+ html += '<div class="stat-value">' + fmtNum(ov.totalTokens) + '</div>';
827
+ html += '<div class="token-split"><div class="inp" style="width:' + inpPct + '%"></div><div class="outp" style="width:' + (100 - inpPct) + '%"></div></div>';
828
+ html += '<div class="kpi-sub"><span class="inp-color">⬤</span> ' + fmtNum(ov.inputTokens) + ' in <span class="outp-color">⬤</span> ' + fmtNum(ov.outputTokens) + ' out</div>';
829
+ html += '</div>';
830
+
831
+ html += statCard('Actions', fmtNum(ov.totalActions));
832
+ html += statCard('Avg Latency', fmtDur(ov.avgLatencyMs));
833
+ html += statCard('Models', String(ov.activeModels));
834
+ html += statCard('Alerts', String(ov.securityAlerts));
835
+ html += '</div>';
836
+
837
+ // --- Row 1: Traces by time + Token usage by time ---
838
+ html += '<div class="an-grid an-grid-2">';
839
+
840
+ // Traces by time (bar chart)
841
+ html += '<div class="an-card">';
842
+ var gran = data.granularity || 'day';
843
+ var granLabel = gran === 'hour' ? (ts.length + ' hours') : (ts.length + ' days');
844
+ html += '<h3><span class="icon">📊</span> Activity Over Time';
845
+ html += '<span class="sub">' + granLabel + '</span></h3>';
846
+ html += buildTimeSeriesChart(ts, anMetricTab, gran);
847
+ html += '</div>';
848
+
849
+ // Model Usage (horizontal bars)
850
+ html += '<div class="an-card">';
851
+ html += '<h3><span class="icon">🤖</span> Model Usage';
852
+ html += '<span class="sub">' + mu.length + ' models</span></h3>';
853
+ html += buildModelUsageChart(mu, tbm);
854
+ html += '</div>';
855
+
856
+ html += '</div>';
857
+
858
+ // --- Row 2: Action Distribution + Top Agents ---
859
+ html += '<div class="an-grid an-grid-2">';
860
+
861
+ // Action type distribution (donut + legend)
862
+ html += '<div class="an-card">';
863
+ html += '<h3><span class="icon">🎯</span> Action Distribution</h3>';
864
+ html += buildActionDistribution(ad);
865
+ html += '</div>';
866
+
867
+ // Top agents table
868
+ html += '<div class="an-card">';
869
+ html += '<h3><span class="icon">👤</span> Top Agents';
870
+ html += '<span class="sub">' + ta.length + ' agents</span></h3>';
871
+ html += buildAgentsTable(ta);
872
+ html += '</div>';
873
+
874
+ html += '</div>';
875
+
876
+ app.innerHTML = renderLayout('analytics', html);
877
+
878
+ } catch(err) {
879
+ app.innerHTML = renderLayout('analytics',
880
+ '<div class="empty"><div class="icon">⚠️</div><div class="text">Failed to load analytics: ' + esc(String(err)) + '</div></div>');
881
+ }
882
+ }
883
+
884
+ /* --- Chart builders --- */
885
+
886
+ function buildTimeSeriesChart(ts, metric, gran) {
887
+ gran = gran || 'day';
888
+ if (!ts || ts.length === 0) {
889
+ return '<div class="empty" style="padding:24px"><div class="text">No data in selected range</div></div>';
890
+ }
891
+
892
+ // Metric tab switcher
893
+ var h = '<div class="metric-tabs">';
894
+ h += '<div class="metric-tab' + (metric==='sessions'?' active':'') + '" onclick="anSwitchMetric(\\'sessions\\')">Sessions</div>';
895
+ h += '<div class="metric-tab' + (metric==='tokens'?' active':'') + '" onclick="anSwitchMetric(\\'tokens\\')">Tokens</div>';
896
+ h += '<div class="metric-tab' + (metric==='actions'?' active':'') + '" onclick="anSwitchMetric(\\'actions\\')">Actions</div>';
897
+ h += '</div>';
898
+
899
+ // Determine values and max
900
+ var vals = ts.map(function(p) {
901
+ if (metric === 'tokens') return { v1: p.inputTokens, v2: p.outputTokens, total: p.tokens, label: p.date };
902
+ if (metric === 'actions') return { v1: p.actions, v2: 0, total: p.actions, label: p.date };
903
+ return { v1: p.sessions, v2: 0, total: p.sessions, label: p.date };
904
+ });
905
+ var maxVal = Math.max.apply(null, vals.map(function(v){ return v.total; }));
906
+ if (maxVal === 0) maxVal = 1;
907
+
908
+ // Y-axis
909
+ h += '<div class="chart-container">';
910
+ h += '<div class="chart-y-axis">';
911
+ h += '<span>' + fmtNum(maxVal) + '</span>';
912
+ h += '<span>' + fmtNum(Math.round(maxVal * 0.5)) + '</span>';
913
+ h += '<span>0</span>';
914
+ h += '</div>';
915
+
916
+ // Bars
917
+ h += '<div class="chart-main">';
918
+ h += '<div class="chart-bars">';
919
+ var showTokenSplit = metric === 'tokens';
920
+ var barColor1 = metric === 'tokens' ? '#8b5cf6' : (metric === 'actions' ? '#3b82f6' : 'var(--accent)');
921
+ var barColor2 = '#f59e0b';
922
+
923
+ vals.forEach(function(v) {
924
+ var pct = Math.max((v.total / maxVal) * 100, 1);
925
+ var dayLabel;
926
+ if (gran === 'hour') {
927
+ // "2026-03-12 14" → "14:00"
928
+ var hourPart = v.label.length >= 13 ? v.label.slice(11, 13) : v.label;
929
+ dayLabel = hourPart + ':00';
930
+ } else {
931
+ dayLabel = v.label.length > 5 ? v.label.slice(5) : v.label; // MM-DD
932
+ }
933
+ h += '<div class="chart-bar-col">';
934
+ h += '<div class="chart-tooltip">' + v.label + ': ' + fmtNum(v.total);
935
+ if (showTokenSplit) h += ' (in:' + fmtNum(v.v1) + ' out:' + fmtNum(v.v2) + ')';
936
+ h += '</div>';
937
+
938
+ if (showTokenSplit && v.v1 + v.v2 > 0) {
939
+ var pct1 = (v.v1 / maxVal) * 100;
940
+ var pct2 = (v.v2 / maxVal) * 100;
941
+ h += '<div class="chart-bar-stack" style="height:' + pct + '%">';
942
+ h += '<div class="chart-bar-seg" style="height:' + (v.v2 / v.total * 100) + '%;background:' + barColor2 + '"></div>';
943
+ h += '<div class="chart-bar-seg" style="height:' + (v.v1 / v.total * 100) + '%;background:' + barColor1 + '"></div>';
944
+ h += '</div>';
945
+ } else {
946
+ h += '<div class="chart-bar" style="height:' + pct + '%;background:' + barColor1 + '"></div>';
947
+ }
948
+ h += '</div>';
949
+ });
950
+ h += '</div>';
951
+
952
+ // X labels (show max 15 labels)
953
+ h += '<div class="chart-x-labels">';
954
+ var step = Math.max(1, Math.ceil(vals.length / 15));
955
+ vals.forEach(function(v, i) {
956
+ var xLabel;
957
+ if (gran === 'hour') {
958
+ var hp = v.label.length >= 13 ? v.label.slice(11, 13) : v.label;
959
+ xLabel = hp + ':00';
960
+ } else {
961
+ xLabel = v.label.length > 5 ? v.label.slice(5) : v.label;
962
+ }
963
+ h += '<span>' + (i % step === 0 ? xLabel : '') + '</span>';
964
+ });
965
+ h += '</div>';
966
+ h += '</div></div>'; // chart-main, chart-container
967
+
968
+ // Legend for token split
969
+ if (showTokenSplit) {
970
+ h += '<div class="chart-legend">';
971
+ h += '<div class="chart-legend-item"><div class="chart-legend-dot" style="background:#8b5cf6"></div>Input tokens</div>';
972
+ h += '<div class="chart-legend-item"><div class="chart-legend-dot" style="background:#f59e0b"></div>Output tokens</div>';
973
+ h += '</div>';
974
+ }
975
+
976
+ return h;
977
+ }
978
+
979
+ function buildModelUsageChart(mu, tbm) {
980
+ if (!mu || mu.length === 0) {
981
+ return '<div class="empty" style="padding:24px"><div class="text">No model data</div></div>';
982
+ }
983
+
984
+ var maxTokens = Math.max.apply(null, mu.map(function(m){ return m.inputTokens + m.outputTokens; }));
985
+ if (maxTokens === 0) maxTokens = 1;
986
+
987
+ var h = '<div class="hbar-list">';
988
+ mu.forEach(function(m) {
989
+ var total = m.inputTokens + m.outputTokens;
990
+ var pctInp = (m.inputTokens / maxTokens) * 100;
991
+ var pctOutp = (m.outputTokens / maxTokens) * 100;
992
+ var shortModel = m.model.length > 20 ? m.model.slice(m.model.indexOf('/') + 1) : m.model;
993
+
994
+ h += '<div class="hbar-row">';
995
+ h += '<div class="hbar-label" title="' + esc(m.model) + '">' + esc(shortModel) + '</div>';
996
+ h += '<div class="hbar-track">';
997
+ h += '<div class="hbar-fill" style="width:' + pctInp + '%;background:#8b5cf6"></div>';
998
+ h += '<div class="hbar-fill" style="width:' + pctOutp + '%;background:#f59e0b"></div>';
999
+ h += '</div>';
1000
+ h += '<div class="hbar-value">' + fmtNum(total) + '</div>';
1001
+ h += '</div>';
1002
+ });
1003
+ h += '</div>';
1004
+
1005
+ h += '<div class="chart-legend" style="margin-top:16px">';
1006
+ h += '<div class="chart-legend-item"><div class="chart-legend-dot" style="background:#8b5cf6"></div>Input</div>';
1007
+ h += '<div class="chart-legend-item"><div class="chart-legend-dot" style="background:#f59e0b"></div>Output</div>';
1008
+ h += '</div>';
1009
+
1010
+ // Model details table
1011
+ h += '<table class="an-table" style="margin-top:16px">';
1012
+ h += '<tr><th>Model</th><th>Calls</th><th>Avg Latency</th><th>Tokens</th></tr>';
1013
+ mu.forEach(function(m) {
1014
+ var shortModel = m.model.length > 20 ? m.model.slice(m.model.indexOf('/') + 1) : m.model;
1015
+ h += '<tr>';
1016
+ h += '<td title="' + esc(m.model) + '">' + esc(shortModel) + '</td>';
1017
+ h += '<td class="mono">' + m.calls + '</td>';
1018
+ h += '<td class="mono">' + fmtDur(m.avgLatency) + '</td>';
1019
+ h += '<td class="mono">' + fmtNum(m.inputTokens + m.outputTokens) + '</td>';
1020
+ h += '</tr>';
1021
+ });
1022
+ h += '</table>';
1023
+
1024
+ return h;
1025
+ }
1026
+
1027
+ function buildActionDistribution(ad) {
1028
+ var keys = Object.keys(ad);
1029
+ if (keys.length === 0) {
1030
+ return '<div class="empty" style="padding:24px"><div class="text">No action data</div></div>';
1031
+ }
1032
+
1033
+ var total = keys.reduce(function(s, k) { return s + ad[k]; }, 0);
1034
+ var COLORS = ['#8b5cf6','#f59e0b','#3b82f6','#10b981','#ef4444','#06b6d4','#f97316','#a855f7','#22c55e','#64748b','#d946ef','#84cc16'];
1035
+
1036
+ // Donut chart using SVG
1037
+ var h = '<div class="donut-wrap">';
1038
+ var radius = 55;
1039
+ var circumference = 2 * Math.PI * radius;
1040
+ h += '<div style="position:relative;width:140px;height:140px">';
1041
+ h += '<svg class="donut-svg" viewBox="0 0 140 140">';
1042
+ var offset = 0;
1043
+ keys.forEach(function(k, i) {
1044
+ var pct = ad[k] / total;
1045
+ var dashLen = pct * circumference;
1046
+ var color = COLORS[i % COLORS.length];
1047
+ h += '<circle class="donut-circle" cx="70" cy="70" r="' + radius + '" stroke="' + color + '" stroke-width="16" ';
1048
+ h += 'stroke-dasharray="' + dashLen + ' ' + (circumference - dashLen) + '" ';
1049
+ h += 'stroke-dashoffset="' + (-offset) + '"/>';
1050
+ offset += dashLen;
1051
+ });
1052
+ h += '</svg>';
1053
+ h += '<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center">';
1054
+ h += '<div class="donut-center">' + fmtNum(total) + '</div>';
1055
+ h += '<div style="font-size:10px;color:var(--muted)">total</div>';
1056
+ h += '</div></div>';
1057
+
1058
+ // Legend
1059
+ h += '<div class="donut-legend">';
1060
+ keys.slice(0, 10).forEach(function(k, i) {
1061
+ var pct = Math.round(ad[k] / total * 100);
1062
+ h += '<div class="donut-legend-item">';
1063
+ h += '<div class="donut-legend-dot" style="background:' + COLORS[i % COLORS.length] + '"></div>';
1064
+ h += '<span>' + typeLabel(k) + '</span>';
1065
+ h += '<span class="donut-legend-val">' + ad[k] + ' (' + pct + '%)</span>';
1066
+ h += '</div>';
1067
+ });
1068
+ if (keys.length > 10) {
1069
+ h += '<div class="donut-legend-item" style="color:var(--muted)">... +' + (keys.length - 10) + ' more</div>';
1070
+ }
1071
+ h += '</div>';
1072
+ h += '</div>';
1073
+
1074
+ return h;
1075
+ }
1076
+
1077
+ function buildAgentsTable(ta) {
1078
+ if (!ta || ta.length === 0) {
1079
+ return '<div class="empty" style="padding:24px"><div class="text">No agent data</div></div>';
1080
+ }
1081
+
1082
+ var maxSess = Math.max.apply(null, ta.map(function(a){ return a.sessions; }));
1083
+ var h = '<table class="an-table">';
1084
+ h += '<tr><th>Agent</th><th>Sessions</th><th>Actions</th><th>Tokens</th></tr>';
1085
+ ta.forEach(function(a) {
1086
+ h += '<tr>';
1087
+ h += '<td>🤖 ' + esc(a.agent) + '</td>';
1088
+ h += '<td class="mono">' + a.sessions + '</td>';
1089
+ h += '<td class="mono">' + fmtNum(a.actions) + '</td>';
1090
+ h += '<td class="mono">' + fmtNum(a.tokens) + '</td>';
1091
+ h += '</tr>';
1092
+ });
1093
+ h += '</table>';
1094
+
1095
+ return h;
1096
+ }
1097
+
1098
+ /* Analytics event handlers */
1099
+ window.anToggleTimeMenu = function(e) {
1100
+ e.stopPropagation();
1101
+ var menu = document.getElementById('an-time-menu');
1102
+ if (menu) menu.classList.toggle('open');
1103
+ };
1104
+
1105
+ window.anSelectTime = function(key) {
1106
+ anTimeRange = key;
1107
+ var menu = document.getElementById('an-time-menu');
1108
+ if (menu) menu.classList.remove('open');
1109
+ renderAnalytics();
1110
+ };
1111
+
1112
+ window.anSwitchMetric = function(metric) {
1113
+ anMetricTab = metric;
1114
+ renderAnalytics();
1115
+ };
1116
+
683
1117
  /* ================================================================ */
684
1118
  /* Security tab — alert badge count */
685
1119
  /* ================================================================ */
@@ -1 +1 @@
1
- {"version":3,"file":"ui.js","sourceRoot":"","sources":["../../src/web/ui.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;AAEH,gCAmBC;AAnBD,SAAgB,UAAU;IACxB,OAAO,mBAAmB;QAC5B,oBAAoB;QACpB,UAAU;QACV,0BAA0B;QAC1B,0EAA0E;QAC1E,wCAAwC;QACxC,mDAAmD;QACnD,WAAW;QACX,GAAG;QACH,YAAY;QACZ,WAAW;QACX,UAAU;QACV,wBAAwB;QACxB,YAAY;QACZ,SAAS;QACT,aAAa;QACb,WAAW;QACX,SAAS,CAAC;AACV,CAAC;AAED,wEAAwE;AACxE,yEAAyE;AACzE,wEAAwE;AAExE,MAAM,GAAG,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqRX,CAAC;AAEF,wEAAwE;AACxE,wEAAwE;AACxE,wEAAwE;AAExE,MAAM,SAAS,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAm/BjB,CAAC"}
1
+ {"version":3,"file":"ui.js","sourceRoot":"","sources":["../../src/web/ui.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;AAEH,gCAmBC;AAnBD,SAAgB,UAAU;IACxB,OAAO,mBAAmB;QAC5B,oBAAoB;QACpB,UAAU;QACV,0BAA0B;QAC1B,0EAA0E;QAC1E,yCAAyC;QACzC,mDAAmD;QACnD,WAAW;QACX,GAAG;QACH,YAAY;QACZ,WAAW;QACX,UAAU;QACV,wBAAwB;QACxB,YAAY;QACZ,SAAS;QACT,aAAa;QACb,WAAW;QACX,SAAS,CAAC;AACV,CAAC;AAED,wEAAwE;AACxE,yEAAyE;AACzE,wEAAwE;AAExE,MAAM,GAAG,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6VX,CAAC;AAEF,wEAAwE;AACxE,wEAAwE;AACxE,wEAAwE;AAExE,MAAM,SAAS,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA61CjB,CAAC"}
@@ -1,8 +1,8 @@
1
1
  {
2
- "id": "audit-duckdb",
3
- "name": "openclaw-plugin-audit",
4
- "version": "1.0.5",
5
- "description": "Conversation model action audit plugin — full-chain traceability with built-in visualization",
2
+ "id": "openclaw-observability",
3
+ "name": "openclaw-observability",
4
+ "version": "1.0.2",
5
+ "description": "Conversation model action observability plugin — full-chain traceability with built-in visualization",
6
6
  "entry": "dist/index.js",
7
7
  "slots": [
8
8
  "before_model_resolve",
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "duckdb.path": {
35
35
  "label": "Local DB File Path",
36
- "placeholder": "~/.openclaw/audit.duckdb",
36
+ "placeholder": "~/.openclaw/observability.duckdb",
37
37
  "help": "Local database file path (only effective in local mode)"
38
38
  },
39
39
  "mysql.host": {
@@ -57,8 +57,8 @@
57
57
  },
58
58
  "mysql.database": {
59
59
  "label": "Database Name",
60
- "placeholder": "openclaw_audit",
61
- "help": "Audit database name (auto-created if not exists)"
60
+ "placeholder": "openclaw_observability",
61
+ "help": "Database name (auto-created if not exists)"
62
62
  },
63
63
  "buffer.batchSize": {
64
64
  "label": "Batch Size",
@@ -68,8 +68,8 @@
68
68
  },
69
69
  "buffer.flushIntervalMs": {
70
70
  "label": "Flush Interval (ms)",
71
- "placeholder": "30000",
72
- "help": "Flush interval in milliseconds (default: 30s)",
71
+ "placeholder": "5000",
72
+ "help": "Flush interval in milliseconds (default: 5s)",
73
73
  "advanced": true
74
74
  },
75
75
  "redaction.enabled": {
@@ -83,7 +83,7 @@
83
83
  },
84
84
  "security.enabled": {
85
85
  "label": "Enable Security Scanning",
86
- "help": "Enable security audit scanning (secret leaks, dangerous commands, prompt injection, etc.)"
86
+ "help": "Enable security scanning (secret leaks, dangerous commands, prompt injection, etc.)"
87
87
  },
88
88
  "security.rules.secretLeakage": {
89
89
  "label": "Secret Leakage Detection",
@@ -126,7 +126,7 @@
126
126
  "properties": {
127
127
  "path": {
128
128
  "type": "string",
129
- "default": "~/.openclaw/audit.duckdb"
129
+ "default": "~/.openclaw/observability.duckdb"
130
130
  }
131
131
  }
132
132
  },
@@ -152,7 +152,7 @@
152
152
  },
153
153
  "database": {
154
154
  "type": "string",
155
- "default": "openclaw_audit"
155
+ "default": "openclaw_observability"
156
156
  }
157
157
  }
158
158
  },
@@ -168,7 +168,7 @@
168
168
  },
169
169
  "flushIntervalMs": {
170
170
  "type": "number",
171
- "default": 30000,
171
+ "default": 5000,
172
172
  "minimum": 1000,
173
173
  "maximum": 300000
174
174
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "openclaw-observability",
3
- "version": "1.0.0",
4
- "description": "OpenClaw audit plugin — records all conversation model actions into DuckDB/MySQL for traceability, with built-in Langfuse-style visualization",
3
+ "version": "1.0.2",
4
+ "description": "OpenClaw observability plugin — records all conversation model actions into DuckDB/MySQL for traceability, with built-in visualization",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "openclaw": {
@@ -11,7 +11,8 @@
11
11
  },
12
12
  "files": [
13
13
  "dist/",
14
- "openclaw.plugin.json"
14
+ "openclaw.plugin.json",
15
+ "README.md"
15
16
  ],
16
17
  "scripts": {
17
18
  "build": "tsc",
@@ -23,11 +24,10 @@
23
24
  "openclaw",
24
25
  "plugin",
25
26
  "observability",
26
- "audit",
27
+ "tracing",
27
28
  "duckdb",
28
29
  "mysql",
29
30
  "trace",
30
- "langfuse",
31
31
  "security"
32
32
  ],
33
33
  "license": "MIT",