dsh-tool-stats 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/stats.web.js CHANGED
@@ -42,7 +42,17 @@ var zh = {
42
42
  "refresh": "\u5237\u65B0",
43
43
  "loading": "\u6B63\u5728\u52A0\u8F7D\u2026",
44
44
  "failed": "\u52A0\u8F7D\u5931\u8D25",
45
- "retry": "\u91CD\u8BD5"
45
+ "retry": "\u91CD\u8BD5",
46
+ "totalCalls": "\u603B\u8C03\u7528",
47
+ "totalFailures": "\u603B\u5931\u8D25",
48
+ "sessions": "\u4F1A\u8BDD\u6570",
49
+ "avgLatency": "\u5E73\u5747\u5EF6\u8FDF",
50
+ "export": "\u5BFC\u51FA CSV",
51
+ "clear": "\u6E05\u7A7A",
52
+ "confirmClear": "\u786E\u5B9A\u6E05\u7A7A\u6240\u6709\u8BB0\u5F55\uFF1F",
53
+ "errorBreakdown": "\u9519\u8BEF\u5206\u7C7B",
54
+ "recentCalls": "\u6700\u8FD1\u8C03\u7528",
55
+ "sessionsUnused": "\u4F1A\u8BDD\u672A\u7528"
46
56
  };
47
57
  var en = {
48
58
  "nav": "Tool Stats",
@@ -57,105 +67,630 @@ var en = {
57
67
  "refresh": "Refresh",
58
68
  "loading": "Loading\u2026",
59
69
  "failed": "Failed to load",
60
- "retry": "Retry"
70
+ "retry": "Retry",
71
+ "totalCalls": "Total Calls",
72
+ "totalFailures": "Total Failures",
73
+ "sessions": "Sessions",
74
+ "avgLatency": "Avg Latency",
75
+ "export": "Export CSV",
76
+ "clear": "Clear",
77
+ "confirmClear": "Clear all recorded data?",
78
+ "errorBreakdown": "Error Breakdown",
79
+ "recentCalls": "Recent Calls",
80
+ "sessionsUnused": "sessions unused"
61
81
  };
62
82
 
63
83
  // src/client/view.tsx
84
+ var import_react2 = require("react");
85
+
86
+ // src/client/ui.tsx
64
87
  var import_react = require("react");
65
88
  var import_jsx_runtime = require("react/jsx-runtime");
66
- var PANEL_PATH = "/api/stats.panel";
67
- var wrap = { display: "flex", flexDirection: "column", gap: 12, maxWidth: 760, fontFamily: "inherit" };
68
- var head = { display: "flex", alignItems: "baseline", gap: 12, flexWrap: "wrap" };
69
- var muted = { fontSize: 12, opacity: 0.75 };
70
- var table = { borderCollapse: "collapse", width: "100%" };
71
- var th = { textAlign: "left", padding: "4px 10px 4px 0", fontWeight: 600, fontSize: 12, opacity: 0.8, borderBottom: "0.5px solid rgba(128,128,128,0.4)" };
72
- var td = { padding: "6px 10px 6px 0", fontSize: 13, borderBottom: "0.5px solid rgba(128,128,128,0.18)" };
73
- var list = { margin: 0, paddingLeft: 18, fontSize: 13 };
74
- function usePanel() {
75
- const [state, setState] = (0, import_react.useState)({ payload: null, error: null });
89
+ var T = {
90
+ accent: "var(--accent, #4B8BBE)",
91
+ accentHover: "var(--accent-hover, #3a6f9e)",
92
+ error: "var(--error, #e53935)",
93
+ success: "var(--success, #2e7d32)",
94
+ warning: "var(--warning, #ed6c02)",
95
+ bg: "var(--bg-secondary, rgba(128,128,128,0.04))",
96
+ bgHover: "var(--bg-tertiary, rgba(128,128,128,0.08))",
97
+ border: "var(--border, rgba(128,128,128,0.2))",
98
+ text: "var(--text-primary, inherit)",
99
+ muted: "var(--text-secondary, rgba(128,128,128,0.65))",
100
+ surface: "var(--bg-primary, #fff)"
101
+ };
102
+ var injected = false;
103
+ function ensureStyles() {
104
+ if (injected || typeof document === "undefined") return;
105
+ injected = true;
106
+ const el = document.createElement("style");
107
+ el.textContent = `
108
+ @keyframes dsh-fade-in{from{opacity:0}to{opacity:1}}
109
+ @keyframes dsh-slide-up{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
110
+ @keyframes dsh-spin{to{transform:rotate(360deg)}}
111
+ .dsh-modal-overlay{animation:dsh-fade-in .15s ease}
112
+ .dsh-modal-body{animation:dsh-slide-up .2s ease}
113
+ .dsh-toast{animation:dsh-slide-up .2s ease}
114
+ .dsh-spin{animation:dsh-spin .6s linear infinite}
115
+ `;
116
+ document.head.appendChild(el);
117
+ }
118
+ function Modal({
119
+ title,
120
+ onClose,
121
+ children,
122
+ footer,
123
+ width = 520
124
+ }) {
125
+ (0, import_react.useEffect)(() => {
126
+ ensureStyles();
127
+ const handler = (e) => {
128
+ if (e.key === "Escape") onClose();
129
+ };
130
+ document.addEventListener("keydown", handler);
131
+ return () => document.removeEventListener("keydown", handler);
132
+ }, [onClose]);
133
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
134
+ "div",
135
+ {
136
+ className: "dsh-modal-overlay",
137
+ style: {
138
+ position: "fixed",
139
+ inset: 0,
140
+ zIndex: 1e3,
141
+ display: "flex",
142
+ alignItems: "center",
143
+ justifyContent: "center",
144
+ background: "rgba(0,0,0,0.45)",
145
+ backdropFilter: "blur(3px)",
146
+ WebkitBackdropFilter: "blur(3px)"
147
+ },
148
+ onClick: onClose,
149
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
150
+ "div",
151
+ {
152
+ className: "dsh-modal-body",
153
+ style: {
154
+ background: T.surface,
155
+ borderRadius: 14,
156
+ boxShadow: "0 12px 40px rgba(0,0,0,0.25)",
157
+ width: `min(92vw, ${width}px)`,
158
+ maxHeight: "85vh",
159
+ display: "flex",
160
+ flexDirection: "column",
161
+ overflow: "hidden"
162
+ },
163
+ onClick: (e) => e.stopPropagation(),
164
+ children: [
165
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
166
+ "div",
167
+ {
168
+ style: {
169
+ display: "flex",
170
+ alignItems: "center",
171
+ justifyContent: "space-between",
172
+ padding: "16px 22px",
173
+ borderBottom: `1px solid ${T.border}`
174
+ },
175
+ children: [
176
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { style: { margin: 0, fontSize: 15, fontWeight: 600 }, children: title }),
177
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
178
+ "button",
179
+ {
180
+ type: "button",
181
+ onClick: onClose,
182
+ style: {
183
+ background: "none",
184
+ border: "none",
185
+ cursor: "pointer",
186
+ fontSize: 20,
187
+ color: T.muted,
188
+ padding: "0 4px",
189
+ lineHeight: 1,
190
+ borderRadius: 4
191
+ },
192
+ "aria-label": "Close",
193
+ children: "\xD7"
194
+ }
195
+ )
196
+ ]
197
+ }
198
+ ),
199
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { padding: 22, overflow: "auto", flex: 1 }, children }),
200
+ footer !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
201
+ "div",
202
+ {
203
+ style: {
204
+ padding: "14px 22px",
205
+ borderTop: `1px solid ${T.border}`,
206
+ display: "flex",
207
+ justifyContent: "flex-end",
208
+ gap: 10,
209
+ background: T.bg
210
+ },
211
+ children: footer
212
+ }
213
+ )
214
+ ]
215
+ }
216
+ )
217
+ }
218
+ );
219
+ }
220
+ function ConfirmDialog({
221
+ title,
222
+ message,
223
+ confirmLabel,
224
+ cancelLabel = "Cancel",
225
+ onConfirm,
226
+ onClose,
227
+ danger
228
+ }) {
229
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
230
+ Modal,
231
+ {
232
+ title,
233
+ onClose,
234
+ width: 420,
235
+ footer: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
236
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, { variant: "secondary", onClick: onClose, children: cancelLabel }),
237
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
238
+ Button,
239
+ {
240
+ variant: danger === true ? "danger" : "primary",
241
+ onClick: () => {
242
+ onConfirm();
243
+ onClose();
244
+ },
245
+ children: confirmLabel
246
+ }
247
+ )
248
+ ] }),
249
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { margin: 0, fontSize: 14, lineHeight: 1.6 }, children: message })
250
+ }
251
+ );
252
+ }
253
+ function Button({
254
+ variant = "secondary",
255
+ size = "md",
256
+ children,
257
+ style,
258
+ ...rest
259
+ }) {
260
+ const base = {
261
+ cursor: "pointer",
262
+ borderRadius: 7,
263
+ fontWeight: 500,
264
+ transition: "all 0.15s ease",
265
+ border: "none",
266
+ display: "inline-flex",
267
+ alignItems: "center",
268
+ gap: 4,
269
+ fontSize: size === "sm" ? 12 : 13,
270
+ padding: size === "sm" ? "5px 12px" : "8px 18px",
271
+ fontFamily: "inherit"
272
+ };
273
+ const variants = {
274
+ primary: { background: T.accent, color: "#fff" },
275
+ secondary: {
276
+ background: T.bg,
277
+ color: T.text,
278
+ border: `1px solid ${T.border}`
279
+ },
280
+ danger: {
281
+ background: "transparent",
282
+ color: T.error,
283
+ border: `1px solid ${T.error}`
284
+ },
285
+ ghost: { background: "transparent", color: T.muted, border: "none" }
286
+ };
287
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: { ...base, ...variants[variant], ...style }, ...rest, children });
288
+ }
289
+ function Card({
290
+ title,
291
+ icon,
292
+ children,
293
+ actions,
294
+ style,
295
+ padding = 14
296
+ }) {
297
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
298
+ "div",
299
+ {
300
+ style: {
301
+ borderRadius: 10,
302
+ border: `1px solid ${T.border}`,
303
+ background: T.bg,
304
+ overflow: "hidden",
305
+ ...style
306
+ },
307
+ children: [
308
+ (title !== void 0 || actions !== void 0) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
309
+ "div",
310
+ {
311
+ style: {
312
+ display: "flex",
313
+ alignItems: "center",
314
+ justifyContent: "space-between",
315
+ padding: "12px 16px",
316
+ borderBottom: title !== void 0 ? `1px solid ${T.border}` : "none"
317
+ },
318
+ children: [
319
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { fontSize: 13, fontWeight: 600, display: "flex", alignItems: "center", gap: 6 }, children: [
320
+ icon !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: icon }),
321
+ title
322
+ ] }),
323
+ actions
324
+ ]
325
+ }
326
+ ),
327
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { padding }, children })
328
+ ]
329
+ }
330
+ );
331
+ }
332
+ function StatCard({
333
+ value,
334
+ label,
335
+ color,
336
+ unit
337
+ }) {
338
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
339
+ "div",
340
+ {
341
+ style: {
342
+ flex: 1,
343
+ minWidth: 90,
344
+ textAlign: "center",
345
+ padding: "14px 10px",
346
+ borderRadius: 10,
347
+ border: `1px solid ${T.border}`,
348
+ background: T.bg
349
+ },
350
+ children: [
351
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { fontSize: 24, fontWeight: 700, color: color ?? T.text, lineHeight: 1.2 }, children: [
352
+ value,
353
+ unit !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: 13, fontWeight: 500, opacity: 0.7, marginLeft: 2 }, children: unit })
354
+ ] }),
355
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 11, color: T.muted, marginTop: 4 }, children: label })
356
+ ]
357
+ }
358
+ );
359
+ }
360
+ function Badge({
361
+ children,
362
+ color = "default"
363
+ }) {
364
+ const colors = {
365
+ default: { bg: "rgba(128,128,128,0.12)", fg: T.muted },
366
+ success: { bg: "rgba(46,125,50,0.12)", fg: T.success },
367
+ error: { bg: "rgba(229,57,53,0.12)", fg: T.error },
368
+ warning: { bg: "rgba(245,124,0,0.12)", fg: T.warning },
369
+ info: { bg: "rgba(75,139,190,0.12)", fg: T.accent }
370
+ };
371
+ const c = colors[color];
372
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
373
+ "span",
374
+ {
375
+ style: {
376
+ display: "inline-block",
377
+ padding: "2px 9px",
378
+ borderRadius: 10,
379
+ fontSize: 11,
380
+ fontWeight: 600,
381
+ background: c.bg,
382
+ color: c.fg
383
+ },
384
+ children
385
+ }
386
+ );
387
+ }
388
+ var ToastContext = (0, import_react.createContext)(
389
+ () => {
390
+ }
391
+ );
392
+ function ToastProvider({ children }) {
393
+ const [toasts, setToasts] = (0, import_react.useState)([]);
394
+ const show = (0, import_react.useCallback)((type, message) => {
395
+ const id = Date.now() + Math.random();
396
+ setToasts((t) => [...t, { id, type, message }]);
397
+ setTimeout(() => setToasts((t) => t.filter((x) => x.id !== id)), 3500);
398
+ }, []);
399
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ToastContext.Provider, { value: show, children: [
400
+ children,
401
+ toasts.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
402
+ "div",
403
+ {
404
+ style: {
405
+ position: "fixed",
406
+ bottom: 24,
407
+ right: 24,
408
+ zIndex: 2e3,
409
+ display: "flex",
410
+ flexDirection: "column",
411
+ gap: 8
412
+ },
413
+ children: toasts.map((t) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
414
+ "div",
415
+ {
416
+ className: "dsh-toast",
417
+ style: {
418
+ padding: "12px 18px",
419
+ borderRadius: 8,
420
+ fontSize: 13,
421
+ fontWeight: 500,
422
+ boxShadow: "0 6px 20px rgba(0,0,0,0.2)",
423
+ color: "#fff",
424
+ background: t.type === "success" ? T.success : t.type === "error" ? T.error : T.accent,
425
+ maxWidth: 360
426
+ },
427
+ children: t.message
428
+ },
429
+ t.id
430
+ ))
431
+ }
432
+ )
433
+ ] });
434
+ }
435
+ function useToast() {
436
+ return (0, import_react.useContext)(ToastContext);
437
+ }
438
+ function Spinner({ size = 16 }) {
439
+ ensureStyles();
440
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
441
+ "span",
442
+ {
443
+ className: "dsh-spin",
444
+ style: {
445
+ display: "inline-block",
446
+ width: size,
447
+ height: size,
448
+ border: `2px solid ${T.border}`,
449
+ borderTopColor: T.accent,
450
+ borderRadius: "50%"
451
+ }
452
+ }
453
+ );
454
+ }
455
+ function EmptyState({
456
+ icon,
457
+ message
458
+ }) {
459
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
460
+ "div",
461
+ {
462
+ style: {
463
+ textAlign: "center",
464
+ padding: "36px 16px",
465
+ color: T.muted
466
+ },
467
+ children: [
468
+ icon !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 36, marginBottom: 10, opacity: 0.4 }, children: icon }),
469
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 13 }, children: message })
470
+ ]
471
+ }
472
+ );
473
+ }
474
+ function SectionTitle({
475
+ icon,
476
+ children,
477
+ actions
478
+ }) {
479
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
480
+ "div",
481
+ {
482
+ style: {
483
+ display: "flex",
484
+ alignItems: "center",
485
+ justifyContent: "space-between",
486
+ marginTop: 6,
487
+ marginBottom: 8
488
+ },
489
+ children: [
490
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
491
+ "span",
492
+ {
493
+ style: {
494
+ fontSize: 14,
495
+ fontWeight: 600,
496
+ display: "flex",
497
+ alignItems: "center",
498
+ gap: 6
499
+ },
500
+ children: [
501
+ icon !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: icon }),
502
+ children
503
+ ]
504
+ }
505
+ ),
506
+ actions
507
+ ]
508
+ }
509
+ );
510
+ }
511
+ var tableStyles = {
512
+ table: {
513
+ borderCollapse: "collapse",
514
+ width: "100%"
515
+ },
516
+ th: {
517
+ textAlign: "left",
518
+ padding: "8px 12px 8px 0",
519
+ fontWeight: 600,
520
+ fontSize: 12,
521
+ color: T.muted,
522
+ borderBottom: `1px solid ${T.border}`
523
+ },
524
+ td: {
525
+ padding: "8px 12px 8px 0",
526
+ fontSize: 13,
527
+ borderBottom: "1px solid rgba(128,128,128,0.08)"
528
+ },
529
+ clickRow: { cursor: "pointer" }
530
+ };
531
+ function usePanel(path) {
532
+ const [payload, setPayload] = (0, import_react.useState)(null);
533
+ const [error, setError] = (0, import_react.useState)(null);
76
534
  const [tick, setTick] = (0, import_react.useState)(0);
77
- const reload = (0, import_react.useCallback)(() => setTick((value) => value + 1), []);
535
+ const reload = (0, import_react.useCallback)(() => setTick((v) => v + 1), []);
78
536
  (0, import_react.useEffect)(() => {
79
- const controller = new AbortController();
80
- setState((previous) => ({ ...previous, error: null }));
81
- fetch(PANEL_PATH, { signal: controller.signal }).then(async (response) => {
82
- if (!response.ok) throw new Error(String(response.status));
83
- return response.json();
84
- }).then((payload) => {
85
- if (!controller.signal.aborted) setState({ payload, error: null });
86
- }).catch((cause) => {
87
- if (controller.signal.aborted) return;
88
- setState({ payload: null, error: cause instanceof Error ? cause.message : String(cause) });
537
+ const c = new AbortController();
538
+ setError(null);
539
+ fetch(path, { signal: c.signal }).then(async (r) => {
540
+ if (!r.ok) throw new Error(String(r.status));
541
+ return r.json();
542
+ }).then((p) => {
543
+ if (!c.signal.aborted) setPayload(p);
544
+ }).catch((e) => {
545
+ if (!c.signal.aborted) setPayload(null), setError(e instanceof Error ? e.message : String(e));
89
546
  });
90
- return () => controller.abort();
91
- }, [tick]);
92
- return { ...state, reload };
547
+ return () => c.abort();
548
+ }, [tick, path]);
549
+ return (0, import_react.useMemo)(() => ({ payload, error, reload }), [payload, error, reload]);
93
550
  }
94
- function ToolStatsPanel({ t }) {
95
- const { payload, error, reload } = usePanel();
96
- const header = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", { style: head, children: [
97
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { style: { fontSize: 13 }, children: t("title") }),
98
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { flex: 1 } }),
99
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: reload, style: { fontSize: 12 }, children: t("refresh") })
100
- ] });
101
- if (error !== null) {
102
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: wrap, children: [
103
- header,
104
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { role: "alert", style: { margin: 0, fontSize: 13 }, children: [
105
- t("failed"),
106
- ": ",
107
- error
551
+
552
+ // src/client/view.tsx
553
+ var import_jsx_runtime2 = require("react/jsx-runtime");
554
+ var PANEL_PATH = "/api/stats.panel";
555
+ var DETAIL_PATH = "/api/stats.detail";
556
+ var EXPORT_PATH = "/api/stats.export";
557
+ var CLEAR_PATH = "/api/stats.clear";
558
+ function ToolModal({ tool, t, onClose }) {
559
+ const [detail, setDetail] = (0, import_react2.useState)(null);
560
+ const [loading, setLoading] = (0, import_react2.useState)(true);
561
+ (0, import_react2.useCallback)(() => {
562
+ setLoading(true);
563
+ fetch(`${DETAIL_PATH}?tool=${encodeURIComponent(tool)}`).then(async (r) => {
564
+ if (r.ok) setDetail(await r.json());
565
+ }).finally(() => setLoading(false));
566
+ }, [tool])();
567
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Modal, { title: `${t("tool")}: ${tool}`, onClose, width: 600, children: loading ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { display: "flex", justifyContent: "center", padding: 20 }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Spinner, { size: 24 }) }) : detail === null ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(EmptyState, { message: t("notFound") }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
568
+ detail.errorBreakdown.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Card, { title: t("errorBreakdown"), children: detail.errorBreakdown.map((e, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: 8, alignItems: "center", marginBottom: 4 }, children: [
569
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Badge, { color: "error", children: [
570
+ "\xD7",
571
+ e.count
108
572
  ] }),
109
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: reload, style: { alignSelf: "flex-start", fontSize: 12 }, children: t("retry") })
110
- ] });
111
- }
112
- if (payload === null) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: muted, "aria-live": "polite", children: t("loading") });
113
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: wrap, children: [
573
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { fontSize: 12 }, children: e.message })
574
+ ] }, i)) }),
575
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Card, { title: t("recentCalls"), children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("table", { style: tableStyles.table, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("tbody", { children: detail.recentCalls.slice(0, 15).map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tr", { children: [
576
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { style: tableStyles.td, children: c.success ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Badge, { color: "success", children: "\u2713" }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Badge, { color: "error", children: "\u2717" }) }),
577
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("td", { style: tableStyles.td, children: [
578
+ c.latencyMs,
579
+ "ms"
580
+ ] }),
581
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { style: tableStyles.td, children: new Date(c.timestamp).toLocaleTimeString() }),
582
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { style: { ...tableStyles.td, fontSize: 10, opacity: 0.6 }, children: c.errorMessage?.slice(0, 60) })
583
+ ] }, i)) }) }) })
584
+ ] }) });
585
+ }
586
+ function ToolStatsPanelInner({ t }) {
587
+ const { payload, error, reload } = usePanel(PANEL_PATH);
588
+ const toast = useToast();
589
+ const [toolModal, setToolModal] = (0, import_react2.useState)(null);
590
+ const [confirmClear, setConfirmClear] = (0, import_react2.useState)(false);
591
+ const handleClear = (0, import_react2.useCallback)(async () => {
592
+ const r = await fetch(CLEAR_PATH, { method: "POST" });
593
+ if (r.ok) {
594
+ toast("success", t("cleared"));
595
+ reload();
596
+ }
597
+ }, [t, toast, reload]);
598
+ const header = /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("header", { style: { display: "flex", alignItems: "center", gap: 12, marginBottom: 4 }, children: [
599
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("strong", { style: { fontSize: 15 }, children: [
600
+ "\u{1F527} ",
601
+ t("title")
602
+ ] }),
603
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { flex: 1 } }),
604
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("a", { href: EXPORT_PATH, download: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Button, { variant: "secondary", size: "sm", children: t("export") }) }),
605
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Button, { variant: "danger", size: "sm", onClick: () => setConfirmClear(true), children: t("clear") }),
606
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Button, { variant: "secondary", size: "sm", onClick: reload, children: t("refresh") })
607
+ ] });
608
+ if (error !== null) return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { maxWidth: 820 }, children: [
114
609
  header,
115
- payload.tools.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { margin: 0, fontSize: 13, opacity: 0.8 }, children: t("empty") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("table", { style: table, children: [
116
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", { children: [
117
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: th, children: t("tool") }),
118
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: th, children: t("calls") }),
119
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: th, children: t("failRate") }),
120
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: th, children: "p50" }),
121
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: th, children: "p95" })
122
- ] }) }),
123
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { children: payload.tools.map((row) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", { children: [
124
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { style: td, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { style: { fontSize: 11 }, children: row.tool }) }),
125
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { style: td, children: row.invocations }),
126
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("td", { style: td, children: [
127
- Math.round(row.failureRate * 100),
128
- "%"
129
- ] }),
130
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("td", { style: td, children: [
131
- row.p50Latency,
132
- "ms"
133
- ] }),
134
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("td", { style: td, children: [
135
- row.p95Latency,
136
- "ms"
137
- ] })
138
- ] }, row.tool)) })
610
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Card, { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { role: "alert", style: { margin: 0, fontSize: 13, color: "var(--error, #e53935)" }, children: [
611
+ t("failed"),
612
+ ": ",
613
+ error
614
+ ] }) })
615
+ ] });
616
+ if (payload === null) return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { maxWidth: 820 }, children: [
617
+ header,
618
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { display: "flex", justifyContent: "center", padding: 40 }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Spinner, { size: 28 }) })
619
+ ] });
620
+ const o = payload.overview;
621
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 14, maxWidth: 820 }, children: [
622
+ header,
623
+ payload.alerts.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: payload.alerts.map((a, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Card, { style: { borderColor: a.level === "error" ? "var(--error, #e53935)" : "var(--warning, #ed6c02)" }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [
624
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Badge, { color: a.level === "error" ? "error" : "warning", children: a.level === "error" ? "\u{1F534}" : "\u{1F7E1}" }),
625
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("strong", { children: a.tool }),
626
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { fontSize: 12 }, children: a.message })
627
+ ] }) }, i)) }),
628
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: 10, flexWrap: "wrap" }, children: [
629
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(StatCard, { value: o.totalCalls, label: t("totalCalls") }),
630
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(StatCard, { value: o.totalFailures, label: t("totalFailures"), color: o.totalFailures > 0 ? "var(--error, #e53935)" : void 0 }),
631
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(StatCard, { value: `${Math.round(o.overallFailureRate * 100)}%`, label: t("failRate") }),
632
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(StatCard, { value: o.activeSessions, label: t("sessions") }),
633
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(StatCard, { value: o.avgLatencyMs, unit: "ms", label: t("avgLatency") })
634
+ ] }),
635
+ payload.tools.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(EmptyState, { icon: "\u{1F4ED}", message: t("empty") }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
636
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SectionTitle, { icon: "\u{1F4CA}", children: t("tools") }),
637
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Card, { padding: 0, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("table", { style: tableStyles.table, children: [
638
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tr", { children: [
639
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { style: tableStyles.th, children: t("tool") }),
640
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { style: tableStyles.th, children: t("calls") }),
641
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { style: tableStyles.th, children: t("failRate") }),
642
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { style: tableStyles.th, children: "p50" }),
643
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { style: tableStyles.th, children: "p95" })
644
+ ] }) }),
645
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("tbody", { children: payload.tools.map((row) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tr", { style: tableStyles.clickRow, onClick: () => setToolModal(row.tool), children: [
646
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { style: tableStyles.td, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("code", { style: { fontSize: 11 }, children: row.tool }) }),
647
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { style: tableStyles.td, children: row.invocations }),
648
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("td", { style: { ...tableStyles.td, color: row.failureRate > 0.3 ? "var(--error, #e53935)" : void 0 }, children: [
649
+ Math.round(row.failureRate * 100),
650
+ "%"
651
+ ] }),
652
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("td", { style: tableStyles.td, children: [
653
+ row.p50Latency,
654
+ "ms"
655
+ ] }),
656
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("td", { style: tableStyles.td, children: [
657
+ row.p95Latency,
658
+ "ms"
659
+ ] })
660
+ ] }, row.tool)) })
661
+ ] }) })
139
662
  ] }),
140
- payload.deadTools.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
141
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { style: { fontSize: 13 }, children: t("deadTools") }),
142
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { style: list, children: payload.deadTools.map((row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { style: { fontSize: 11 }, children: row.tool }) }, row.tool)) })
663
+ payload.deadTools.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
664
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SectionTitle, { icon: "\u{1F480}", children: t("deadTools") }),
665
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Card, { children: payload.deadTools.map((row) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { marginBottom: 4 }, children: [
666
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("code", { style: { fontSize: 11 }, children: row.tool }),
667
+ " \u2014 ",
668
+ row.sessionsSinceLastUse,
669
+ " ",
670
+ t("sessionsUnused")
671
+ ] }, row.tool)) })
143
672
  ] }),
144
- payload.failingTools.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
145
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { style: { fontSize: 13 }, children: t("failingTools") }),
146
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { style: list, children: payload.failingTools.map((row) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: [
147
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { style: { fontSize: 11 }, children: row.tool }),
673
+ payload.failingTools.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
674
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SectionTitle, { icon: "\u26A0\uFE0F", children: t("failingTools") }),
675
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Card, { children: payload.failingTools.map((row) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { marginBottom: 4 }, children: [
676
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("code", { style: { fontSize: 11 }, children: row.tool }),
148
677
  " \xB7 ",
149
678
  Math.round(row.failureRate * 100),
150
- "%"
679
+ "%",
680
+ row.lastError ? ` \u2014 ${row.lastError.slice(0, 60)}` : ""
151
681
  ] }, row.tool)) })
152
682
  ] }),
153
- payload.recommendations.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
154
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { style: { fontSize: 13 }, children: t("recommendations") }),
155
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { style: list, children: payload.recommendations.map((row, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: row.message }, index)) })
156
- ] })
683
+ payload.recommendations.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
684
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SectionTitle, { icon: "\u{1F4A1}", children: t("recommendations") }),
685
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Card, { children: payload.recommendations.map((row, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { marginBottom: 4 }, children: row.message }, i)) })
686
+ ] }),
687
+ toolModal !== null && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ToolModal, { tool: toolModal, t, onClose: () => setToolModal(null) }),
688
+ confirmClear && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ConfirmDialog, { title: t("clear"), message: t("confirmClear"), confirmLabel: t("clear"), danger: true, onConfirm: handleClear, onClose: () => setConfirmClear(false) })
157
689
  ] });
158
690
  }
691
+ function ToolStatsPanel({ t }) {
692
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ToastProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ToolStatsPanelInner, { t }) });
693
+ }
159
694
 
160
695
  // src/client/index.tsx
161
696
  var inject = ["slots", "locale"];