milens 0.7.2 → 0.7.3
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/.agents/skills/adapters/SKILL.md +59 -59
- package/.agents/skills/analyzer/SKILL.md +83 -83
- package/.agents/skills/apps/SKILL.md +65 -65
- package/.agents/skills/docs/SKILL.md +63 -63
- package/.agents/skills/milens/SKILL.md +322 -322
- package/.agents/skills/milens-security-review/SKILL.md +224 -224
- package/.agents/skills/orchestrator/SKILL.md +64 -64
- package/.agents/skills/parser/SKILL.md +86 -86
- package/.agents/skills/root/SKILL.md +89 -89
- package/.agents/skills/scripts/SKILL.md +56 -56
- package/.agents/skills/security/SKILL.md +67 -67
- package/.agents/skills/server/SKILL.md +84 -84
- package/.agents/skills/store/SKILL.md +77 -77
- package/.agents/skills/test/SKILL.md +155 -155
- package/.agents/skills/ui/SKILL.md +56 -56
- package/README.md +577 -577
- package/adapters/README.md +144 -144
- package/adapters/claude-code/.claude/mcp.json +9 -9
- package/adapters/claude-code/.claude/settings.json.hooks-snippet.json +26 -26
- package/adapters/claude-code/.claude-plugin/plugin.json +11 -11
- package/adapters/claude-code/.mcp.json +9 -9
- package/adapters/claude-code/CLAUDE.md +81 -81
- package/adapters/claude-code/hooks/hooks.json +27 -27
- package/adapters/codex/.codex/config.toml +3 -3
- package/adapters/copilot/.vscode/mcp.json +10 -10
- package/adapters/cursor/.cursor/mcp.json +9 -9
- package/adapters/cursor/.cursorrules +69 -69
- package/adapters/gemini/.gemini/context.md +81 -81
- package/adapters/gemini/.gemini/settings.json +9 -9
- package/adapters/opencode/.opencode/config.json +8 -8
- package/adapters/zed/.zed/settings.json +8 -8
- package/dist/build-info.d.ts +2 -2
- package/dist/build-info.js +2 -2
- package/dist/cli.js +482 -482
- package/dist/parser/lang-css.js +9 -9
- package/dist/parser/lang-go.js +58 -58
- package/dist/parser/lang-html.js +3 -3
- package/dist/parser/lang-java.js +37 -37
- package/dist/parser/lang-js.js +105 -105
- package/dist/parser/lang-php.js +45 -45
- package/dist/parser/lang-py.js +43 -43
- package/dist/parser/lang-ruby.js +28 -28
- package/dist/parser/lang-rust.js +48 -48
- package/dist/parser/lang-ts.js +210 -210
- package/dist/server/mcp-prompts.js +502 -502
- package/dist/server/mcp.js +56 -56
- package/dist/skills.js +296 -296
- package/dist/store/annotations.js +1 -1
- package/dist/store/db.js +224 -224
- package/dist/store/schema.sql +144 -144
- package/dist/store/vectors.js +2 -2
- package/package.json +85 -85
package/dist/cli.js
CHANGED
|
@@ -1344,29 +1344,29 @@ program
|
|
|
1344
1344
|
return;
|
|
1345
1345
|
}
|
|
1346
1346
|
}
|
|
1347
|
-
const preCommitContent = `#!/bin/bash
|
|
1348
|
-
# Auto-installed by milens init
|
|
1349
|
-
echo "Milens: Pre-commit check..."
|
|
1350
|
-
|
|
1351
|
-
OUTPUT=$(npx milens workflow review --path . 2>&1)
|
|
1352
|
-
echo "$OUTPUT"
|
|
1353
|
-
|
|
1354
|
-
# Block commit if CRITICAL risk detected
|
|
1355
|
-
if echo "$OUTPUT" | grep -q "CRITICAL"; then
|
|
1356
|
-
echo ""
|
|
1357
|
-
echo "❌ COMMIT BLOCKED: Critical risk detected."
|
|
1358
|
-
echo " Run 'milens workflow review' to see details."
|
|
1359
|
-
echo " To override: git commit --no-verify"
|
|
1360
|
-
exit 1
|
|
1361
|
-
fi
|
|
1362
|
-
|
|
1363
|
-
# Warn if HIGH risk but don't block
|
|
1364
|
-
if echo "$OUTPUT" | grep -q "HIGH"; then
|
|
1365
|
-
echo ""
|
|
1366
|
-
echo "⚠️ WARNING: High risk changes detected. Consider adding tests."
|
|
1367
|
-
fi
|
|
1368
|
-
|
|
1369
|
-
echo "Milens: Done."
|
|
1347
|
+
const preCommitContent = `#!/bin/bash
|
|
1348
|
+
# Auto-installed by milens init
|
|
1349
|
+
echo "Milens: Pre-commit check..."
|
|
1350
|
+
|
|
1351
|
+
OUTPUT=$(npx milens workflow review --path . 2>&1)
|
|
1352
|
+
echo "$OUTPUT"
|
|
1353
|
+
|
|
1354
|
+
# Block commit if CRITICAL risk detected
|
|
1355
|
+
if echo "$OUTPUT" | grep -q "CRITICAL"; then
|
|
1356
|
+
echo ""
|
|
1357
|
+
echo "❌ COMMIT BLOCKED: Critical risk detected."
|
|
1358
|
+
echo " Run 'milens workflow review' to see details."
|
|
1359
|
+
echo " To override: git commit --no-verify"
|
|
1360
|
+
exit 1
|
|
1361
|
+
fi
|
|
1362
|
+
|
|
1363
|
+
# Warn if HIGH risk but don't block
|
|
1364
|
+
if echo "$OUTPUT" | grep -q "HIGH"; then
|
|
1365
|
+
echo ""
|
|
1366
|
+
echo "⚠️ WARNING: High risk changes detected. Consider adding tests."
|
|
1367
|
+
fi
|
|
1368
|
+
|
|
1369
|
+
echo "Milens: Done."
|
|
1370
1370
|
`;
|
|
1371
1371
|
writeFileSync(resolve(hooksDir, 'pre-commit'), preCommitContent);
|
|
1372
1372
|
try {
|
|
@@ -1829,467 +1829,467 @@ function generateDashboardHtml(stats, annotationStats, repoFilter) {
|
|
|
1829
1829
|
: 0;
|
|
1830
1830
|
const fmtNum = (n) => n >= 1_000_000 ? (n / 1_000_000).toFixed(1) + 'M' : n >= 1_000 ? (n / 1_000).toFixed(1) + 'K' : String(n);
|
|
1831
1831
|
const confBars = annotationStats ? renderConfBars(annotationStats.confidenceBands, annotationStats.total) : '';
|
|
1832
|
-
const recentAnnots = annotationStats ? annotationStats.recent.map(a => `
|
|
1833
|
-
<li>
|
|
1834
|
-
<div><span class="annot-sym">${a.symbol}</span><span class="annot-key">${a.key}</span></div>
|
|
1835
|
-
<span class="annot-conf ${a.confidence >= 0.7 ? 'hi' : a.confidence >= 0.4 ? 'md' : 'lo'}">${(a.confidence * 100).toFixed(0)}%</span>
|
|
1832
|
+
const recentAnnots = annotationStats ? annotationStats.recent.map(a => `
|
|
1833
|
+
<li>
|
|
1834
|
+
<div><span class="annot-sym">${a.symbol}</span><span class="annot-key">${a.key}</span></div>
|
|
1835
|
+
<span class="annot-conf ${a.confidence >= 0.7 ? 'hi' : a.confidence >= 0.4 ? 'md' : 'lo'}">${(a.confidence * 100).toFixed(0)}%</span>
|
|
1836
1836
|
</li>`).join('') : '';
|
|
1837
|
-
const learningStats = annotationStats ? `
|
|
1838
|
-
<div class="card learning-section">
|
|
1839
|
-
<div class="card-header">
|
|
1840
|
-
<div><div class="card-title">Confidence Distribution</div><div class="card-subtitle">${annotationStats.total} total annotations</div></div>
|
|
1841
|
-
</div>
|
|
1842
|
-
<div class="tool-bars">${confBars}</div>
|
|
1843
|
-
</div>
|
|
1844
|
-
<div class="card learning-section">
|
|
1845
|
-
<div class="card-header">
|
|
1846
|
-
<div><div class="card-title">Recent Annotations</div><div class="card-subtitle">Last ${annotationStats.recent.length}</div></div>
|
|
1847
|
-
</div>
|
|
1848
|
-
<ul class="annot-list">${recentAnnots}</ul>
|
|
1849
|
-
</div>
|
|
1837
|
+
const learningStats = annotationStats ? `
|
|
1838
|
+
<div class="card learning-section">
|
|
1839
|
+
<div class="card-header">
|
|
1840
|
+
<div><div class="card-title">Confidence Distribution</div><div class="card-subtitle">${annotationStats.total} total annotations</div></div>
|
|
1841
|
+
</div>
|
|
1842
|
+
<div class="tool-bars">${confBars}</div>
|
|
1843
|
+
</div>
|
|
1844
|
+
<div class="card learning-section">
|
|
1845
|
+
<div class="card-header">
|
|
1846
|
+
<div><div class="card-title">Recent Annotations</div><div class="card-subtitle">Last ${annotationStats.recent.length}</div></div>
|
|
1847
|
+
</div>
|
|
1848
|
+
<ul class="annot-list">${recentAnnots}</ul>
|
|
1849
|
+
</div>
|
|
1850
1850
|
` : '';
|
|
1851
|
-
return `<!DOCTYPE html>
|
|
1852
|
-
<html lang="en">
|
|
1853
|
-
<head>
|
|
1854
|
-
<meta charset="UTF-8">
|
|
1855
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1856
|
-
<title>milens Dashboard</title>
|
|
1857
|
-
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
|
1858
|
-
<style>
|
|
1859
|
-
:root {
|
|
1860
|
-
--bg: #0a0e14; --surface: #12171e; --card: #161d27; --card-hover: #1a2332;
|
|
1861
|
-
--border: #1e2a3a; --border-light: #2a3a4e;
|
|
1862
|
-
--text: #e2e8f0; --text-secondary: #94a3b8; --text-muted: #64748b;
|
|
1863
|
-
--accent: #60a5fa; --accent-dim: #60a5fa22;
|
|
1864
|
-
--green: #34d399; --green-dim: #34d39915;
|
|
1865
|
-
--orange: #fbbf24; --orange-dim: #fbbf2415;
|
|
1866
|
-
--purple: #a78bfa; --purple-dim: #a78bfa15;
|
|
1867
|
-
--red: #f87171;
|
|
1868
|
-
--radius: 16px; --radius-sm: 10px;
|
|
1869
|
-
}
|
|
1870
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
1871
|
-
body {
|
|
1872
|
-
background: var(--bg); color: var(--text);
|
|
1873
|
-
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
1874
|
-
line-height: 1.5; min-height: 100vh;
|
|
1875
|
-
}
|
|
1876
|
-
|
|
1877
|
-
/* ── Layout ── */
|
|
1878
|
-
.wrapper { max-width: 1400px; margin: 0 auto; padding: 32px 24px 80px; }
|
|
1879
|
-
|
|
1880
|
-
/* ── Header ── */
|
|
1881
|
-
.header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 32px; }
|
|
1882
|
-
.header-left { display: flex; align-items: center; gap: 14px; }
|
|
1883
|
-
.logo { width: 40px; height: 40px; border-radius: 12px; background: linear-gradient(135deg, var(--accent), var(--purple)); display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 18px; color: #fff; }
|
|
1884
|
-
.header h1 { font-size: 22px; font-weight: 700; letter-spacing: -0.3px; }
|
|
1885
|
-
.header h1 span { color: var(--text-muted); font-weight: 400; font-size: 14px; margin-left: 8px; }
|
|
1886
|
-
.header-right { display: flex; align-items: center; gap: 12px; }
|
|
1887
|
-
.status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); animation: pulse 2s infinite; }
|
|
1888
|
-
@keyframes pulse { 0%,80%,100% { opacity: 1; } 40% { opacity: 0.4; } }
|
|
1889
|
-
.status-text { font-size: 12px; color: var(--text-muted); }
|
|
1890
|
-
.refresh-btn {
|
|
1891
|
-
background: var(--accent-dim); color: var(--accent); border: 1px solid var(--border);
|
|
1892
|
-
border-radius: var(--radius-sm); padding: 8px 16px; cursor: pointer;
|
|
1893
|
-
font-weight: 500; font-size: 13px; transition: all 0.2s;
|
|
1894
|
-
}
|
|
1895
|
-
.refresh-btn:hover { background: var(--accent); color: #0a0e14; }
|
|
1896
|
-
|
|
1897
|
-
/* ── KPI Cards ── */
|
|
1898
|
-
.kpi-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 28px; }
|
|
1899
|
-
.kpi {
|
|
1900
|
-
background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
|
|
1901
|
-
padding: 24px; position: relative; overflow: hidden; transition: border-color 0.2s;
|
|
1902
|
-
}
|
|
1903
|
-
.kpi:hover { border-color: var(--border-light); }
|
|
1904
|
-
.kpi-icon { width: 40px; height: 40px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 20px; margin-bottom: 16px; }
|
|
1905
|
-
.kpi-icon.blue { background: var(--accent-dim); }
|
|
1906
|
-
.kpi-icon.green { background: var(--green-dim); }
|
|
1907
|
-
.kpi-icon.purple { background: var(--purple-dim); }
|
|
1908
|
-
.kpi-icon.orange { background: var(--orange-dim); }
|
|
1909
|
-
.kpi .value { font-size: 32px; font-weight: 800; letter-spacing: -1px; line-height: 1; }
|
|
1910
|
-
.kpi .value.blue { color: var(--accent); }
|
|
1911
|
-
.kpi .value.green { color: var(--green); }
|
|
1912
|
-
.kpi .value.purple { color: var(--purple); }
|
|
1913
|
-
.kpi .value.orange { color: var(--orange); }
|
|
1914
|
-
.kpi .label { color: var(--text-muted); font-size: 13px; margin-top: 6px; font-weight: 500; }
|
|
1915
|
-
.kpi .sub { color: var(--text-secondary); font-size: 12px; margin-top: 4px; }
|
|
1916
|
-
.kpi-glow {
|
|
1917
|
-
position: absolute; top: -40px; right: -40px; width: 120px; height: 120px;
|
|
1918
|
-
border-radius: 50%; opacity: 0.06; pointer-events: none;
|
|
1919
|
-
}
|
|
1920
|
-
.kpi-glow.blue { background: var(--accent); }
|
|
1921
|
-
.kpi-glow.green { background: var(--green); }
|
|
1922
|
-
.kpi-glow.purple { background: var(--purple); }
|
|
1923
|
-
.kpi-glow.orange { background: var(--orange); }
|
|
1924
|
-
|
|
1925
|
-
/* ── Charts ── */
|
|
1926
|
-
.grid-2 { display: grid; grid-template-columns: 5fr 7fr; gap: 16px; margin-bottom: 16px; }
|
|
1927
|
-
.grid-full { margin-bottom: 16px; }
|
|
1928
|
-
.card {
|
|
1929
|
-
background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
|
|
1930
|
-
padding: 24px; transition: border-color 0.2s;
|
|
1931
|
-
}
|
|
1932
|
-
.card:hover { border-color: var(--border-light); }
|
|
1933
|
-
.card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
|
|
1934
|
-
.card-title { font-size: 14px; font-weight: 600; color: var(--text); }
|
|
1935
|
-
.card-subtitle { font-size: 12px; color: var(--text-muted); }
|
|
1936
|
-
|
|
1937
|
-
/* ── Chart containers ── */
|
|
1938
|
-
.chart-container { position: relative; width: 100%; }
|
|
1939
|
-
.chart-container.h-280 { height: 280px; }
|
|
1940
|
-
.chart-container.h-300 { height: 300px; }
|
|
1941
|
-
|
|
1942
|
-
/* ── Top Tools Bar ── */
|
|
1943
|
-
.tool-bars { display: flex; flex-direction: column; gap: 10px; }
|
|
1944
|
-
.tool-bar-row { display: flex; align-items: center; gap: 12px; }
|
|
1945
|
-
.tool-bar-name { width: 140px; font-size: 12px; font-family: 'SF Mono', 'Fira Code', monospace; color: var(--text-secondary); text-align: right; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1946
|
-
.tool-bar-track { flex: 1; height: 28px; background: var(--surface); border-radius: 6px; overflow: hidden; position: relative; }
|
|
1947
|
-
.tool-bar-fill { height: 100%; border-radius: 6px; display: flex; align-items: center; padding: 0 10px; font-size: 11px; font-weight: 600; color: #fff; min-width: fit-content; transition: width 0.6s ease; }
|
|
1948
|
-
.tool-bar-count { font-size: 12px; color: var(--text-muted); min-width: 36px; text-align: right; }
|
|
1949
|
-
|
|
1950
|
-
/* ── Recent Table ── */
|
|
1951
|
-
.table-wrapper { max-height: 400px; overflow-y: auto; border-radius: var(--radius-sm); }
|
|
1952
|
-
.table-wrapper::-webkit-scrollbar { width: 6px; }
|
|
1953
|
-
.table-wrapper::-webkit-scrollbar-track { background: transparent; }
|
|
1954
|
-
.table-wrapper::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 3px; }
|
|
1955
|
-
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
1956
|
-
thead { position: sticky; top: 0; z-index: 1; }
|
|
1957
|
-
th {
|
|
1958
|
-
text-align: left; color: var(--text-muted); font-weight: 500; padding: 10px 16px;
|
|
1959
|
-
background: var(--card); border-bottom: 1px solid var(--border); font-size: 11px;
|
|
1960
|
-
text-transform: uppercase; letter-spacing: 0.5px;
|
|
1961
|
-
}
|
|
1962
|
-
td { padding: 10px 16px; border-bottom: 1px solid var(--border); }
|
|
1963
|
-
tr:hover td { background: var(--surface); }
|
|
1964
|
-
.tool-badge {
|
|
1965
|
-
display: inline-flex; align-items: center; gap: 4px;
|
|
1966
|
-
background: var(--accent-dim); color: var(--accent); padding: 3px 10px;
|
|
1967
|
-
border-radius: 6px; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px; font-weight: 500;
|
|
1968
|
-
}
|
|
1969
|
-
.when-text { color: var(--text-muted); }
|
|
1970
|
-
.duration-text { color: var(--text-secondary); font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; }
|
|
1971
|
-
.saved-text { color: var(--green); font-weight: 600; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; }
|
|
1972
|
-
|
|
1973
|
-
/* ── Footer ── */
|
|
1974
|
-
.footer { text-align: center; padding: 24px 0 0; color: var(--text-muted); font-size: 12px; }
|
|
1975
|
-
|
|
1976
|
-
/* ── Tabs ── */
|
|
1977
|
-
.tab-nav { display: flex; gap: 4px; margin-bottom: 28px; border-bottom: 2px solid var(--border); padding-bottom: 0; }
|
|
1978
|
-
.tab { padding: 10px 24px; cursor: pointer; font-size: 14px; font-weight: 500; color: var(--text-muted); border-bottom: 2px solid transparent; margin-bottom: -2px; transition: all 0.2s; background: none; border-top: none; border-left: none; border-right: none; outline: none; }
|
|
1979
|
-
.tab:hover { color: var(--text-secondary); }
|
|
1980
|
-
.tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
1981
|
-
.tab-content { display: none; }
|
|
1982
|
-
.tab-content.active { display: block; }
|
|
1983
|
-
|
|
1984
|
-
/* ── Learning ── */
|
|
1985
|
-
.suggestion-box { background: var(--accent-dim); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 20px; text-align: center; margin-bottom: 20px; }
|
|
1986
|
-
.suggestion-box code { background: var(--surface); padding: 2px 8px; border-radius: 4px; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px; color: var(--accent); }
|
|
1987
|
-
.learning-section { margin-bottom: 16px; }
|
|
1988
|
-
.annot-list { list-style: none; padding: 0; }
|
|
1989
|
-
.annot-list li { padding: 10px 0; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
|
|
1990
|
-
.annot-list li:last-child { border-bottom: none; }
|
|
1991
|
-
.annot-sym { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px; color: var(--accent); }
|
|
1992
|
-
.annot-key { font-size: 11px; color: var(--text-muted); background: var(--surface); padding: 2px 8px; border-radius: 4px; margin-left: 8px; }
|
|
1993
|
-
.annot-conf { font-size: 12px; font-weight: 600; }
|
|
1994
|
-
.annot-conf.hi { color: var(--green); }
|
|
1995
|
-
.annot-conf.md { color: var(--orange); }
|
|
1996
|
-
.annot-conf.lo { color: var(--red); }
|
|
1997
|
-
|
|
1998
|
-
/* ── Responsive ── */
|
|
1999
|
-
@media (max-width: 1024px) { .kpi-grid { grid-template-columns: repeat(2, 1fr); } .grid-2 { grid-template-columns: 1fr; } }
|
|
2000
|
-
@media (max-width: 640px) { .kpi-grid { grid-template-columns: 1fr; } .wrapper { padding: 16px 12px 80px; } }
|
|
2001
|
-
</style>
|
|
2002
|
-
</head>
|
|
2003
|
-
<body>
|
|
2004
|
-
<div class="wrapper">
|
|
2005
|
-
|
|
2006
|
-
<!-- Header -->
|
|
2007
|
-
<div class="header">
|
|
2008
|
-
<div class="header-left">
|
|
2009
|
-
<div class="logo">m</div>
|
|
2010
|
-
<h1>milens <span>dashboard</span></h1>
|
|
2011
|
-
</div>
|
|
2012
|
-
<div class="header-right">
|
|
2013
|
-
<span class="repo-badge" style="background:rgba(88,166,255,0.12);color:#58a6ff;border:1px solid rgba(88,166,255,0.25);border-radius:20px;padding:4px 12px;font-size:0.78em;font-weight:600;margin-right:12px;">${repoFilter ? repoFilter.replace(/\\\\/g, '/').split('/').pop() || repoFilter : 'All Repos'}</span>
|
|
2014
|
-
<div class="status-dot"></div>
|
|
2015
|
-
<span class="status-text">Live</span>
|
|
2016
|
-
<button class="refresh-btn" onclick="refreshData()">↻ Refresh</button>
|
|
2017
|
-
</div>
|
|
2018
|
-
</div>
|
|
2019
|
-
|
|
2020
|
-
<!-- Tab Navigation -->
|
|
2021
|
-
<div class="tab-nav">
|
|
2022
|
-
<button class="tab active" data-tab="usage" onclick="switchTab('usage')">Usage Analytics</button>
|
|
2023
|
-
<button class="tab" data-tab="learning" onclick="switchTab('learning')">Learning</button>
|
|
2024
|
-
</div>
|
|
2025
|
-
|
|
2026
|
-
<div id="tab-usage" class="tab-content active">
|
|
2027
|
-
|
|
2028
|
-
<!-- KPIs -->
|
|
2029
|
-
<div class="kpi-grid">
|
|
2030
|
-
<div class="kpi">
|
|
2031
|
-
<div class="kpi-icon blue">⚙</div>
|
|
2032
|
-
<div class="value blue" id="kpi-calls">${fmtNum(stats.totalCalls)}</div>
|
|
2033
|
-
<div class="label">Tool Calls</div>
|
|
2034
|
-
<div class="sub" id="kpi-calls-sub">${stats.totalCalls.toLocaleString()} total invocations</div>
|
|
2035
|
-
<div class="kpi-glow blue"></div>
|
|
2036
|
-
</div>
|
|
2037
|
-
<div class="kpi">
|
|
2038
|
-
<div class="kpi-icon green">⚡</div>
|
|
2039
|
-
<div class="value green" id="kpi-saved">${fmtNum(stats.totalTokensSaved)}</div>
|
|
2040
|
-
<div class="label">Tokens Saved</div>
|
|
2041
|
-
<div class="sub" id="kpi-saved-sub">${stats.totalTokensSaved.toLocaleString()} tokens not wasted</div>
|
|
2042
|
-
<div class="kpi-glow green"></div>
|
|
2043
|
-
</div>
|
|
2044
|
-
<div class="kpi">
|
|
2045
|
-
<div class="kpi-icon purple">★</div>
|
|
2046
|
-
<div class="value purple" id="kpi-pct">${savingsPercent}%</div>
|
|
2047
|
-
<div class="label">Token Efficiency</div>
|
|
2048
|
-
<div class="sub">${fmtNum(stats.totalTokensOut)} returned vs ${fmtNum(stats.totalTokensSaved)} saved</div>
|
|
2049
|
-
<div class="kpi-glow purple"></div>
|
|
2050
|
-
</div>
|
|
2051
|
-
<div class="kpi">
|
|
2052
|
-
<div class="kpi-icon orange">⏱</div>
|
|
2053
|
-
<div class="value orange" id="kpi-avg">${stats.totalCalls > 0 ? Math.round(stats.totalDurationMs / stats.totalCalls) : 0}ms</div>
|
|
2054
|
-
<div class="label">Avg Response Time</div>
|
|
2055
|
-
<div class="sub">${(stats.totalDurationMs / 1000).toFixed(1)}s total processing</div>
|
|
2056
|
-
<div class="kpi-glow orange"></div>
|
|
2057
|
-
</div>
|
|
2058
|
-
</div>
|
|
2059
|
-
|
|
2060
|
-
<!-- Charts Row -->
|
|
2061
|
-
<div class="grid-2">
|
|
2062
|
-
<div class="card">
|
|
2063
|
-
<div class="card-header">
|
|
2064
|
-
<div>
|
|
2065
|
-
<div class="card-title">Top Tools</div>
|
|
2066
|
-
<div class="card-subtitle">By number of calls</div>
|
|
2067
|
-
</div>
|
|
2068
|
-
</div>
|
|
2069
|
-
<div id="toolBars" class="tool-bars"></div>
|
|
2070
|
-
</div>
|
|
2071
|
-
<div class="card">
|
|
2072
|
-
<div class="card-header">
|
|
2073
|
-
<div>
|
|
2074
|
-
<div class="card-title">Daily Activity</div>
|
|
2075
|
-
<div class="card-subtitle">Calls & tokens saved over the last 30 days</div>
|
|
2076
|
-
</div>
|
|
2077
|
-
</div>
|
|
2078
|
-
<div class="chart-container h-280"><canvas id="dayChart"></canvas></div>
|
|
2079
|
-
</div>
|
|
2080
|
-
</div>
|
|
2081
|
-
|
|
2082
|
-
<!-- Savings Distribution -->
|
|
2083
|
-
<div class="grid-2" style="grid-template-columns: 7fr 5fr;">
|
|
2084
|
-
<div class="card">
|
|
2085
|
-
<div class="card-header">
|
|
2086
|
-
<div>
|
|
2087
|
-
<div class="card-title">Recent Tool Calls</div>
|
|
2088
|
-
<div class="card-subtitle">Last 50 invocations</div>
|
|
2089
|
-
</div>
|
|
2090
|
-
</div>
|
|
2091
|
-
<div class="table-wrapper">
|
|
2092
|
-
<table>
|
|
2093
|
-
<thead><tr><th>Tool</th><th>When</th><th>Duration</th><th style="text-align:right">Tokens Saved</th></tr></thead>
|
|
2094
|
-
<tbody id="recentBody"></tbody>
|
|
2095
|
-
</table>
|
|
2096
|
-
</div>
|
|
2097
|
-
</div>
|
|
2098
|
-
<div class="card">
|
|
2099
|
-
<div class="card-header">
|
|
2100
|
-
<div>
|
|
2101
|
-
<div class="card-title">Savings by Tool</div>
|
|
2102
|
-
<div class="card-subtitle">Token savings distribution</div>
|
|
2103
|
-
</div>
|
|
2104
|
-
</div>
|
|
2105
|
-
<div class="chart-container h-300"><canvas id="savingsChart"></canvas></div>
|
|
2106
|
-
</div>
|
|
2107
|
-
</div>
|
|
2108
|
-
|
|
2109
|
-
<div class="footer">milens · auto-refreshes every 30s</div>
|
|
2110
|
-
</div><!-- /tab-usage -->
|
|
2111
|
-
|
|
2112
|
-
<div id="tab-learning" class="tab-content">
|
|
2113
|
-
<div class="suggestion-box">
|
|
2114
|
-
<h3 style="margin-bottom:8px;font-weight:600;">Metrics & Insights</h3>
|
|
2115
|
-
<p style="color:var(--text-secondary);font-size:13px;">Run <code>milens metrics</code> for a full code-quality metrics report.</p>
|
|
2116
|
-
</div>
|
|
2117
|
-
${learningStats}
|
|
2118
|
-
</div>
|
|
2119
|
-
|
|
2120
|
-
</div><!-- /wrapper -->
|
|
2121
|
-
|
|
2122
|
-
<script>
|
|
2123
|
-
const COLORS = ['#60a5fa','#34d399','#fbbf24','#a78bfa','#f87171','#2dd4bf','#818cf8','#fb923c','#e879f9','#38bdf8','#4ade80','#facc15','#f472b6','#22d3ee','#a3e635','#c084fc'];
|
|
2124
|
-
const BAR_COLORS = ['#60a5fa','#34d399','#fbbf24','#a78bfa','#f87171','#2dd4bf','#818cf8','#fb923c','#e879f9','#38bdf8'];
|
|
2125
|
-
let byTool = ${byToolJson};
|
|
2126
|
-
let byDay = ${byDayJson};
|
|
2127
|
-
|
|
2128
|
-
function fmtK(n) { return n >= 1e6 ? (n/1e6).toFixed(1)+'M' : n >= 1e3 ? (n/1e3).toFixed(1)+'K' : n; }
|
|
2129
|
-
|
|
2130
|
-
/* ── Top Tools Horizontal Bars ── */
|
|
2131
|
-
function renderToolBars() {
|
|
2132
|
-
const el = document.getElementById('toolBars');
|
|
2133
|
-
const sorted = [...byTool].sort((a,b) => b.calls - a.calls).slice(0, 10);
|
|
2134
|
-
const maxCalls = sorted[0]?.calls || 1;
|
|
2135
|
-
el.innerHTML = sorted.map((t, i) => {
|
|
2136
|
-
const pct = Math.max(8, (t.calls / maxCalls) * 100);
|
|
2137
|
-
const col = BAR_COLORS[i % BAR_COLORS.length];
|
|
2138
|
-
return \`<div class="tool-bar-row">
|
|
2139
|
-
<span class="tool-bar-name">\${t.tool}</span>
|
|
2140
|
-
<div class="tool-bar-track">
|
|
2141
|
-
<div class="tool-bar-fill" style="width:\${pct}%;background:linear-gradient(90deg,\${col}dd,\${col}88)">\${t.calls}</div>
|
|
2142
|
-
</div>
|
|
2143
|
-
<span class="tool-bar-count">\${fmtK(t.tokensSaved)}</span>
|
|
2144
|
-
</div>\`;
|
|
2145
|
-
}).join('');
|
|
2146
|
-
}
|
|
2147
|
-
|
|
2148
|
-
/* ── Daily Activity Chart ── */
|
|
2149
|
-
let dayChartInstance = null;
|
|
2150
|
-
function renderDayChart() {
|
|
2151
|
-
if (dayChartInstance) dayChartInstance.destroy();
|
|
2152
|
-
const ctx = document.getElementById('dayChart');
|
|
2153
|
-
const labels = byDay.map(d => {
|
|
2154
|
-
const parts = d.date.split('-');
|
|
2155
|
-
return parts[1] + '/' + parts[2];
|
|
2156
|
-
});
|
|
2157
|
-
dayChartInstance = new Chart(ctx, {
|
|
2158
|
-
type: 'bar',
|
|
2159
|
-
data: {
|
|
2160
|
-
labels,
|
|
2161
|
-
datasets: [
|
|
2162
|
-
{
|
|
2163
|
-
label: 'Calls', data: byDay.map(d => d.calls),
|
|
2164
|
-
backgroundColor: '#60a5fa44', hoverBackgroundColor: '#60a5fa88',
|
|
2165
|
-
borderRadius: 4, borderSkipped: false, yAxisID: 'y', barPercentage: 0.7,
|
|
2166
|
-
},
|
|
2167
|
-
{
|
|
2168
|
-
label: 'Tokens Saved', data: byDay.map(d => d.tokensSaved),
|
|
2169
|
-
type: 'line', borderColor: '#34d399', pointBackgroundColor: '#34d399',
|
|
2170
|
-
pointRadius: 2, pointHoverRadius: 5, borderWidth: 2.5,
|
|
2171
|
-
yAxisID: 'y1', tension: 0.4, fill: { target: 'origin', above: '#34d39910' },
|
|
2172
|
-
},
|
|
2173
|
-
],
|
|
2174
|
-
},
|
|
2175
|
-
options: {
|
|
2176
|
-
responsive: true, maintainAspectRatio: false,
|
|
2177
|
-
interaction: { mode: 'index', intersect: false },
|
|
2178
|
-
scales: {
|
|
2179
|
-
x: {
|
|
2180
|
-
ticks: { color: '#64748b', font: { size: 11 }, maxRotation: 0, autoSkip: true, maxTicksLimit: 15 },
|
|
2181
|
-
grid: { display: false },
|
|
2182
|
-
},
|
|
2183
|
-
y: {
|
|
2184
|
-
position: 'left', ticks: { color: '#60a5fa', font: { size: 11 } },
|
|
2185
|
-
grid: { color: '#1e2a3a' }, title: { display: true, text: 'Calls', color: '#60a5fa', font: { size: 11 } },
|
|
2186
|
-
},
|
|
2187
|
-
y1: {
|
|
2188
|
-
position: 'right', ticks: { color: '#34d399', font: { size: 11 }, callback: v => fmtK(v) },
|
|
2189
|
-
grid: { drawOnChartArea: false }, title: { display: true, text: 'Tokens Saved', color: '#34d399', font: { size: 11 } },
|
|
2190
|
-
},
|
|
2191
|
-
},
|
|
2192
|
-
plugins: {
|
|
2193
|
-
legend: { labels: { color: '#94a3b8', boxWidth: 12, usePointStyle: true, padding: 16 } },
|
|
2194
|
-
tooltip: {
|
|
2195
|
-
backgroundColor: '#1e293b', borderColor: '#334155', borderWidth: 1, titleColor: '#e2e8f0',
|
|
2196
|
-
bodyColor: '#94a3b8', cornerRadius: 8, padding: 12,
|
|
2197
|
-
callbacks: { label: ctx => ctx.dataset.label + ': ' + (ctx.datasetIndex === 1 ? fmtK(ctx.raw) : ctx.raw) },
|
|
2198
|
-
},
|
|
2199
|
-
},
|
|
2200
|
-
},
|
|
2201
|
-
});
|
|
2202
|
-
}
|
|
2203
|
-
|
|
2204
|
-
/* ── Savings Doughnut ── */
|
|
2205
|
-
function renderSavingsChart() {
|
|
2206
|
-
const ctx = document.getElementById('savingsChart');
|
|
2207
|
-
const sorted = [...byTool].sort((a,b) => b.tokensSaved - a.tokensSaved);
|
|
2208
|
-
const top = sorted.slice(0, 8);
|
|
2209
|
-
const rest = sorted.slice(8);
|
|
2210
|
-
if (rest.length) top.push({ tool: 'others', tokensSaved: rest.reduce((s,t) => s + t.tokensSaved, 0), calls: 0 });
|
|
2211
|
-
new Chart(ctx, {
|
|
2212
|
-
type: 'doughnut',
|
|
2213
|
-
data: {
|
|
2214
|
-
labels: top.map(t => t.tool),
|
|
2215
|
-
datasets: [{
|
|
2216
|
-
data: top.map(t => t.tokensSaved),
|
|
2217
|
-
backgroundColor: top.map((_, i) => COLORS[i]),
|
|
2218
|
-
borderWidth: 0, hoverOffset: 6,
|
|
2219
|
-
}],
|
|
2220
|
-
},
|
|
2221
|
-
options: {
|
|
2222
|
-
responsive: true, cutout: '68%',
|
|
2223
|
-
plugins: {
|
|
2224
|
-
legend: { position: 'right', labels: { color: '#94a3b8', font: { size: 12 }, padding: 8, usePointStyle: true, pointStyleWidth: 10 } },
|
|
2225
|
-
tooltip: {
|
|
2226
|
-
backgroundColor: '#1e293b', borderColor: '#334155', borderWidth: 1, titleColor: '#e2e8f0',
|
|
2227
|
-
bodyColor: '#94a3b8', cornerRadius: 8, padding: 12,
|
|
2228
|
-
callbacks: { label: ctx => ' ' + ctx.label + ': ' + fmtK(ctx.raw) + ' tokens' },
|
|
2229
|
-
},
|
|
2230
|
-
},
|
|
2231
|
-
},
|
|
2232
|
-
});
|
|
2233
|
-
}
|
|
2234
|
-
|
|
2235
|
-
/* ── Recent Calls Table ── */
|
|
2236
|
-
function renderRecent(data) {
|
|
2237
|
-
const tbody = document.getElementById('recentBody');
|
|
2238
|
-
tbody.innerHTML = data.map(r => {
|
|
2239
|
-
const ago = timeAgo(r.calledAt);
|
|
2240
|
-
return \`<tr>
|
|
2241
|
-
<td><span class="tool-badge">\${r.tool}</span></td>
|
|
2242
|
-
<td class="when-text">\${ago}</td>
|
|
2243
|
-
<td class="duration-text">\${r.durationMs}ms</td>
|
|
2244
|
-
<td class="saved-text" style="text-align:right">+\${r.tokensSaved.toLocaleString()}</td>
|
|
2245
|
-
</tr>\`;
|
|
2246
|
-
}).join('');
|
|
2247
|
-
}
|
|
2248
|
-
|
|
2249
|
-
function timeAgo(iso) {
|
|
2250
|
-
const d = new Date(iso.includes('T') ? iso : iso + 'Z');
|
|
2251
|
-
const s = Math.floor((Date.now() - d.getTime()) / 1000);
|
|
2252
|
-
if (s < 0) return 'just now';
|
|
2253
|
-
if (s < 60) return s + 's ago';
|
|
2254
|
-
if (s < 3600) return Math.floor(s/60) + 'm ago';
|
|
2255
|
-
if (s < 86400) return Math.floor(s/3600) + 'h ago';
|
|
2256
|
-
return Math.floor(s/86400) + 'd ago';
|
|
2257
|
-
}
|
|
2258
|
-
|
|
2259
|
-
/* ── Refresh ── */
|
|
2260
|
-
async function refreshData() {
|
|
2261
|
-
const btn = document.querySelector('.refresh-btn');
|
|
2262
|
-
btn.textContent = '⟳ Loading...';
|
|
2263
|
-
try {
|
|
2264
|
-
const res = await fetch('/api/stats');
|
|
2265
|
-
const data = await res.json();
|
|
2266
|
-
byTool = data.byTool; byDay = data.byDay;
|
|
2267
|
-
document.getElementById('kpi-calls').textContent = fmtK(data.totalCalls);
|
|
2268
|
-
document.getElementById('kpi-calls-sub').textContent = data.totalCalls.toLocaleString() + ' total invocations';
|
|
2269
|
-
document.getElementById('kpi-saved').textContent = fmtK(data.totalTokensSaved);
|
|
2270
|
-
document.getElementById('kpi-saved-sub').textContent = data.totalTokensSaved.toLocaleString() + ' tokens not wasted';
|
|
2271
|
-
const pct = data.totalTokensOut > 0 ? Math.round((data.totalTokensSaved / (data.totalTokensOut + data.totalTokensSaved)) * 100) : 0;
|
|
2272
|
-
document.getElementById('kpi-pct').textContent = pct + '%';
|
|
2273
|
-
document.getElementById('kpi-avg').textContent = (data.totalCalls > 0 ? Math.round(data.totalDurationMs / data.totalCalls) : 0) + 'ms';
|
|
2274
|
-
renderToolBars(); renderDayChart(); renderRecent(data.recentCalls);
|
|
2275
|
-
} catch(e) { console.error('Refresh failed', e); }
|
|
2276
|
-
btn.innerHTML = '↻ Refresh';
|
|
2277
|
-
}
|
|
2278
|
-
|
|
2279
|
-
/* ── Tab Switching ── */
|
|
2280
|
-
function switchTab(tab) {
|
|
2281
|
-
document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab));
|
|
2282
|
-
document.querySelectorAll('.tab-content').forEach(c => c.classList.toggle('active', c.id === 'tab-' + tab));
|
|
2283
|
-
}
|
|
2284
|
-
|
|
2285
|
-
/* ── Init ── */
|
|
2286
|
-
renderToolBars();
|
|
2287
|
-
renderDayChart();
|
|
2288
|
-
renderSavingsChart();
|
|
2289
|
-
renderRecent(${recentJson});
|
|
2290
|
-
setInterval(refreshData, 30000);
|
|
2291
|
-
</script>
|
|
2292
|
-
</body>
|
|
1851
|
+
return `<!DOCTYPE html>
|
|
1852
|
+
<html lang="en">
|
|
1853
|
+
<head>
|
|
1854
|
+
<meta charset="UTF-8">
|
|
1855
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1856
|
+
<title>milens Dashboard</title>
|
|
1857
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
|
1858
|
+
<style>
|
|
1859
|
+
:root {
|
|
1860
|
+
--bg: #0a0e14; --surface: #12171e; --card: #161d27; --card-hover: #1a2332;
|
|
1861
|
+
--border: #1e2a3a; --border-light: #2a3a4e;
|
|
1862
|
+
--text: #e2e8f0; --text-secondary: #94a3b8; --text-muted: #64748b;
|
|
1863
|
+
--accent: #60a5fa; --accent-dim: #60a5fa22;
|
|
1864
|
+
--green: #34d399; --green-dim: #34d39915;
|
|
1865
|
+
--orange: #fbbf24; --orange-dim: #fbbf2415;
|
|
1866
|
+
--purple: #a78bfa; --purple-dim: #a78bfa15;
|
|
1867
|
+
--red: #f87171;
|
|
1868
|
+
--radius: 16px; --radius-sm: 10px;
|
|
1869
|
+
}
|
|
1870
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
1871
|
+
body {
|
|
1872
|
+
background: var(--bg); color: var(--text);
|
|
1873
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
1874
|
+
line-height: 1.5; min-height: 100vh;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
/* ── Layout ── */
|
|
1878
|
+
.wrapper { max-width: 1400px; margin: 0 auto; padding: 32px 24px 80px; }
|
|
1879
|
+
|
|
1880
|
+
/* ── Header ── */
|
|
1881
|
+
.header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 32px; }
|
|
1882
|
+
.header-left { display: flex; align-items: center; gap: 14px; }
|
|
1883
|
+
.logo { width: 40px; height: 40px; border-radius: 12px; background: linear-gradient(135deg, var(--accent), var(--purple)); display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 18px; color: #fff; }
|
|
1884
|
+
.header h1 { font-size: 22px; font-weight: 700; letter-spacing: -0.3px; }
|
|
1885
|
+
.header h1 span { color: var(--text-muted); font-weight: 400; font-size: 14px; margin-left: 8px; }
|
|
1886
|
+
.header-right { display: flex; align-items: center; gap: 12px; }
|
|
1887
|
+
.status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); animation: pulse 2s infinite; }
|
|
1888
|
+
@keyframes pulse { 0%,80%,100% { opacity: 1; } 40% { opacity: 0.4; } }
|
|
1889
|
+
.status-text { font-size: 12px; color: var(--text-muted); }
|
|
1890
|
+
.refresh-btn {
|
|
1891
|
+
background: var(--accent-dim); color: var(--accent); border: 1px solid var(--border);
|
|
1892
|
+
border-radius: var(--radius-sm); padding: 8px 16px; cursor: pointer;
|
|
1893
|
+
font-weight: 500; font-size: 13px; transition: all 0.2s;
|
|
1894
|
+
}
|
|
1895
|
+
.refresh-btn:hover { background: var(--accent); color: #0a0e14; }
|
|
1896
|
+
|
|
1897
|
+
/* ── KPI Cards ── */
|
|
1898
|
+
.kpi-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 28px; }
|
|
1899
|
+
.kpi {
|
|
1900
|
+
background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
|
|
1901
|
+
padding: 24px; position: relative; overflow: hidden; transition: border-color 0.2s;
|
|
1902
|
+
}
|
|
1903
|
+
.kpi:hover { border-color: var(--border-light); }
|
|
1904
|
+
.kpi-icon { width: 40px; height: 40px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 20px; margin-bottom: 16px; }
|
|
1905
|
+
.kpi-icon.blue { background: var(--accent-dim); }
|
|
1906
|
+
.kpi-icon.green { background: var(--green-dim); }
|
|
1907
|
+
.kpi-icon.purple { background: var(--purple-dim); }
|
|
1908
|
+
.kpi-icon.orange { background: var(--orange-dim); }
|
|
1909
|
+
.kpi .value { font-size: 32px; font-weight: 800; letter-spacing: -1px; line-height: 1; }
|
|
1910
|
+
.kpi .value.blue { color: var(--accent); }
|
|
1911
|
+
.kpi .value.green { color: var(--green); }
|
|
1912
|
+
.kpi .value.purple { color: var(--purple); }
|
|
1913
|
+
.kpi .value.orange { color: var(--orange); }
|
|
1914
|
+
.kpi .label { color: var(--text-muted); font-size: 13px; margin-top: 6px; font-weight: 500; }
|
|
1915
|
+
.kpi .sub { color: var(--text-secondary); font-size: 12px; margin-top: 4px; }
|
|
1916
|
+
.kpi-glow {
|
|
1917
|
+
position: absolute; top: -40px; right: -40px; width: 120px; height: 120px;
|
|
1918
|
+
border-radius: 50%; opacity: 0.06; pointer-events: none;
|
|
1919
|
+
}
|
|
1920
|
+
.kpi-glow.blue { background: var(--accent); }
|
|
1921
|
+
.kpi-glow.green { background: var(--green); }
|
|
1922
|
+
.kpi-glow.purple { background: var(--purple); }
|
|
1923
|
+
.kpi-glow.orange { background: var(--orange); }
|
|
1924
|
+
|
|
1925
|
+
/* ── Charts ── */
|
|
1926
|
+
.grid-2 { display: grid; grid-template-columns: 5fr 7fr; gap: 16px; margin-bottom: 16px; }
|
|
1927
|
+
.grid-full { margin-bottom: 16px; }
|
|
1928
|
+
.card {
|
|
1929
|
+
background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
|
|
1930
|
+
padding: 24px; transition: border-color 0.2s;
|
|
1931
|
+
}
|
|
1932
|
+
.card:hover { border-color: var(--border-light); }
|
|
1933
|
+
.card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
|
|
1934
|
+
.card-title { font-size: 14px; font-weight: 600; color: var(--text); }
|
|
1935
|
+
.card-subtitle { font-size: 12px; color: var(--text-muted); }
|
|
1936
|
+
|
|
1937
|
+
/* ── Chart containers ── */
|
|
1938
|
+
.chart-container { position: relative; width: 100%; }
|
|
1939
|
+
.chart-container.h-280 { height: 280px; }
|
|
1940
|
+
.chart-container.h-300 { height: 300px; }
|
|
1941
|
+
|
|
1942
|
+
/* ── Top Tools Bar ── */
|
|
1943
|
+
.tool-bars { display: flex; flex-direction: column; gap: 10px; }
|
|
1944
|
+
.tool-bar-row { display: flex; align-items: center; gap: 12px; }
|
|
1945
|
+
.tool-bar-name { width: 140px; font-size: 12px; font-family: 'SF Mono', 'Fira Code', monospace; color: var(--text-secondary); text-align: right; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1946
|
+
.tool-bar-track { flex: 1; height: 28px; background: var(--surface); border-radius: 6px; overflow: hidden; position: relative; }
|
|
1947
|
+
.tool-bar-fill { height: 100%; border-radius: 6px; display: flex; align-items: center; padding: 0 10px; font-size: 11px; font-weight: 600; color: #fff; min-width: fit-content; transition: width 0.6s ease; }
|
|
1948
|
+
.tool-bar-count { font-size: 12px; color: var(--text-muted); min-width: 36px; text-align: right; }
|
|
1949
|
+
|
|
1950
|
+
/* ── Recent Table ── */
|
|
1951
|
+
.table-wrapper { max-height: 400px; overflow-y: auto; border-radius: var(--radius-sm); }
|
|
1952
|
+
.table-wrapper::-webkit-scrollbar { width: 6px; }
|
|
1953
|
+
.table-wrapper::-webkit-scrollbar-track { background: transparent; }
|
|
1954
|
+
.table-wrapper::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 3px; }
|
|
1955
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
1956
|
+
thead { position: sticky; top: 0; z-index: 1; }
|
|
1957
|
+
th {
|
|
1958
|
+
text-align: left; color: var(--text-muted); font-weight: 500; padding: 10px 16px;
|
|
1959
|
+
background: var(--card); border-bottom: 1px solid var(--border); font-size: 11px;
|
|
1960
|
+
text-transform: uppercase; letter-spacing: 0.5px;
|
|
1961
|
+
}
|
|
1962
|
+
td { padding: 10px 16px; border-bottom: 1px solid var(--border); }
|
|
1963
|
+
tr:hover td { background: var(--surface); }
|
|
1964
|
+
.tool-badge {
|
|
1965
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
1966
|
+
background: var(--accent-dim); color: var(--accent); padding: 3px 10px;
|
|
1967
|
+
border-radius: 6px; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px; font-weight: 500;
|
|
1968
|
+
}
|
|
1969
|
+
.when-text { color: var(--text-muted); }
|
|
1970
|
+
.duration-text { color: var(--text-secondary); font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; }
|
|
1971
|
+
.saved-text { color: var(--green); font-weight: 600; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; }
|
|
1972
|
+
|
|
1973
|
+
/* ── Footer ── */
|
|
1974
|
+
.footer { text-align: center; padding: 24px 0 0; color: var(--text-muted); font-size: 12px; }
|
|
1975
|
+
|
|
1976
|
+
/* ── Tabs ── */
|
|
1977
|
+
.tab-nav { display: flex; gap: 4px; margin-bottom: 28px; border-bottom: 2px solid var(--border); padding-bottom: 0; }
|
|
1978
|
+
.tab { padding: 10px 24px; cursor: pointer; font-size: 14px; font-weight: 500; color: var(--text-muted); border-bottom: 2px solid transparent; margin-bottom: -2px; transition: all 0.2s; background: none; border-top: none; border-left: none; border-right: none; outline: none; }
|
|
1979
|
+
.tab:hover { color: var(--text-secondary); }
|
|
1980
|
+
.tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
1981
|
+
.tab-content { display: none; }
|
|
1982
|
+
.tab-content.active { display: block; }
|
|
1983
|
+
|
|
1984
|
+
/* ── Learning ── */
|
|
1985
|
+
.suggestion-box { background: var(--accent-dim); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 20px; text-align: center; margin-bottom: 20px; }
|
|
1986
|
+
.suggestion-box code { background: var(--surface); padding: 2px 8px; border-radius: 4px; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px; color: var(--accent); }
|
|
1987
|
+
.learning-section { margin-bottom: 16px; }
|
|
1988
|
+
.annot-list { list-style: none; padding: 0; }
|
|
1989
|
+
.annot-list li { padding: 10px 0; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
|
|
1990
|
+
.annot-list li:last-child { border-bottom: none; }
|
|
1991
|
+
.annot-sym { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px; color: var(--accent); }
|
|
1992
|
+
.annot-key { font-size: 11px; color: var(--text-muted); background: var(--surface); padding: 2px 8px; border-radius: 4px; margin-left: 8px; }
|
|
1993
|
+
.annot-conf { font-size: 12px; font-weight: 600; }
|
|
1994
|
+
.annot-conf.hi { color: var(--green); }
|
|
1995
|
+
.annot-conf.md { color: var(--orange); }
|
|
1996
|
+
.annot-conf.lo { color: var(--red); }
|
|
1997
|
+
|
|
1998
|
+
/* ── Responsive ── */
|
|
1999
|
+
@media (max-width: 1024px) { .kpi-grid { grid-template-columns: repeat(2, 1fr); } .grid-2 { grid-template-columns: 1fr; } }
|
|
2000
|
+
@media (max-width: 640px) { .kpi-grid { grid-template-columns: 1fr; } .wrapper { padding: 16px 12px 80px; } }
|
|
2001
|
+
</style>
|
|
2002
|
+
</head>
|
|
2003
|
+
<body>
|
|
2004
|
+
<div class="wrapper">
|
|
2005
|
+
|
|
2006
|
+
<!-- Header -->
|
|
2007
|
+
<div class="header">
|
|
2008
|
+
<div class="header-left">
|
|
2009
|
+
<div class="logo">m</div>
|
|
2010
|
+
<h1>milens <span>dashboard</span></h1>
|
|
2011
|
+
</div>
|
|
2012
|
+
<div class="header-right">
|
|
2013
|
+
<span class="repo-badge" style="background:rgba(88,166,255,0.12);color:#58a6ff;border:1px solid rgba(88,166,255,0.25);border-radius:20px;padding:4px 12px;font-size:0.78em;font-weight:600;margin-right:12px;">${repoFilter ? repoFilter.replace(/\\\\/g, '/').split('/').pop() || repoFilter : 'All Repos'}</span>
|
|
2014
|
+
<div class="status-dot"></div>
|
|
2015
|
+
<span class="status-text">Live</span>
|
|
2016
|
+
<button class="refresh-btn" onclick="refreshData()">↻ Refresh</button>
|
|
2017
|
+
</div>
|
|
2018
|
+
</div>
|
|
2019
|
+
|
|
2020
|
+
<!-- Tab Navigation -->
|
|
2021
|
+
<div class="tab-nav">
|
|
2022
|
+
<button class="tab active" data-tab="usage" onclick="switchTab('usage')">Usage Analytics</button>
|
|
2023
|
+
<button class="tab" data-tab="learning" onclick="switchTab('learning')">Learning</button>
|
|
2024
|
+
</div>
|
|
2025
|
+
|
|
2026
|
+
<div id="tab-usage" class="tab-content active">
|
|
2027
|
+
|
|
2028
|
+
<!-- KPIs -->
|
|
2029
|
+
<div class="kpi-grid">
|
|
2030
|
+
<div class="kpi">
|
|
2031
|
+
<div class="kpi-icon blue">⚙</div>
|
|
2032
|
+
<div class="value blue" id="kpi-calls">${fmtNum(stats.totalCalls)}</div>
|
|
2033
|
+
<div class="label">Tool Calls</div>
|
|
2034
|
+
<div class="sub" id="kpi-calls-sub">${stats.totalCalls.toLocaleString()} total invocations</div>
|
|
2035
|
+
<div class="kpi-glow blue"></div>
|
|
2036
|
+
</div>
|
|
2037
|
+
<div class="kpi">
|
|
2038
|
+
<div class="kpi-icon green">⚡</div>
|
|
2039
|
+
<div class="value green" id="kpi-saved">${fmtNum(stats.totalTokensSaved)}</div>
|
|
2040
|
+
<div class="label">Tokens Saved</div>
|
|
2041
|
+
<div class="sub" id="kpi-saved-sub">${stats.totalTokensSaved.toLocaleString()} tokens not wasted</div>
|
|
2042
|
+
<div class="kpi-glow green"></div>
|
|
2043
|
+
</div>
|
|
2044
|
+
<div class="kpi">
|
|
2045
|
+
<div class="kpi-icon purple">★</div>
|
|
2046
|
+
<div class="value purple" id="kpi-pct">${savingsPercent}%</div>
|
|
2047
|
+
<div class="label">Token Efficiency</div>
|
|
2048
|
+
<div class="sub">${fmtNum(stats.totalTokensOut)} returned vs ${fmtNum(stats.totalTokensSaved)} saved</div>
|
|
2049
|
+
<div class="kpi-glow purple"></div>
|
|
2050
|
+
</div>
|
|
2051
|
+
<div class="kpi">
|
|
2052
|
+
<div class="kpi-icon orange">⏱</div>
|
|
2053
|
+
<div class="value orange" id="kpi-avg">${stats.totalCalls > 0 ? Math.round(stats.totalDurationMs / stats.totalCalls) : 0}ms</div>
|
|
2054
|
+
<div class="label">Avg Response Time</div>
|
|
2055
|
+
<div class="sub">${(stats.totalDurationMs / 1000).toFixed(1)}s total processing</div>
|
|
2056
|
+
<div class="kpi-glow orange"></div>
|
|
2057
|
+
</div>
|
|
2058
|
+
</div>
|
|
2059
|
+
|
|
2060
|
+
<!-- Charts Row -->
|
|
2061
|
+
<div class="grid-2">
|
|
2062
|
+
<div class="card">
|
|
2063
|
+
<div class="card-header">
|
|
2064
|
+
<div>
|
|
2065
|
+
<div class="card-title">Top Tools</div>
|
|
2066
|
+
<div class="card-subtitle">By number of calls</div>
|
|
2067
|
+
</div>
|
|
2068
|
+
</div>
|
|
2069
|
+
<div id="toolBars" class="tool-bars"></div>
|
|
2070
|
+
</div>
|
|
2071
|
+
<div class="card">
|
|
2072
|
+
<div class="card-header">
|
|
2073
|
+
<div>
|
|
2074
|
+
<div class="card-title">Daily Activity</div>
|
|
2075
|
+
<div class="card-subtitle">Calls & tokens saved over the last 30 days</div>
|
|
2076
|
+
</div>
|
|
2077
|
+
</div>
|
|
2078
|
+
<div class="chart-container h-280"><canvas id="dayChart"></canvas></div>
|
|
2079
|
+
</div>
|
|
2080
|
+
</div>
|
|
2081
|
+
|
|
2082
|
+
<!-- Savings Distribution -->
|
|
2083
|
+
<div class="grid-2" style="grid-template-columns: 7fr 5fr;">
|
|
2084
|
+
<div class="card">
|
|
2085
|
+
<div class="card-header">
|
|
2086
|
+
<div>
|
|
2087
|
+
<div class="card-title">Recent Tool Calls</div>
|
|
2088
|
+
<div class="card-subtitle">Last 50 invocations</div>
|
|
2089
|
+
</div>
|
|
2090
|
+
</div>
|
|
2091
|
+
<div class="table-wrapper">
|
|
2092
|
+
<table>
|
|
2093
|
+
<thead><tr><th>Tool</th><th>When</th><th>Duration</th><th style="text-align:right">Tokens Saved</th></tr></thead>
|
|
2094
|
+
<tbody id="recentBody"></tbody>
|
|
2095
|
+
</table>
|
|
2096
|
+
</div>
|
|
2097
|
+
</div>
|
|
2098
|
+
<div class="card">
|
|
2099
|
+
<div class="card-header">
|
|
2100
|
+
<div>
|
|
2101
|
+
<div class="card-title">Savings by Tool</div>
|
|
2102
|
+
<div class="card-subtitle">Token savings distribution</div>
|
|
2103
|
+
</div>
|
|
2104
|
+
</div>
|
|
2105
|
+
<div class="chart-container h-300"><canvas id="savingsChart"></canvas></div>
|
|
2106
|
+
</div>
|
|
2107
|
+
</div>
|
|
2108
|
+
|
|
2109
|
+
<div class="footer">milens · auto-refreshes every 30s</div>
|
|
2110
|
+
</div><!-- /tab-usage -->
|
|
2111
|
+
|
|
2112
|
+
<div id="tab-learning" class="tab-content">
|
|
2113
|
+
<div class="suggestion-box">
|
|
2114
|
+
<h3 style="margin-bottom:8px;font-weight:600;">Metrics & Insights</h3>
|
|
2115
|
+
<p style="color:var(--text-secondary);font-size:13px;">Run <code>milens metrics</code> for a full code-quality metrics report.</p>
|
|
2116
|
+
</div>
|
|
2117
|
+
${learningStats}
|
|
2118
|
+
</div>
|
|
2119
|
+
|
|
2120
|
+
</div><!-- /wrapper -->
|
|
2121
|
+
|
|
2122
|
+
<script>
|
|
2123
|
+
const COLORS = ['#60a5fa','#34d399','#fbbf24','#a78bfa','#f87171','#2dd4bf','#818cf8','#fb923c','#e879f9','#38bdf8','#4ade80','#facc15','#f472b6','#22d3ee','#a3e635','#c084fc'];
|
|
2124
|
+
const BAR_COLORS = ['#60a5fa','#34d399','#fbbf24','#a78bfa','#f87171','#2dd4bf','#818cf8','#fb923c','#e879f9','#38bdf8'];
|
|
2125
|
+
let byTool = ${byToolJson};
|
|
2126
|
+
let byDay = ${byDayJson};
|
|
2127
|
+
|
|
2128
|
+
function fmtK(n) { return n >= 1e6 ? (n/1e6).toFixed(1)+'M' : n >= 1e3 ? (n/1e3).toFixed(1)+'K' : n; }
|
|
2129
|
+
|
|
2130
|
+
/* ── Top Tools Horizontal Bars ── */
|
|
2131
|
+
function renderToolBars() {
|
|
2132
|
+
const el = document.getElementById('toolBars');
|
|
2133
|
+
const sorted = [...byTool].sort((a,b) => b.calls - a.calls).slice(0, 10);
|
|
2134
|
+
const maxCalls = sorted[0]?.calls || 1;
|
|
2135
|
+
el.innerHTML = sorted.map((t, i) => {
|
|
2136
|
+
const pct = Math.max(8, (t.calls / maxCalls) * 100);
|
|
2137
|
+
const col = BAR_COLORS[i % BAR_COLORS.length];
|
|
2138
|
+
return \`<div class="tool-bar-row">
|
|
2139
|
+
<span class="tool-bar-name">\${t.tool}</span>
|
|
2140
|
+
<div class="tool-bar-track">
|
|
2141
|
+
<div class="tool-bar-fill" style="width:\${pct}%;background:linear-gradient(90deg,\${col}dd,\${col}88)">\${t.calls}</div>
|
|
2142
|
+
</div>
|
|
2143
|
+
<span class="tool-bar-count">\${fmtK(t.tokensSaved)}</span>
|
|
2144
|
+
</div>\`;
|
|
2145
|
+
}).join('');
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
/* ── Daily Activity Chart ── */
|
|
2149
|
+
let dayChartInstance = null;
|
|
2150
|
+
function renderDayChart() {
|
|
2151
|
+
if (dayChartInstance) dayChartInstance.destroy();
|
|
2152
|
+
const ctx = document.getElementById('dayChart');
|
|
2153
|
+
const labels = byDay.map(d => {
|
|
2154
|
+
const parts = d.date.split('-');
|
|
2155
|
+
return parts[1] + '/' + parts[2];
|
|
2156
|
+
});
|
|
2157
|
+
dayChartInstance = new Chart(ctx, {
|
|
2158
|
+
type: 'bar',
|
|
2159
|
+
data: {
|
|
2160
|
+
labels,
|
|
2161
|
+
datasets: [
|
|
2162
|
+
{
|
|
2163
|
+
label: 'Calls', data: byDay.map(d => d.calls),
|
|
2164
|
+
backgroundColor: '#60a5fa44', hoverBackgroundColor: '#60a5fa88',
|
|
2165
|
+
borderRadius: 4, borderSkipped: false, yAxisID: 'y', barPercentage: 0.7,
|
|
2166
|
+
},
|
|
2167
|
+
{
|
|
2168
|
+
label: 'Tokens Saved', data: byDay.map(d => d.tokensSaved),
|
|
2169
|
+
type: 'line', borderColor: '#34d399', pointBackgroundColor: '#34d399',
|
|
2170
|
+
pointRadius: 2, pointHoverRadius: 5, borderWidth: 2.5,
|
|
2171
|
+
yAxisID: 'y1', tension: 0.4, fill: { target: 'origin', above: '#34d39910' },
|
|
2172
|
+
},
|
|
2173
|
+
],
|
|
2174
|
+
},
|
|
2175
|
+
options: {
|
|
2176
|
+
responsive: true, maintainAspectRatio: false,
|
|
2177
|
+
interaction: { mode: 'index', intersect: false },
|
|
2178
|
+
scales: {
|
|
2179
|
+
x: {
|
|
2180
|
+
ticks: { color: '#64748b', font: { size: 11 }, maxRotation: 0, autoSkip: true, maxTicksLimit: 15 },
|
|
2181
|
+
grid: { display: false },
|
|
2182
|
+
},
|
|
2183
|
+
y: {
|
|
2184
|
+
position: 'left', ticks: { color: '#60a5fa', font: { size: 11 } },
|
|
2185
|
+
grid: { color: '#1e2a3a' }, title: { display: true, text: 'Calls', color: '#60a5fa', font: { size: 11 } },
|
|
2186
|
+
},
|
|
2187
|
+
y1: {
|
|
2188
|
+
position: 'right', ticks: { color: '#34d399', font: { size: 11 }, callback: v => fmtK(v) },
|
|
2189
|
+
grid: { drawOnChartArea: false }, title: { display: true, text: 'Tokens Saved', color: '#34d399', font: { size: 11 } },
|
|
2190
|
+
},
|
|
2191
|
+
},
|
|
2192
|
+
plugins: {
|
|
2193
|
+
legend: { labels: { color: '#94a3b8', boxWidth: 12, usePointStyle: true, padding: 16 } },
|
|
2194
|
+
tooltip: {
|
|
2195
|
+
backgroundColor: '#1e293b', borderColor: '#334155', borderWidth: 1, titleColor: '#e2e8f0',
|
|
2196
|
+
bodyColor: '#94a3b8', cornerRadius: 8, padding: 12,
|
|
2197
|
+
callbacks: { label: ctx => ctx.dataset.label + ': ' + (ctx.datasetIndex === 1 ? fmtK(ctx.raw) : ctx.raw) },
|
|
2198
|
+
},
|
|
2199
|
+
},
|
|
2200
|
+
},
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
/* ── Savings Doughnut ── */
|
|
2205
|
+
function renderSavingsChart() {
|
|
2206
|
+
const ctx = document.getElementById('savingsChart');
|
|
2207
|
+
const sorted = [...byTool].sort((a,b) => b.tokensSaved - a.tokensSaved);
|
|
2208
|
+
const top = sorted.slice(0, 8);
|
|
2209
|
+
const rest = sorted.slice(8);
|
|
2210
|
+
if (rest.length) top.push({ tool: 'others', tokensSaved: rest.reduce((s,t) => s + t.tokensSaved, 0), calls: 0 });
|
|
2211
|
+
new Chart(ctx, {
|
|
2212
|
+
type: 'doughnut',
|
|
2213
|
+
data: {
|
|
2214
|
+
labels: top.map(t => t.tool),
|
|
2215
|
+
datasets: [{
|
|
2216
|
+
data: top.map(t => t.tokensSaved),
|
|
2217
|
+
backgroundColor: top.map((_, i) => COLORS[i]),
|
|
2218
|
+
borderWidth: 0, hoverOffset: 6,
|
|
2219
|
+
}],
|
|
2220
|
+
},
|
|
2221
|
+
options: {
|
|
2222
|
+
responsive: true, cutout: '68%',
|
|
2223
|
+
plugins: {
|
|
2224
|
+
legend: { position: 'right', labels: { color: '#94a3b8', font: { size: 12 }, padding: 8, usePointStyle: true, pointStyleWidth: 10 } },
|
|
2225
|
+
tooltip: {
|
|
2226
|
+
backgroundColor: '#1e293b', borderColor: '#334155', borderWidth: 1, titleColor: '#e2e8f0',
|
|
2227
|
+
bodyColor: '#94a3b8', cornerRadius: 8, padding: 12,
|
|
2228
|
+
callbacks: { label: ctx => ' ' + ctx.label + ': ' + fmtK(ctx.raw) + ' tokens' },
|
|
2229
|
+
},
|
|
2230
|
+
},
|
|
2231
|
+
},
|
|
2232
|
+
});
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
/* ── Recent Calls Table ── */
|
|
2236
|
+
function renderRecent(data) {
|
|
2237
|
+
const tbody = document.getElementById('recentBody');
|
|
2238
|
+
tbody.innerHTML = data.map(r => {
|
|
2239
|
+
const ago = timeAgo(r.calledAt);
|
|
2240
|
+
return \`<tr>
|
|
2241
|
+
<td><span class="tool-badge">\${r.tool}</span></td>
|
|
2242
|
+
<td class="when-text">\${ago}</td>
|
|
2243
|
+
<td class="duration-text">\${r.durationMs}ms</td>
|
|
2244
|
+
<td class="saved-text" style="text-align:right">+\${r.tokensSaved.toLocaleString()}</td>
|
|
2245
|
+
</tr>\`;
|
|
2246
|
+
}).join('');
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
function timeAgo(iso) {
|
|
2250
|
+
const d = new Date(iso.includes('T') ? iso : iso + 'Z');
|
|
2251
|
+
const s = Math.floor((Date.now() - d.getTime()) / 1000);
|
|
2252
|
+
if (s < 0) return 'just now';
|
|
2253
|
+
if (s < 60) return s + 's ago';
|
|
2254
|
+
if (s < 3600) return Math.floor(s/60) + 'm ago';
|
|
2255
|
+
if (s < 86400) return Math.floor(s/3600) + 'h ago';
|
|
2256
|
+
return Math.floor(s/86400) + 'd ago';
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
/* ── Refresh ── */
|
|
2260
|
+
async function refreshData() {
|
|
2261
|
+
const btn = document.querySelector('.refresh-btn');
|
|
2262
|
+
btn.textContent = '⟳ Loading...';
|
|
2263
|
+
try {
|
|
2264
|
+
const res = await fetch('/api/stats');
|
|
2265
|
+
const data = await res.json();
|
|
2266
|
+
byTool = data.byTool; byDay = data.byDay;
|
|
2267
|
+
document.getElementById('kpi-calls').textContent = fmtK(data.totalCalls);
|
|
2268
|
+
document.getElementById('kpi-calls-sub').textContent = data.totalCalls.toLocaleString() + ' total invocations';
|
|
2269
|
+
document.getElementById('kpi-saved').textContent = fmtK(data.totalTokensSaved);
|
|
2270
|
+
document.getElementById('kpi-saved-sub').textContent = data.totalTokensSaved.toLocaleString() + ' tokens not wasted';
|
|
2271
|
+
const pct = data.totalTokensOut > 0 ? Math.round((data.totalTokensSaved / (data.totalTokensOut + data.totalTokensSaved)) * 100) : 0;
|
|
2272
|
+
document.getElementById('kpi-pct').textContent = pct + '%';
|
|
2273
|
+
document.getElementById('kpi-avg').textContent = (data.totalCalls > 0 ? Math.round(data.totalDurationMs / data.totalCalls) : 0) + 'ms';
|
|
2274
|
+
renderToolBars(); renderDayChart(); renderRecent(data.recentCalls);
|
|
2275
|
+
} catch(e) { console.error('Refresh failed', e); }
|
|
2276
|
+
btn.innerHTML = '↻ Refresh';
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
/* ── Tab Switching ── */
|
|
2280
|
+
function switchTab(tab) {
|
|
2281
|
+
document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab));
|
|
2282
|
+
document.querySelectorAll('.tab-content').forEach(c => c.classList.toggle('active', c.id === 'tab-' + tab));
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
/* ── Init ── */
|
|
2286
|
+
renderToolBars();
|
|
2287
|
+
renderDayChart();
|
|
2288
|
+
renderSavingsChart();
|
|
2289
|
+
renderRecent(${recentJson});
|
|
2290
|
+
setInterval(refreshData, 30000);
|
|
2291
|
+
</script>
|
|
2292
|
+
</body>
|
|
2293
2293
|
</html>`;
|
|
2294
2294
|
}
|
|
2295
2295
|
function renderConfBars(bands, total) {
|