dsh-tool-stats 0.3.0 → 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/client/ui.d.ts +124 -0
- package/lib/client/ui.js +388 -0
- package/lib/client/view.d.ts +3 -17
- package/lib/client/view.js +34 -82
- package/lib/stats.web.js +575 -183
- package/lib/stats.web.js.map +4 -4
- package/package.json +1 -1
package/lib/client/view.js
CHANGED
|
@@ -1,96 +1,48 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* Pure rendering half of the toolStats page.
|
|
4
|
-
*
|
|
5
|
-
* Separate from `index.tsx` so a static render can assert in Node what the page draws —
|
|
6
|
-
* the shipped bundle is a loader factory only a browser can run. Every user-visible
|
|
7
|
-
* string comes from the `t` seat the renderer binds from this plugin's namespace, so the
|
|
8
|
-
* page follows the UI language; no copy is hardcoded here.
|
|
9
|
-
*
|
|
3
|
+
* Pure rendering half of the toolStats page. Uses shared UI kit.
|
|
10
4
|
* @module client/view
|
|
11
5
|
*/
|
|
12
|
-
import { useCallback,
|
|
6
|
+
import { useCallback, useState } from 'react';
|
|
7
|
+
import { Badge, Button, Card, ConfirmDialog, EmptyState, Modal, SectionTitle, Spinner, StatCard, ToastProvider, tableStyles, usePanel, useToast, } from './ui.js';
|
|
13
8
|
const PANEL_PATH = '/api/stats.panel';
|
|
14
9
|
const DETAIL_PATH = '/api/stats.detail';
|
|
15
10
|
const EXPORT_PATH = '/api/stats.export';
|
|
16
11
|
const CLEAR_PATH = '/api/stats.clear';
|
|
17
|
-
|
|
18
|
-
const head = { display: 'flex', alignItems: 'baseline', gap: 12, flexWrap: 'wrap' };
|
|
19
|
-
const muted = { fontSize: 12, opacity: 0.75 };
|
|
20
|
-
const table = { borderCollapse: 'collapse', width: '100%' };
|
|
21
|
-
const 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)' };
|
|
22
|
-
const td = { padding: '6px 10px 6px 0', fontSize: 13, borderBottom: '0.5px solid rgba(128,128,128,0.18)' };
|
|
23
|
-
const list = { margin: 0, paddingLeft: 18, fontSize: 13 };
|
|
24
|
-
const card = { padding: '8px 12px', borderRadius: 6, border: '1px solid rgba(128,128,128,0.2)', fontSize: 13 };
|
|
25
|
-
const statBox = { ...card, flex: 1, minWidth: 100, textAlign: 'center' };
|
|
26
|
-
const statRow = { display: 'flex', gap: 12, flexWrap: 'wrap' };
|
|
27
|
-
const btn = { fontSize: 12, cursor: 'pointer', padding: '3px 10px', borderRadius: 4, border: '0.5px solid rgba(128,128,128,0.4)' };
|
|
28
|
-
const dangerBtn = { ...btn, color: '#e55', borderColor: '#e55' };
|
|
29
|
-
const alertCard = { ...card, display: 'flex', alignItems: 'center', gap: 8, marginTop: 4 };
|
|
30
|
-
const preBlock = { whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: 11, maxHeight: 150, overflow: 'auto', padding: 6, borderRadius: 4, background: 'rgba(128,128,128,0.06)' };
|
|
31
|
-
const clickRow = { cursor: 'pointer' };
|
|
32
|
-
export function usePanel() {
|
|
33
|
-
const [state, setState] = useState({ payload: null, error: null });
|
|
34
|
-
const [tick, setTick] = useState(0);
|
|
35
|
-
const reload = useCallback(() => setTick((v) => v + 1), []);
|
|
36
|
-
useEffect(() => {
|
|
37
|
-
const c = new AbortController();
|
|
38
|
-
setState((p) => ({ ...p, error: null }));
|
|
39
|
-
fetch(PANEL_PATH, { signal: c.signal })
|
|
40
|
-
.then(async (r) => { if (!r.ok)
|
|
41
|
-
throw new Error(String(r.status)); return r.json(); })
|
|
42
|
-
.then((payload) => { if (!c.signal.aborted)
|
|
43
|
-
setState({ payload, error: null }); })
|
|
44
|
-
.catch((e) => { if (!c.signal.aborted)
|
|
45
|
-
setState({ payload: null, error: e instanceof Error ? e.message : String(e) }); });
|
|
46
|
-
return () => c.abort();
|
|
47
|
-
}, [tick]);
|
|
48
|
-
return { ...state, reload };
|
|
49
|
-
}
|
|
50
|
-
// --- Alert banner ---
|
|
51
|
-
function AlertBanner({ alerts, t }) {
|
|
52
|
-
if (alerts.length === 0)
|
|
53
|
-
return null;
|
|
54
|
-
return (_jsx("div", { children: alerts.map((a, i) => (_jsxs("div", { style: { ...alertCard, borderColor: a.level === 'error' ? '#e55' : '#ed0', background: a.level === 'error' ? 'rgba(255,80,80,0.06)' : 'rgba(255,200,0,0.06)' }, children: [_jsx("span", { style: { fontSize: 16 }, children: a.level === 'error' ? '🔴' : '🟡' }), _jsxs("span", { children: [_jsx("strong", { children: a.tool }), ": ", a.message] })] }, i))) }));
|
|
55
|
-
}
|
|
56
|
-
// --- Overview stats ---
|
|
57
|
-
function Overview({ payload, t }) {
|
|
58
|
-
const o = payload.overview;
|
|
59
|
-
return (_jsxs("div", { style: statRow, children: [_jsxs("div", { style: statBox, children: [_jsx("div", { style: { fontSize: 18, fontWeight: 700 }, children: o.totalCalls }), _jsx("div", { style: muted, children: t('totalCalls') })] }), _jsxs("div", { style: statBox, children: [_jsx("div", { style: { fontSize: 18, fontWeight: 700, color: o.totalFailures > 0 ? '#e55' : undefined }, children: o.totalFailures }), _jsx("div", { style: muted, children: t('totalFailures') })] }), _jsxs("div", { style: statBox, children: [_jsxs("div", { style: { fontSize: 18, fontWeight: 700 }, children: [Math.round(o.overallFailureRate * 100), "%"] }), _jsx("div", { style: muted, children: t('failRate') })] }), _jsxs("div", { style: statBox, children: [_jsx("div", { style: { fontSize: 18, fontWeight: 700 }, children: o.activeSessions }), _jsx("div", { style: muted, children: t('sessions') })] }), _jsxs("div", { style: statBox, children: [_jsxs("div", { style: { fontSize: 18, fontWeight: 700 }, children: [o.avgLatencyMs, "ms"] }), _jsx("div", { style: muted, children: t('avgLatency') })] })] }));
|
|
60
|
-
}
|
|
61
|
-
// --- Tool detail expansion ---
|
|
62
|
-
function ToolDetailRow({ tool, t }) {
|
|
12
|
+
function ToolModal({ tool, t, onClose }) {
|
|
63
13
|
const [detail, setDetail] = useState(null);
|
|
64
|
-
const [loading, setLoading] = useState(
|
|
65
|
-
|
|
66
|
-
if (detail) {
|
|
67
|
-
setDetail(null);
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
14
|
+
const [loading, setLoading] = useState(true);
|
|
15
|
+
useCallback(() => {
|
|
70
16
|
setLoading(true);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
17
|
+
fetch(`${DETAIL_PATH}?tool=${encodeURIComponent(tool)}`)
|
|
18
|
+
.then(async (r) => { if (r.ok)
|
|
19
|
+
setDetail(await r.json()); })
|
|
20
|
+
.finally(() => setLoading(false));
|
|
21
|
+
}, [tool])();
|
|
22
|
+
return (_jsx(Modal, { title: `${t('tool')}: ${tool}`, onClose: onClose, width: 600, children: loading ? _jsx("div", { style: { display: 'flex', justifyContent: 'center', padding: 20 }, children: _jsx(Spinner, { size: 24 }) })
|
|
23
|
+
: detail === null ? _jsx(EmptyState, { message: t('notFound') })
|
|
24
|
+
: (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 12 }, children: [detail.errorBreakdown.length > 0 && (_jsx(Card, { title: t('errorBreakdown'), children: detail.errorBreakdown.map((e, i) => (_jsxs("div", { style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 4 }, children: [_jsxs(Badge, { color: "error", children: ["\u00D7", e.count] }), _jsx("span", { style: { fontSize: 12 }, children: e.message })] }, i))) })), _jsx(Card, { title: t('recentCalls'), children: _jsx("table", { style: tableStyles.table, children: _jsx("tbody", { children: detail.recentCalls.slice(0, 15).map((c, i) => (_jsxs("tr", { children: [_jsx("td", { style: tableStyles.td, children: c.success ? _jsx(Badge, { color: "success", children: "\u2713" }) : _jsx(Badge, { color: "error", children: "\u2717" }) }), _jsxs("td", { style: tableStyles.td, children: [c.latencyMs, "ms"] }), _jsx("td", { style: tableStyles.td, children: new Date(c.timestamp).toLocaleTimeString() }), _jsx("td", { style: { ...tableStyles.td, fontSize: 10, opacity: 0.6 }, children: c.errorMessage?.slice(0, 60) })] }, i))) }) }) })] })) }));
|
|
25
|
+
}
|
|
26
|
+
function ToolStatsPanelInner({ t }) {
|
|
27
|
+
const { payload, error, reload } = usePanel(PANEL_PATH);
|
|
28
|
+
const toast = useToast();
|
|
29
|
+
const [toolModal, setToolModal] = useState(null);
|
|
30
|
+
const [confirmClear, setConfirmClear] = useState(false);
|
|
31
|
+
const handleClear = useCallback(async () => {
|
|
32
|
+
const r = await fetch(CLEAR_PATH, { method: 'POST' });
|
|
33
|
+
if (r.ok) {
|
|
34
|
+
toast('success', t('cleared'));
|
|
35
|
+
reload();
|
|
78
36
|
}
|
|
79
|
-
}, [
|
|
80
|
-
|
|
37
|
+
}, [t, toast, reload]);
|
|
38
|
+
const header = (_jsxs("header", { style: { display: 'flex', alignItems: 'center', gap: 12, marginBottom: 4 }, children: [_jsxs("strong", { style: { fontSize: 15 }, children: ["\uD83D\uDD27 ", t('title')] }), _jsx("span", { style: { flex: 1 } }), _jsx("a", { href: EXPORT_PATH, download: true, children: _jsx(Button, { variant: "secondary", size: "sm", children: t('export') }) }), _jsx(Button, { variant: "danger", size: "sm", onClick: () => setConfirmClear(true), children: t('clear') }), _jsx(Button, { variant: "secondary", size: "sm", onClick: reload, children: t('refresh') })] }));
|
|
39
|
+
if (error !== null)
|
|
40
|
+
return _jsxs("div", { style: { maxWidth: 820 }, children: [header, _jsx(Card, { children: _jsxs("p", { role: "alert", style: { margin: 0, fontSize: 13, color: 'var(--error, #e53935)' }, children: [t('failed'), ": ", error] }) })] });
|
|
41
|
+
if (payload === null)
|
|
42
|
+
return _jsxs("div", { style: { maxWidth: 820 }, children: [header, _jsx("div", { style: { display: 'flex', justifyContent: 'center', padding: 40 }, children: _jsx(Spinner, { size: 28 }) })] });
|
|
43
|
+
const o = payload.overview;
|
|
44
|
+
return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 14, maxWidth: 820 }, children: [header, payload.alerts.length > 0 && (_jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 6 }, children: payload.alerts.map((a, i) => (_jsx(Card, { style: { borderColor: a.level === 'error' ? 'var(--error, #e53935)' : 'var(--warning, #ed6c02)' }, children: _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8 }, children: [_jsx(Badge, { color: a.level === 'error' ? 'error' : 'warning', children: a.level === 'error' ? '🔴' : '🟡' }), _jsx("strong", { children: a.tool }), _jsx("span", { style: { fontSize: 12 }, children: a.message })] }) }, i))) })), _jsxs("div", { style: { display: 'flex', gap: 10, flexWrap: 'wrap' }, children: [_jsx(StatCard, { value: o.totalCalls, label: t('totalCalls') }), _jsx(StatCard, { value: o.totalFailures, label: t('totalFailures'), color: o.totalFailures > 0 ? 'var(--error, #e53935)' : undefined }), _jsx(StatCard, { value: `${Math.round(o.overallFailureRate * 100)}%`, label: t('failRate') }), _jsx(StatCard, { value: o.activeSessions, label: t('sessions') }), _jsx(StatCard, { value: o.avgLatencyMs, unit: "ms", label: t('avgLatency') })] }), payload.tools.length === 0 ? _jsx(EmptyState, { icon: "\uD83D\uDCED", message: t('empty') }) : (_jsxs(_Fragment, { children: [_jsx(SectionTitle, { icon: "\uD83D\uDCCA", children: t('tools') }), _jsx(Card, { padding: 0, children: _jsxs("table", { style: tableStyles.table, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { style: tableStyles.th, children: t('tool') }), _jsx("th", { style: tableStyles.th, children: t('calls') }), _jsx("th", { style: tableStyles.th, children: t('failRate') }), _jsx("th", { style: tableStyles.th, children: "p50" }), _jsx("th", { style: tableStyles.th, children: "p95" })] }) }), _jsx("tbody", { children: payload.tools.map((row) => (_jsxs("tr", { style: tableStyles.clickRow, onClick: () => setToolModal(row.tool), children: [_jsx("td", { style: tableStyles.td, children: _jsx("code", { style: { fontSize: 11 }, children: row.tool }) }), _jsx("td", { style: tableStyles.td, children: row.invocations }), _jsxs("td", { style: { ...tableStyles.td, color: row.failureRate > 0.3 ? 'var(--error, #e53935)' : undefined }, children: [Math.round(row.failureRate * 100), "%"] }), _jsxs("td", { style: tableStyles.td, children: [row.p50Latency, "ms"] }), _jsxs("td", { style: tableStyles.td, children: [row.p95Latency, "ms"] })] }, row.tool))) })] }) })] })), payload.deadTools.length > 0 && (_jsxs(_Fragment, { children: [_jsx(SectionTitle, { icon: "\uD83D\uDC80", children: t('deadTools') }), _jsx(Card, { children: payload.deadTools.map((row) => (_jsxs("div", { style: { marginBottom: 4 }, children: [_jsx("code", { style: { fontSize: 11 }, children: row.tool }), " \u2014 ", row.sessionsSinceLastUse, " ", t('sessionsUnused')] }, row.tool))) })] })), payload.failingTools.length > 0 && (_jsxs(_Fragment, { children: [_jsx(SectionTitle, { icon: "\u26A0\uFE0F", children: t('failingTools') }), _jsx(Card, { children: payload.failingTools.map((row) => (_jsxs("div", { style: { marginBottom: 4 }, children: [_jsx("code", { style: { fontSize: 11 }, children: row.tool }), " \u00B7 ", Math.round(row.failureRate * 100), "%", row.lastError ? ` — ${row.lastError.slice(0, 60)}` : ''] }, row.tool))) })] })), payload.recommendations.length > 0 && (_jsxs(_Fragment, { children: [_jsx(SectionTitle, { icon: "\uD83D\uDCA1", children: t('recommendations') }), _jsx(Card, { children: payload.recommendations.map((row, i) => _jsx("div", { style: { marginBottom: 4 }, children: row.message }, i)) })] })), toolModal !== null && _jsx(ToolModal, { tool: toolModal, t: t, onClose: () => setToolModal(null) }), confirmClear && _jsx(ConfirmDialog, { title: t('clear'), message: t('confirmClear'), confirmLabel: t('clear'), danger: true, onConfirm: handleClear, onClose: () => setConfirmClear(false) })] }));
|
|
81
45
|
}
|
|
82
46
|
export function ToolStatsPanel({ t }) {
|
|
83
|
-
|
|
84
|
-
const handleClear = useCallback(() => {
|
|
85
|
-
if (!confirm(t('confirmClear')))
|
|
86
|
-
return;
|
|
87
|
-
fetch(CLEAR_PATH, { method: 'POST' }).then(() => reload()).catch(() => { });
|
|
88
|
-
}, [t, reload]);
|
|
89
|
-
const header = (_jsxs("header", { style: head, children: [_jsx("strong", { style: { fontSize: 13 }, children: t('title') }), _jsx("span", { style: { flex: 1 } }), _jsx("a", { href: EXPORT_PATH, download: true, style: { ...btn, textDecoration: 'none', color: 'inherit' }, children: t('export') }), _jsx("button", { type: "button", style: dangerBtn, onClick: handleClear, children: t('clear') }), _jsx("button", { type: "button", style: btn, onClick: reload, children: t('refresh') })] }));
|
|
90
|
-
if (error !== null) {
|
|
91
|
-
return (_jsxs("div", { style: wrap, children: [header, _jsxs("p", { role: "alert", style: { margin: 0, fontSize: 13 }, children: [t('failed'), ": ", error] }), _jsx("button", { type: "button", onClick: reload, style: { ...btn, alignSelf: 'flex-start' }, children: t('retry') })] }));
|
|
92
|
-
}
|
|
93
|
-
if (payload === null)
|
|
94
|
-
return _jsx("p", { style: muted, "aria-live": "polite", children: t('loading') });
|
|
95
|
-
return (_jsxs("div", { style: wrap, children: [header, _jsx(AlertBanner, { alerts: payload.alerts, t: t }), _jsx(Overview, { payload: payload, t: t }), payload.tools.length === 0 ? (_jsx("p", { style: { margin: 0, fontSize: 13, opacity: 0.8 }, children: t('empty') })) : (_jsxs("table", { style: table, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { style: th }), _jsx("th", { style: th, children: t('tool') }), _jsx("th", { style: th, children: t('calls') }), _jsx("th", { style: th, children: t('failRate') }), _jsx("th", { style: th, children: "p50" }), _jsx("th", { style: th, children: "p95" })] }) }), _jsx("tbody", { children: payload.tools.map((row) => (_jsx(_Fragment, { children: _jsxs("tr", { style: clickRow, onClick: () => { }, children: [_jsx("td", { style: td, children: _jsx(ToolDetailRow, { tool: row.tool, t: t }) }), _jsx("td", { style: td, children: _jsx("code", { style: { fontSize: 11 }, children: row.tool }) }), _jsx("td", { style: td, children: row.invocations }), _jsxs("td", { style: { ...td, color: row.failureRate > 0.3 ? '#e55' : undefined }, children: [Math.round(row.failureRate * 100), "%"] }), _jsxs("td", { style: td, children: [row.p50Latency, "ms"] }), _jsxs("td", { style: td, children: [row.p95Latency, "ms"] })] }, row.tool) }))) })] })), payload.deadTools.length > 0 && (_jsxs(_Fragment, { children: [_jsxs("strong", { style: { fontSize: 13 }, children: ["\uD83D\uDC80 ", t('deadTools')] }), _jsx("ul", { style: list, children: payload.deadTools.map((row) => _jsxs("li", { children: [_jsx("code", { style: { fontSize: 11 }, children: row.tool }), " \u2014 ", row.sessionsSinceLastUse, " ", t('sessionsUnused')] }, row.tool)) })] })), payload.failingTools.length > 0 && (_jsxs(_Fragment, { children: [_jsxs("strong", { style: { fontSize: 13 }, children: ["\u26A0\uFE0F ", t('failingTools')] }), _jsx("ul", { style: list, children: payload.failingTools.map((row) => (_jsxs("li", { children: [_jsx("code", { style: { fontSize: 11 }, children: row.tool }), " \u00B7 ", Math.round(row.failureRate * 100), "%", row.lastError ? ` — ${row.lastError.slice(0, 60)}` : ''] }, row.tool))) })] })), payload.recommendations.length > 0 && (_jsxs(_Fragment, { children: [_jsxs("strong", { style: { fontSize: 13 }, children: ["\uD83D\uDCA1 ", t('recommendations')] }), _jsx("ul", { style: list, children: payload.recommendations.map((row, i) => _jsx("li", { children: row.message }, i)) })] }))] }));
|
|
47
|
+
return _jsx(ToastProvider, { children: _jsx(ToolStatsPanelInner, { t: t }) });
|
|
96
48
|
}
|