bosun 0.37.0 → 0.37.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/.env.example +4 -1
- package/agent-tool-config.mjs +338 -0
- package/bosun-skills.mjs +59 -4
- package/bosun.schema.json +1 -1
- package/desktop/launch.mjs +18 -0
- package/desktop/main.mjs +52 -13
- package/fleet-coordinator.mjs +34 -1
- package/kanban-adapter.mjs +30 -3
- package/library-manager.mjs +66 -0
- package/maintenance.mjs +30 -5
- package/monitor.mjs +56 -0
- package/package.json +4 -1
- package/setup-web-server.mjs +73 -12
- package/setup.mjs +3 -3
- package/ui/app.js +40 -3
- package/ui/components/session-list.js +25 -7
- package/ui/components/workspace-switcher.js +48 -1
- package/ui/demo.html +176 -0
- package/ui/modules/mic-track-registry.js +83 -0
- package/ui/modules/settings-schema.js +4 -1
- package/ui/modules/state.js +25 -0
- package/ui/modules/streaming.js +1 -1
- package/ui/modules/voice-barge-in.js +27 -0
- package/ui/modules/voice-client-sdk.js +268 -42
- package/ui/modules/voice-client.js +665 -61
- package/ui/modules/voice-overlay.js +829 -47
- package/ui/setup.html +151 -9
- package/ui/styles.css +258 -0
- package/ui/tabs/chat.js +11 -0
- package/ui/tabs/library.js +890 -15
- package/ui/tabs/settings.js +51 -11
- package/ui/tabs/telemetry.js +327 -105
- package/ui/tabs/workflows.js +86 -0
- package/ui-server.mjs +1201 -107
- package/voice-action-dispatcher.mjs +81 -0
- package/voice-agents-sdk.mjs +2 -2
- package/voice-relay.mjs +131 -14
- package/voice-tools.mjs +475 -9
- package/workflow-engine.mjs +54 -0
- package/workflow-nodes.mjs +177 -28
- package/workflow-templates/github.mjs +205 -94
- package/workflow-templates/task-batch.mjs +247 -0
- package/workflow-templates.mjs +15 -0
package/ui/tabs/settings.js
CHANGED
|
@@ -2114,25 +2114,36 @@ function VoiceEndpointsEditor() {
|
|
|
2114
2114
|
];
|
|
2115
2115
|
}, []);
|
|
2116
2116
|
|
|
2117
|
-
const normalizeEp = useCallback((ep = {}, idx = 0) =>
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2117
|
+
const normalizeEp = useCallback((ep = {}, idx = 0) => {
|
|
2118
|
+
const provider = ["azure", "openai", "claude", "gemini", "custom"].includes(ep.provider)
|
|
2119
|
+
? ep.provider
|
|
2120
|
+
: "azure";
|
|
2121
|
+
const transcriptionEnabled = ep.transcriptionEnabled == null
|
|
2122
|
+
? provider !== "azure"
|
|
2123
|
+
: ep.transcriptionEnabled !== false;
|
|
2124
|
+
return {
|
|
2125
|
+
_id: ep._id ?? `ep-${idx}-${Date.now()}`,
|
|
2126
|
+
name: String(ep.name || `endpoint-${idx + 1}`),
|
|
2127
|
+
provider,
|
|
2121
2128
|
endpoint: (() => {
|
|
2122
|
-
const p = ["azure", "openai", "claude", "gemini", "custom"].includes(ep.provider) ? ep.provider : "azure";
|
|
2123
2129
|
const raw = String(ep.endpoint || "");
|
|
2124
|
-
return (
|
|
2130
|
+
return (provider === "azure" || provider === "custom")
|
|
2131
|
+
? raw
|
|
2132
|
+
: (raw || getDefaultEndpointUrl(provider, ep.authSource));
|
|
2125
2133
|
})(),
|
|
2126
2134
|
deployment: String(ep.deployment || ""),
|
|
2127
2135
|
model: String(ep.model || ""),
|
|
2128
2136
|
visionModel: String(ep.visionModel || ""),
|
|
2137
|
+
transcriptionModel: String(ep.transcriptionModel || ""),
|
|
2138
|
+
transcriptionEnabled,
|
|
2129
2139
|
apiKey: String(ep.apiKey || ""),
|
|
2130
2140
|
voiceId: String(ep.voiceId || ""),
|
|
2131
2141
|
role: ["primary", "backup"].includes(ep.role) ? ep.role : "primary",
|
|
2132
2142
|
weight: Number(ep.weight) > 0 ? Number(ep.weight) : 1,
|
|
2133
2143
|
enabled: ep.enabled !== false,
|
|
2134
2144
|
authSource: ["apiKey", "oauth"].includes(ep.authSource) ? ep.authSource : "apiKey",
|
|
2135
|
-
|
|
2145
|
+
};
|
|
2146
|
+
}, [getDefaultEndpointUrl]);
|
|
2136
2147
|
|
|
2137
2148
|
// Fetch OAuth status for all providers
|
|
2138
2149
|
const fetchOAuthStatuses = useCallback(async () => {
|
|
@@ -2359,12 +2370,20 @@ function VoiceEndpointsEditor() {
|
|
|
2359
2370
|
onInput=${(e) => updateEndpoint(ep._id, "endpoint", e.target.value)} />
|
|
2360
2371
|
</div>
|
|
2361
2372
|
<div style="grid-column:1/-1">
|
|
2362
|
-
<div class="setting-row-label">
|
|
2363
|
-
<input type="text" value=${ep.deployment} placeholder="gpt-realtime
|
|
2373
|
+
<div class="setting-row-label">Deployment Name</div>
|
|
2374
|
+
<input type="text" value=${ep.deployment} placeholder="my-gpt-4o-realtime"
|
|
2364
2375
|
onInput=${(e) => updateEndpoint(ep._id, "deployment", e.target.value)} />
|
|
2365
2376
|
<div class="meta-text" style="margin-top:3px">
|
|
2366
|
-
|
|
2367
|
-
|
|
2377
|
+
The deployment name from Azure AI Foundry (not the model name).
|
|
2378
|
+
Find it under your resource → Deployments. Leave empty to test credentials only.
|
|
2379
|
+
</div>
|
|
2380
|
+
</div>
|
|
2381
|
+
<div style="grid-column:1/-1">
|
|
2382
|
+
<div class="setting-row-label">Audio Model (Realtime)</div>
|
|
2383
|
+
<input type="text" value=${ep.model} placeholder="gpt-4o-realtime-preview"
|
|
2384
|
+
onInput=${(e) => updateEndpoint(ep._id, "model", e.target.value)} />
|
|
2385
|
+
<div class="meta-text" style="margin-top:3px">
|
|
2386
|
+
The underlying model name (e.g. gpt-4o-realtime-preview). Used at runtime.
|
|
2368
2387
|
</div>
|
|
2369
2388
|
</div>
|
|
2370
2389
|
`}
|
|
@@ -2451,6 +2470,27 @@ function VoiceEndpointsEditor() {
|
|
|
2451
2470
|
onInput=${(e) => updateEndpoint(ep._id, "visionModel", e.target.value)} />
|
|
2452
2471
|
<div class="meta-text" style="margin-top:3px">Model used for screenshot / image analysis tasks.</div>
|
|
2453
2472
|
</div>
|
|
2473
|
+
${(ep.provider === "openai" || ep.provider === "azure") && html`
|
|
2474
|
+
<div style="grid-column:1/-1;display:grid;grid-template-columns:1fr auto;gap:8px;align-items:end">
|
|
2475
|
+
<div>
|
|
2476
|
+
<div class="setting-row-label">Transcription Model</div>
|
|
2477
|
+
<input type="text" value=${ep.transcriptionModel || ""}
|
|
2478
|
+
placeholder="gpt-4o-transcribe"
|
|
2479
|
+
onInput=${(e) => updateEndpoint(ep._id, "transcriptionModel", e.target.value)} />
|
|
2480
|
+
<div class="meta-text" style="margin-top:3px">
|
|
2481
|
+
Model used for input audio transcription. Leave blank for default (gpt-4o-transcribe).
|
|
2482
|
+
${ep.provider === "azure" ? " Azure endpoints default transcription OFF unless enabled." : ""}
|
|
2483
|
+
</div>
|
|
2484
|
+
</div>
|
|
2485
|
+
<div style="display:flex;align-items:center;gap:6px;padding-bottom:22px">
|
|
2486
|
+
<label style="display:flex;align-items:center;gap:4px;cursor:pointer;font-size:13px">
|
|
2487
|
+
<input type="checkbox" checked=${ep.transcriptionEnabled !== false}
|
|
2488
|
+
onChange=${(e) => updateEndpoint(ep._id, "transcriptionEnabled", e.target.checked)} />
|
|
2489
|
+
Enable
|
|
2490
|
+
</label>
|
|
2491
|
+
</div>
|
|
2492
|
+
</div>
|
|
2493
|
+
`}
|
|
2454
2494
|
</div>
|
|
2455
2495
|
<!-- Test Connection -->
|
|
2456
2496
|
<div style="display:flex;align-items:center;gap:10px;margin-top:8px">
|
package/ui/tabs/telemetry.js
CHANGED
|
@@ -1,166 +1,388 @@
|
|
|
1
1
|
/* ─────────────────────────────────────────────────────────────
|
|
2
|
-
* Tab: Telemetry
|
|
2
|
+
* Tab: Telemetry / Usage Analytics
|
|
3
|
+
* Shows agent runs, skill invocations, MCP tool usage, activity
|
|
4
|
+
* trend chart, and top-N bar charts.
|
|
3
5
|
* ────────────────────────────────────────────────────────────── */
|
|
4
6
|
import { h } from "preact";
|
|
5
|
-
import { useMemo } from "preact/hooks";
|
|
7
|
+
import { useState, useMemo, useEffect } from "preact/hooks";
|
|
6
8
|
import htm from "htm";
|
|
7
9
|
|
|
8
10
|
const html = htm.bind(h);
|
|
9
11
|
|
|
10
12
|
import {
|
|
11
|
-
telemetrySummary,
|
|
12
13
|
telemetryErrors,
|
|
13
|
-
telemetryExecutors,
|
|
14
14
|
telemetryAlerts,
|
|
15
|
+
usageAnalytics,
|
|
15
16
|
loadTelemetrySummary,
|
|
16
17
|
loadTelemetryErrors,
|
|
17
18
|
loadTelemetryExecutors,
|
|
18
19
|
loadTelemetryAlerts,
|
|
20
|
+
loadUsageAnalytics,
|
|
19
21
|
scheduleRefresh,
|
|
20
22
|
} from "../modules/state.js";
|
|
21
|
-
import { Card, EmptyState,
|
|
23
|
+
import { Card, EmptyState, Badge } from "../components/shared.js";
|
|
24
|
+
|
|
25
|
+
// ── Colour palettes ──────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
const AGENT_PALETTE = [
|
|
28
|
+
"#6366f1", "#8b5cf6", "#a78bfa", "#c4b5fd", "#e879f9", "#f472b6",
|
|
29
|
+
];
|
|
30
|
+
const SKILL_PALETTE = [
|
|
31
|
+
"#10b981", "#14b8a6", "#f59e0b", "#84cc16", "#2dd4bf", "#fbbf24",
|
|
32
|
+
];
|
|
33
|
+
const MCP_PALETTE = [
|
|
34
|
+
"#f97316", "#fb923c", "#fbbf24", "#f43f5e", "#22d3ee", "#a3e635",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
function paletteColor(palette, index) {
|
|
38
|
+
return palette[index % palette.length];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Formatters ───────────────────────────────────────────────────────────────
|
|
22
42
|
|
|
23
43
|
function formatCount(value) {
|
|
24
44
|
if (value == null) return "–";
|
|
45
|
+
if (value >= 1000) return `${(value / 1000).toFixed(1)}k`;
|
|
25
46
|
return String(value);
|
|
26
47
|
}
|
|
27
48
|
|
|
28
|
-
function
|
|
29
|
-
if (!
|
|
30
|
-
|
|
31
|
-
return
|
|
49
|
+
function formatRelative(isoStr) {
|
|
50
|
+
if (!isoStr) return "never";
|
|
51
|
+
const diff = Date.now() - Date.parse(isoStr);
|
|
52
|
+
if (!Number.isFinite(diff) || diff < 0) return "just now";
|
|
53
|
+
const mins = Math.floor(diff / 60000);
|
|
54
|
+
if (mins < 2) return "just now";
|
|
55
|
+
if (mins < 60) return `${mins} minutes ago`;
|
|
56
|
+
const hrs = Math.floor(mins / 60);
|
|
57
|
+
if (hrs < 24) return `about ${hrs} hour${hrs > 1 ? "s" : ""} ago`;
|
|
58
|
+
const days = Math.floor(hrs / 24);
|
|
59
|
+
return `${days} day${days > 1 ? "s" : ""} ago`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function formatSinceDate(isoStr) {
|
|
63
|
+
if (!isoStr) return null;
|
|
64
|
+
const d = new Date(isoStr);
|
|
65
|
+
if (isNaN(d)) return null;
|
|
66
|
+
return d.toLocaleDateString("en-US", {
|
|
67
|
+
month: "short", day: "numeric", year: "numeric",
|
|
68
|
+
});
|
|
32
69
|
}
|
|
33
70
|
|
|
34
71
|
function severityBadge(sev = "medium") {
|
|
35
|
-
const
|
|
36
|
-
if (
|
|
37
|
-
if (
|
|
72
|
+
const n = String(sev).toLowerCase();
|
|
73
|
+
if (n === "high" || n === "critical") return "danger";
|
|
74
|
+
if (n === "medium") return "warning";
|
|
38
75
|
return "info";
|
|
39
76
|
}
|
|
40
77
|
|
|
78
|
+
// ── SVG Trend Chart ──────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Renders smooth catmull-rom curves for each series in `seriesMap`.
|
|
82
|
+
* `seriesMap` is `{ name: number[] }` aligned with `dates`.
|
|
83
|
+
*/
|
|
84
|
+
function TrendLines({ dates, seriesMap, palette }) {
|
|
85
|
+
if (!dates?.length || !seriesMap) return null;
|
|
86
|
+
const entries = Object.entries(seriesMap);
|
|
87
|
+
if (!entries.length) return null;
|
|
88
|
+
|
|
89
|
+
const W = 400, H = 140;
|
|
90
|
+
const PL = 28, PR = 8, PT = 8, PB = 24;
|
|
91
|
+
const iW = W - PL - PR;
|
|
92
|
+
const iH = H - PT - PB;
|
|
93
|
+
|
|
94
|
+
const allVals = entries.flatMap(([, v]) => v);
|
|
95
|
+
const maxVal = Math.max(...allVals, 1);
|
|
96
|
+
const n = dates.length;
|
|
97
|
+
const xOf = (i) => PL + (n < 2 ? iW / 2 : (i / (n - 1)) * iW);
|
|
98
|
+
const yOf = (v) => PT + iH - (v / maxVal) * iH;
|
|
99
|
+
|
|
100
|
+
function smoothPath(values) {
|
|
101
|
+
if (!values.length) return "";
|
|
102
|
+
const pts = values.map((v, i) => [xOf(i), yOf(v)]);
|
|
103
|
+
if (pts.length === 1) return `M ${pts[0][0]} ${pts[0][1]}`;
|
|
104
|
+
let d = `M ${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;
|
|
105
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
106
|
+
const p0 = pts[Math.max(0, i - 1)];
|
|
107
|
+
const p1 = pts[i];
|
|
108
|
+
const p2 = pts[i + 1];
|
|
109
|
+
const p3 = pts[Math.min(pts.length - 1, i + 2)];
|
|
110
|
+
const cp1x = p1[0] + (p2[0] - p0[0]) / 6;
|
|
111
|
+
const cp1y = p1[1] + (p2[1] - p0[1]) / 6;
|
|
112
|
+
const cp2x = p2[0] - (p3[0] - p1[0]) / 6;
|
|
113
|
+
const cp2y = p2[1] - (p3[1] - p1[1]) / 6;
|
|
114
|
+
d += ` C ${cp1x.toFixed(1)} ${cp1y.toFixed(1)},${cp2x.toFixed(1)} ${cp2y.toFixed(1)},${p2[0].toFixed(1)} ${p2[1].toFixed(1)}`;
|
|
115
|
+
}
|
|
116
|
+
return d;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const ySteps = [0, Math.ceil(maxVal / 2), maxVal];
|
|
120
|
+
const labelIdxs = n <= 3 ? [...Array(n).keys()] : [0, Math.floor(n / 2), n - 1];
|
|
121
|
+
const xLabels = [...new Set(labelIdxs)].map((i) => ({
|
|
122
|
+
x: xOf(i),
|
|
123
|
+
label: (dates[i] || "").slice(5),
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
return html`
|
|
127
|
+
<svg viewBox="0 0 ${W} ${H}" class="analytics-trend-svg" aria-hidden="true">
|
|
128
|
+
${ySteps.map((v) => html`
|
|
129
|
+
<g key=${v}>
|
|
130
|
+
<line x1=${PL} y1=${yOf(v)} x2=${W - PR} y2=${yOf(v)}
|
|
131
|
+
stroke="var(--border)" stroke-width="0.5" stroke-dasharray="3,3"/>
|
|
132
|
+
<text x=${PL - 4} y=${yOf(v) + 4} text-anchor="end"
|
|
133
|
+
font-size="9" fill="var(--text-hint)">${v}</text>
|
|
134
|
+
</g>
|
|
135
|
+
`)}
|
|
136
|
+
${xLabels.map(({ x, label }) => html`
|
|
137
|
+
<text key=${label} x=${x} y=${H - 5} text-anchor="middle"
|
|
138
|
+
font-size="9" fill="var(--text-hint)">${label}</text>
|
|
139
|
+
`)}
|
|
140
|
+
${entries.map(([name, values], i) => html`
|
|
141
|
+
<path key=${name} d=${smoothPath(values)} fill="none"
|
|
142
|
+
stroke=${paletteColor(palette, i)} stroke-width="1.8"
|
|
143
|
+
stroke-linecap="round" stroke-linejoin="round" opacity="0.9"/>
|
|
144
|
+
`)}
|
|
145
|
+
</svg>
|
|
146
|
+
`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function ChartLegend({ label, seriesMap, palette }) {
|
|
150
|
+
const names = Object.keys(seriesMap || {});
|
|
151
|
+
if (!names.length) return null;
|
|
152
|
+
return html`
|
|
153
|
+
<div class="analytics-legend-group">
|
|
154
|
+
<span class="analytics-legend-category">${label}</span>
|
|
155
|
+
${names.map((name, i) => html`
|
|
156
|
+
<span key=${name} class="analytics-legend-item">
|
|
157
|
+
<span class="analytics-legend-dot"
|
|
158
|
+
style="background:${paletteColor(palette, i)}"></span>
|
|
159
|
+
${name}
|
|
160
|
+
</span>
|
|
161
|
+
`)}
|
|
162
|
+
</div>
|
|
163
|
+
`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Bar Chart ────────────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
function TopBarChart({ items, palette, title }) {
|
|
169
|
+
if (!items?.length) {
|
|
170
|
+
return html`<${EmptyState} title="No data yet"
|
|
171
|
+
description="Activity will appear here once tasks run." />`;
|
|
172
|
+
}
|
|
173
|
+
const max = items[0].count || 1;
|
|
174
|
+
return html`
|
|
175
|
+
<ul class="analytics-bar-list" aria-label=${title}>
|
|
176
|
+
${items.map(({ name, count }, i) => html`
|
|
177
|
+
<li key=${name} class="analytics-bar-row">
|
|
178
|
+
<span class="analytics-bar-label" title=${name}>${name}</span>
|
|
179
|
+
<div class="analytics-bar-track">
|
|
180
|
+
<div class="analytics-bar-fill"
|
|
181
|
+
style="width:${Math.max(2, (count / max) * 100).toFixed(1)}%;background:${paletteColor(palette, i)}">
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
<span class="analytics-bar-count">${count}</span>
|
|
185
|
+
</li>
|
|
186
|
+
`)}
|
|
187
|
+
</ul>
|
|
188
|
+
`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── Stat card ────────────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
function AnalyticsStat({ icon, label, value }) {
|
|
194
|
+
return html`
|
|
195
|
+
<div class="analytics-stat">
|
|
196
|
+
<div class="analytics-stat-icon">${icon}</div>
|
|
197
|
+
<div class="analytics-stat-body">
|
|
198
|
+
<div class="analytics-stat-label">${label}</div>
|
|
199
|
+
<div class="analytics-stat-value">${value}</div>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
const PERIODS = [
|
|
208
|
+
{ days: 7, label: "7d" },
|
|
209
|
+
{ days: 30, label: "30d" },
|
|
210
|
+
{ days: 90, label: "90d" },
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
const TREND_TABS = ["agents", "skills", "mcp"];
|
|
214
|
+
|
|
215
|
+
// ── Main exported component ──────────────────────────────────────────────────
|
|
216
|
+
|
|
41
217
|
export function TelemetryTab() {
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
218
|
+
const data = usageAnalytics.value;
|
|
219
|
+
const [period, setPeriod] = useState(30);
|
|
220
|
+
const [trendTab, setTrendTab] = useState("agents");
|
|
221
|
+
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
loadUsageAnalytics(period).catch(() => {});
|
|
224
|
+
}, [period]);
|
|
46
225
|
|
|
47
|
-
const
|
|
226
|
+
const trend = data?.trend;
|
|
48
227
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
228
|
+
const trendSeriesMap = useMemo(() => {
|
|
229
|
+
if (!trend) return null;
|
|
230
|
+
if (trendTab === "agents") return trend.agents || {};
|
|
231
|
+
if (trendTab === "skills") return trend.skills || {};
|
|
232
|
+
return trend.mcpTools || {};
|
|
233
|
+
}, [trend, trendTab]);
|
|
53
234
|
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
235
|
+
const trendPalette =
|
|
236
|
+
trendTab === "agents" ? AGENT_PALETTE
|
|
237
|
+
: trendTab === "skills" ? SKILL_PALETTE
|
|
238
|
+
: MCP_PALETTE;
|
|
239
|
+
|
|
240
|
+
const hasTrend = trend?.dates?.length > 0 &&
|
|
241
|
+
Object.keys(trendSeriesMap || {}).length > 0;
|
|
242
|
+
|
|
243
|
+
const sinceLabel = formatSinceDate(data?.sinceAt);
|
|
58
244
|
|
|
59
245
|
return html`
|
|
60
|
-
<section class="telemetry-tab">
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
246
|
+
<section class="telemetry-tab analytics-tab">
|
|
247
|
+
|
|
248
|
+
<div class="section-header analytics-header">
|
|
249
|
+
<div class="analytics-title-row">
|
|
250
|
+
<h2>Usage Analytics</h2>
|
|
251
|
+
${sinceLabel ? html`
|
|
252
|
+
<span class="analytics-since">Since ${sinceLabel}</span>
|
|
253
|
+
` : null}
|
|
254
|
+
</div>
|
|
255
|
+
<div class="analytics-header-actions">
|
|
256
|
+
<div class="analytics-period-toggle">
|
|
257
|
+
${PERIODS.map(({ days, label }) => html`
|
|
258
|
+
<button key=${days}
|
|
259
|
+
class="analytics-period-btn ${period === days ? "active" : ""}"
|
|
260
|
+
onClick=${() => setPeriod(days)}>
|
|
261
|
+
${label}
|
|
262
|
+
</button>
|
|
263
|
+
`)}
|
|
264
|
+
</div>
|
|
265
|
+
<button class="btn btn-ghost btn-sm" onClick=${() => {
|
|
266
|
+
loadUsageAnalytics(period).catch(() => {});
|
|
66
267
|
loadTelemetrySummary();
|
|
67
268
|
loadTelemetryErrors();
|
|
68
269
|
loadTelemetryExecutors();
|
|
69
270
|
loadTelemetryAlerts();
|
|
70
271
|
scheduleRefresh(4000);
|
|
71
|
-
}}
|
|
72
|
-
>
|
|
73
|
-
Refresh
|
|
74
|
-
</button>
|
|
272
|
+
}}>Refresh</button>
|
|
273
|
+
</div>
|
|
75
274
|
</div>
|
|
76
275
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
</ul>`}
|
|
120
|
-
</${Card}>
|
|
276
|
+
<!-- Summary stat cards -->
|
|
277
|
+
<div class="analytics-stats-row">
|
|
278
|
+
<${AnalyticsStat} icon="⚡" label="Agent Runs"
|
|
279
|
+
value=${data ? formatCount(data.agentRuns) : "–"} />
|
|
280
|
+
<${AnalyticsStat} icon="✦" label="Skill Invocations"
|
|
281
|
+
value=${data ? formatCount(data.skillInvocations) : "–"} />
|
|
282
|
+
<${AnalyticsStat} icon="⚙" label="MCP Tools"
|
|
283
|
+
value=${data ? formatCount(data.mcpToolCalls) : "–"} />
|
|
284
|
+
<${AnalyticsStat} icon="≈" label="Avg / Day"
|
|
285
|
+
value=${data ? formatCount(data.avgPerDay) : "–"} />
|
|
286
|
+
<${AnalyticsStat} icon="🕐" label="Last Active"
|
|
287
|
+
value=${data?.lastActiveAt ? formatRelative(data.lastActiveAt) : "–"} />
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<!-- Activity trend chart -->
|
|
291
|
+
<${Card} title="Activity Trend" class="analytics-trend-card">
|
|
292
|
+
<div class="analytics-trend-header">
|
|
293
|
+
<div class="analytics-trend-tabs">
|
|
294
|
+
${TREND_TABS.map((tab) => html`
|
|
295
|
+
<button key=${tab}
|
|
296
|
+
class="analytics-trend-tab ${trendTab === tab ? "active" : ""}"
|
|
297
|
+
onClick=${() => setTrendTab(tab)}>
|
|
298
|
+
${tab === "agents" ? "Agents" : tab === "skills" ? "Skills" : "MCP Tools"}
|
|
299
|
+
</button>
|
|
300
|
+
`)}
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
${hasTrend ? html`
|
|
305
|
+
<div class="analytics-legend">
|
|
306
|
+
<${ChartLegend}
|
|
307
|
+
label=${trendTab === "agents" ? "AGENTS" : trendTab === "skills" ? "SKILLS" : "MCP TOOLS"}
|
|
308
|
+
seriesMap=${trendSeriesMap}
|
|
309
|
+
palette=${trendPalette}
|
|
310
|
+
/>
|
|
311
|
+
</div>
|
|
312
|
+
<${TrendLines} dates=${trend.dates} seriesMap=${trendSeriesMap} palette=${trendPalette} />
|
|
313
|
+
` : html`
|
|
314
|
+
<${EmptyState} title="No activity data"
|
|
315
|
+
description="Agent runs will appear here once they start." />
|
|
316
|
+
`}
|
|
317
|
+
</${Card}>
|
|
121
318
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
</ul>`}
|
|
319
|
+
<!-- Top-N bar charts row -->
|
|
320
|
+
<div class="analytics-top-grid">
|
|
321
|
+
<${Card} title="Top Agents">
|
|
322
|
+
<${TopBarChart} items=${data?.topAgents || []}
|
|
323
|
+
palette=${AGENT_PALETTE} title="Top Agents" />
|
|
324
|
+
</${Card}>
|
|
325
|
+
<${Card} title="Top Skills">
|
|
326
|
+
<${TopBarChart} items=${data?.topSkills || []}
|
|
327
|
+
palette=${SKILL_PALETTE} title="Top Skills" />
|
|
328
|
+
</${Card}>
|
|
329
|
+
<${Card} title="Top MCP Tools">
|
|
330
|
+
<${TopBarChart} items=${data?.topMcpTools || []}
|
|
331
|
+
palette=${MCP_PALETTE} title="Top MCP Tools" />
|
|
136
332
|
</${Card}>
|
|
137
333
|
</div>
|
|
138
334
|
|
|
335
|
+
<!-- Errors + alerts (preserved from classic telemetry) -->
|
|
336
|
+
<${ClassicTelemetry} />
|
|
337
|
+
</section>
|
|
338
|
+
`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ── Classic error/alert section ──────────────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
function ClassicTelemetry() {
|
|
344
|
+
const errors = telemetryErrors.value || [];
|
|
345
|
+
const alerts = telemetryAlerts.value || [];
|
|
346
|
+
const alertRows = useMemo(() => alerts.slice(-10).reverse(), [alerts]);
|
|
347
|
+
if (!errors.length && !alerts.length) return null;
|
|
348
|
+
|
|
349
|
+
return html`
|
|
350
|
+
<div class="telemetry-grid">
|
|
351
|
+
<${Card} title="Top Errors">
|
|
352
|
+
${errors.length === 0
|
|
353
|
+
? html`<${EmptyState} title="No errors logged"
|
|
354
|
+
description="Errors appear here when failures are detected." />`
|
|
355
|
+
: html`<ul class="telemetry-list">
|
|
356
|
+
${errors.slice(0, 8).map((err) => html`
|
|
357
|
+
<li key=${err.fingerprint}>
|
|
358
|
+
<span class="telemetry-label">${err.fingerprint}</span>
|
|
359
|
+
<span class="telemetry-count">${err.count}</span>
|
|
360
|
+
</li>`)}
|
|
361
|
+
</ul>`}
|
|
362
|
+
</${Card}>
|
|
363
|
+
|
|
139
364
|
<${Card} title="Recent Alerts">
|
|
140
365
|
${alertRows.length === 0
|
|
141
|
-
? html`<${EmptyState}
|
|
142
|
-
|
|
143
|
-
description="Analyzer alerts will show up here."
|
|
144
|
-
/>`
|
|
366
|
+
? html`<${EmptyState} title="No alerts"
|
|
367
|
+
description="Analyzer alerts will show up here." />`
|
|
145
368
|
: html`<ul class="telemetry-alerts">
|
|
146
|
-
${alertRows.map(
|
|
147
|
-
(alert)
|
|
369
|
+
${alertRows.map((alert) => html`
|
|
370
|
+
<li key=${(alert.attempt_id || "") + (alert.type || "")}>
|
|
148
371
|
<div>
|
|
149
372
|
<div class="telemetry-alert-title">
|
|
150
373
|
${alert.type || "alert"}
|
|
151
|
-
<${Badge} tone=${severityBadge(alert.severity)}
|
|
152
|
-
String(alert.severity || "medium").toUpperCase()
|
|
153
|
-
|
|
374
|
+
<${Badge} tone=${severityBadge(alert.severity)}>
|
|
375
|
+
${String(alert.severity || "medium").toUpperCase()}
|
|
376
|
+
</${Badge}>
|
|
154
377
|
</div>
|
|
155
378
|
<div class="telemetry-alert-meta">
|
|
156
379
|
${alert.attempt_id || "unknown"}
|
|
157
380
|
${alert.executor ? html` · ${alert.executor}` : ""}
|
|
158
381
|
</div>
|
|
159
382
|
</div>
|
|
160
|
-
</li
|
|
161
|
-
)}
|
|
383
|
+
</li>`)}
|
|
162
384
|
</ul>`}
|
|
163
385
|
</${Card}>
|
|
164
|
-
</
|
|
386
|
+
</div>
|
|
165
387
|
`;
|
|
166
388
|
}
|