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.
@@ -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, useEffect, useState } from 'react';
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
- const wrap = { display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 820, fontFamily: 'inherit' };
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(false);
65
- const toggle = useCallback(async () => {
66
- if (detail) {
67
- setDetail(null);
68
- return;
69
- }
14
+ const [loading, setLoading] = useState(true);
15
+ useCallback(() => {
70
16
  setLoading(true);
71
- try {
72
- const r = await fetch(`${DETAIL_PATH}?tool=${encodeURIComponent(tool)}`);
73
- if (r.ok)
74
- setDetail(await r.json());
75
- }
76
- finally {
77
- setLoading(false);
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
- }, [detail, tool]);
80
- return (_jsxs(_Fragment, { children: [_jsx("tr", { style: clickRow, onClick: toggle, children: _jsx("td", { style: td, children: loading ? '' : detail ? '' : '▶' }) }), detail && (_jsx("tr", { children: _jsx("td", { colSpan: 6, style: { padding: '4px 0' }, children: _jsxs("div", { style: card, children: [detail.errorBreakdown.length > 0 && (_jsxs("div", { style: { marginBottom: 8 }, children: [_jsx("strong", { style: { fontSize: 12 }, children: t('errorBreakdown') }), detail.errorBreakdown.map((e, i) => (_jsxs("div", { style: { ...preBlock, marginTop: 4 }, children: [_jsxs("span", { style: { color: '#e55', fontWeight: 600 }, children: ["\u00D7", e.count] }), " ", e.message] }, i)))] })), _jsx("strong", { style: { fontSize: 12 }, children: t('recentCalls') }), _jsx("table", { style: { ...table, marginTop: 4 }, children: _jsx("tbody", { children: detail.recentCalls.slice(0, 10).map((c, i) => (_jsxs("tr", { children: [_jsx("td", { style: td, children: c.success ? '' : '✗' }), _jsxs("td", { style: td, children: [c.latencyMs, "ms"] }), _jsx("td", { style: td, children: new Date(c.timestamp).toLocaleTimeString() }), _jsx("td", { style: { ...td, fontSize: 10, opacity: 0.7 }, children: c.errorMessage?.slice(0, 60) })] }, i))) }) })] }) }) }))] }));
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
- const { payload, error, reload } = usePanel();
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
  }