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.
Files changed (52) hide show
  1. package/.agents/skills/adapters/SKILL.md +59 -59
  2. package/.agents/skills/analyzer/SKILL.md +83 -83
  3. package/.agents/skills/apps/SKILL.md +65 -65
  4. package/.agents/skills/docs/SKILL.md +63 -63
  5. package/.agents/skills/milens/SKILL.md +322 -322
  6. package/.agents/skills/milens-security-review/SKILL.md +224 -224
  7. package/.agents/skills/orchestrator/SKILL.md +64 -64
  8. package/.agents/skills/parser/SKILL.md +86 -86
  9. package/.agents/skills/root/SKILL.md +89 -89
  10. package/.agents/skills/scripts/SKILL.md +56 -56
  11. package/.agents/skills/security/SKILL.md +67 -67
  12. package/.agents/skills/server/SKILL.md +84 -84
  13. package/.agents/skills/store/SKILL.md +77 -77
  14. package/.agents/skills/test/SKILL.md +155 -155
  15. package/.agents/skills/ui/SKILL.md +56 -56
  16. package/README.md +577 -577
  17. package/adapters/README.md +144 -144
  18. package/adapters/claude-code/.claude/mcp.json +9 -9
  19. package/adapters/claude-code/.claude/settings.json.hooks-snippet.json +26 -26
  20. package/adapters/claude-code/.claude-plugin/plugin.json +11 -11
  21. package/adapters/claude-code/.mcp.json +9 -9
  22. package/adapters/claude-code/CLAUDE.md +81 -81
  23. package/adapters/claude-code/hooks/hooks.json +27 -27
  24. package/adapters/codex/.codex/config.toml +3 -3
  25. package/adapters/copilot/.vscode/mcp.json +10 -10
  26. package/adapters/cursor/.cursor/mcp.json +9 -9
  27. package/adapters/cursor/.cursorrules +69 -69
  28. package/adapters/gemini/.gemini/context.md +81 -81
  29. package/adapters/gemini/.gemini/settings.json +9 -9
  30. package/adapters/opencode/.opencode/config.json +8 -8
  31. package/adapters/zed/.zed/settings.json +8 -8
  32. package/dist/build-info.d.ts +2 -2
  33. package/dist/build-info.js +2 -2
  34. package/dist/cli.js +482 -482
  35. package/dist/parser/lang-css.js +9 -9
  36. package/dist/parser/lang-go.js +58 -58
  37. package/dist/parser/lang-html.js +3 -3
  38. package/dist/parser/lang-java.js +37 -37
  39. package/dist/parser/lang-js.js +105 -105
  40. package/dist/parser/lang-php.js +45 -45
  41. package/dist/parser/lang-py.js +43 -43
  42. package/dist/parser/lang-ruby.js +28 -28
  43. package/dist/parser/lang-rust.js +48 -48
  44. package/dist/parser/lang-ts.js +210 -210
  45. package/dist/server/mcp-prompts.js +502 -502
  46. package/dist/server/mcp.js +56 -56
  47. package/dist/skills.js +296 -296
  48. package/dist/store/annotations.js +1 -1
  49. package/dist/store/db.js +224 -224
  50. package/dist/store/schema.sql +144 -144
  51. package/dist/store/vectors.js +2 -2
  52. 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()">&#8635; 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">&#9881;</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">&#9889;</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">&#9733;</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">&#9201;</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 &amp; 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 &middot; 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 &amp; 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 = '&#8635; 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()">&#8635; 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">&#9881;</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">&#9889;</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">&#9733;</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">&#9201;</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 &amp; 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 &middot; 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 &amp; 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 = '&#8635; 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) {