milens 0.7.1 → 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 -54
- package/.agents/skills/analyzer/SKILL.md +83 -78
- package/.agents/skills/apps/SKILL.md +65 -60
- package/.agents/skills/docs/SKILL.md +63 -58
- package/.agents/skills/milens/SKILL.md +322 -319
- package/.agents/skills/milens-security-review/SKILL.md +224 -224
- package/.agents/skills/orchestrator/SKILL.md +64 -59
- package/.agents/skills/parser/SKILL.md +86 -81
- package/.agents/skills/root/SKILL.md +89 -84
- package/.agents/skills/scripts/SKILL.md +56 -51
- package/.agents/skills/security/SKILL.md +67 -62
- package/.agents/skills/server/SKILL.md +84 -77
- package/.agents/skills/store/SKILL.md +77 -72
- package/.agents/skills/test/SKILL.md +155 -148
- package/.agents/skills/ui/SKILL.md +56 -51
- 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 -79
- package/adapters/claude-code/hooks/hooks.json +27 -0
- 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/agents-md.d.ts +1 -0
- package/dist/agents-md.js +45 -1
- package/dist/agents-md.js.map +1 -1
- package/dist/analyzer/config.js +32 -0
- package/dist/analyzer/config.js.map +1 -1
- package/dist/analyzer/engine.js +26 -2
- package/dist/analyzer/engine.js.map +1 -1
- package/dist/analyzer/enrich.js +68 -1
- package/dist/analyzer/enrich.js.map +1 -1
- package/dist/analyzer/resolver.d.ts +4 -1
- package/dist/analyzer/resolver.js +241 -38
- package/dist/analyzer/resolver.js.map +1 -1
- package/dist/analyzer/review.js +32 -2
- package/dist/analyzer/review.js.map +1 -1
- package/dist/analyzer/scanner.js +3 -0
- package/dist/analyzer/scanner.js.map +1 -1
- package/dist/analyzer/scope-resolver.js +1 -1
- package/dist/analyzer/scope-resolver.js.map +1 -1
- package/dist/build-info.d.ts +2 -2
- package/dist/build-info.js +2 -2
- package/dist/cli.js +566 -497
- package/dist/cli.js.map +1 -1
- package/dist/orchestrator/orchestrator.d.ts +1 -1
- package/dist/orchestrator/orchestrator.js +14 -6
- package/dist/orchestrator/orchestrator.js.map +1 -1
- package/dist/parser/extract.d.ts +7 -0
- package/dist/parser/extract.js +23 -1
- package/dist/parser/extract.js.map +1 -1
- package/dist/parser/lang-css.js +18 -6
- package/dist/parser/lang-css.js.map +1 -1
- 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 +120 -109
- package/dist/parser/lang-js.js.map +1 -1
- package/dist/parser/lang-md.js +1 -0
- package/dist/parser/lang-md.js.map +1 -1
- 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 +230 -196
- package/dist/parser/lang-ts.js.map +1 -1
- package/dist/parser/lang-vue.js +0 -21
- package/dist/parser/lang-vue.js.map +1 -1
- package/dist/security/rules.js +22 -23
- package/dist/security/rules.js.map +1 -1
- package/dist/server/guard-hook.d.ts +10 -0
- package/dist/server/guard-hook.js +179 -0
- package/dist/server/guard-hook.js.map +1 -0
- package/dist/server/hooks.js +2 -1
- package/dist/server/hooks.js.map +1 -1
- package/dist/server/mcp-prompts.js +502 -502
- package/dist/server/mcp.js +228 -86
- package/dist/server/mcp.js.map +1 -1
- package/dist/server/test-plan.d.ts +1 -0
- package/dist/server/test-plan.js +25 -7
- package/dist/server/test-plan.js.map +1 -1
- package/dist/server/tools/findings-report.d.ts +3 -0
- package/dist/server/tools/findings-report.js +170 -0
- package/dist/server/tools/findings-report.js.map +1 -0
- package/dist/server/tools/security.js +61 -5
- package/dist/server/tools/security.js.map +1 -1
- package/dist/server/tools/session.js +37 -17
- package/dist/server/tools/session.js.map +1 -1
- package/dist/server/tools/testing.js +58 -11
- package/dist/server/tools/testing.js.map +1 -1
- package/dist/skills.js +307 -301
- package/dist/skills.js.map +1 -1
- package/dist/store/annotations.d.ts +5 -0
- package/dist/store/annotations.js +45 -9
- package/dist/store/annotations.js.map +1 -1
- package/dist/store/db.d.ts +13 -0
- package/dist/store/db.js +318 -205
- package/dist/store/db.js.map +1 -1
- package/dist/store/registry.d.ts +1 -0
- package/dist/store/registry.js +17 -6
- package/dist/store/registry.js.map +1 -1
- package/dist/store/schema.sql +144 -143
- package/dist/store/vectors.js +2 -2
- package/dist/types.d.ts +16 -1
- package/dist/uninstall.d.ts +1 -0
- package/dist/uninstall.js +33 -10
- package/dist/uninstall.js.map +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +0 -0
- package/dist/utils.js.map +1 -1
- package/package.json +85 -85
package/dist/cli.js
CHANGED
|
@@ -235,6 +235,7 @@ program
|
|
|
235
235
|
.description('Show index status')
|
|
236
236
|
.option('-p, --path <path>', 'Repository root path', '.')
|
|
237
237
|
.action(async (opts) => {
|
|
238
|
+
const { existsSync } = await import('node:fs');
|
|
238
239
|
const { Database } = await import('./store/db.js');
|
|
239
240
|
const { RepoRegistry } = await import('./store/registry.js');
|
|
240
241
|
const reg = new RepoRegistry();
|
|
@@ -243,6 +244,10 @@ program
|
|
|
243
244
|
console.log('Not indexed.');
|
|
244
245
|
return;
|
|
245
246
|
}
|
|
247
|
+
if (!existsSync(entry.dbPath)) {
|
|
248
|
+
console.log(`Not indexed. Run \`milens analyze\` first.\n(Registry entry stale — directory may have been deleted: ${entry.rootPath})`);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
246
251
|
const db = new Database(entry.dbPath);
|
|
247
252
|
const stats = db.getStats();
|
|
248
253
|
console.log(`Repository: ${entry.rootPath}`);
|
|
@@ -1279,10 +1284,11 @@ program
|
|
|
1279
1284
|
console.log(' (run `milens analyze -p . --force` manually if this fails)');
|
|
1280
1285
|
}
|
|
1281
1286
|
console.log('Step 2/4: Generating AGENTS.md...');
|
|
1287
|
+
let db = null;
|
|
1282
1288
|
try {
|
|
1283
1289
|
const dbPath = new RepoRegistry().findDbPath(root);
|
|
1284
1290
|
if (dbPath) {
|
|
1285
|
-
|
|
1291
|
+
db = new Database(dbPath);
|
|
1286
1292
|
const { generateAgentsMd } = await import('./agents-md.js');
|
|
1287
1293
|
const agentsMd = generateAgentsMd(db, root);
|
|
1288
1294
|
const { writeFileSync, mkdirSync, existsSync } = await import('node:fs');
|
|
@@ -1290,7 +1296,6 @@ program
|
|
|
1290
1296
|
mkdirSync(root, { recursive: true });
|
|
1291
1297
|
writeFileSync(resolve(root, 'AGENTS.md'), agentsMd);
|
|
1292
1298
|
console.log(' ✓ AGENTS.md created');
|
|
1293
|
-
db.close();
|
|
1294
1299
|
}
|
|
1295
1300
|
}
|
|
1296
1301
|
catch (e) {
|
|
@@ -1299,56 +1304,69 @@ program
|
|
|
1299
1304
|
if (opts.profile !== 'minimal') {
|
|
1300
1305
|
console.log('Step 3/4: Installing skill files...');
|
|
1301
1306
|
try {
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
if (!existsSync(skillDst))
|
|
1309
|
-
mkdirSync(skillDst, { recursive: true });
|
|
1310
|
-
cpSync(skillSrc, skillDst, { recursive: true });
|
|
1311
|
-
console.log(' ✓ Skill files installed');
|
|
1307
|
+
if (db) {
|
|
1308
|
+
const { generateSkills } = await import('./skills.js');
|
|
1309
|
+
const result = generateSkills(db, root, ['agents']);
|
|
1310
|
+
console.log(` ✓ Generated ${result.count} skill files`);
|
|
1311
|
+
for (const d of result.dirs)
|
|
1312
|
+
console.log(` ${d}`);
|
|
1312
1313
|
}
|
|
1313
1314
|
else {
|
|
1314
|
-
console.log(' ⚠
|
|
1315
|
+
console.log(' ⚠ Skills skipped: no database available (run analyze first)');
|
|
1315
1316
|
}
|
|
1316
1317
|
}
|
|
1317
1318
|
catch (e) {
|
|
1318
1319
|
console.log(` ⚠ Skill install skipped: ${e.message}`);
|
|
1319
1320
|
}
|
|
1320
1321
|
}
|
|
1322
|
+
if (db)
|
|
1323
|
+
db.close();
|
|
1321
1324
|
if (opts.profile === 'full' || (opts.with && opts.with.includes('hooks'))) {
|
|
1322
1325
|
console.log('Step 4/4: Installing git hooks...');
|
|
1323
1326
|
try {
|
|
1324
|
-
const { writeFileSync, existsSync, mkdirSync, chmodSync } = await import('node:fs');
|
|
1327
|
+
const { writeFileSync, existsSync, mkdirSync, chmodSync, readFileSync } = await import('node:fs');
|
|
1325
1328
|
const hooksDir = resolve(root, '.git', 'hooks');
|
|
1326
1329
|
if (!existsSync(hooksDir)) {
|
|
1327
1330
|
mkdirSync(hooksDir, { recursive: true });
|
|
1328
1331
|
}
|
|
1329
|
-
const
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
#
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1332
|
+
const hookPath = resolve(hooksDir, 'pre-commit');
|
|
1333
|
+
// Check for existing non-milens hook (e.g. Husky) before overwriting
|
|
1334
|
+
if (existsSync(hookPath)) {
|
|
1335
|
+
const existing = readFileSync(hookPath, 'utf-8');
|
|
1336
|
+
if (!existing.includes('milens') && !existing.includes('Auto-installed by milens')) {
|
|
1337
|
+
const isHusky = existing.includes('husky') || existing.includes('.husky');
|
|
1338
|
+
if (isHusky) {
|
|
1339
|
+
console.log(' ⚠ Skipped pre-commit hook — Husky hook already present (.husky/). Install manually with "milens hooks install".');
|
|
1340
|
+
}
|
|
1341
|
+
else {
|
|
1342
|
+
console.log(' ⚠ Skipped pre-commit hook — existing hook found. Install manually with "milens hooks install".');
|
|
1343
|
+
}
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
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."
|
|
1352
1370
|
`;
|
|
1353
1371
|
writeFileSync(resolve(hooksDir, 'pre-commit'), preCommitContent);
|
|
1354
1372
|
try {
|
|
@@ -1391,6 +1409,22 @@ echo "Milens: Done."
|
|
|
1391
1409
|
}
|
|
1392
1410
|
}
|
|
1393
1411
|
}
|
|
1412
|
+
// Ensure .milens/ is gitignored
|
|
1413
|
+
try {
|
|
1414
|
+
const { existsSync, readFileSync, appendFileSync, writeFileSync } = await import('node:fs');
|
|
1415
|
+
const gitignorePath = resolve(root, '.gitignore');
|
|
1416
|
+
const entry = '\n# milens code intelligence index (local, machine-specific)\n.milens/\n';
|
|
1417
|
+
if (existsSync(gitignorePath)) {
|
|
1418
|
+
const content = readFileSync(gitignorePath, 'utf-8');
|
|
1419
|
+
if (!content.includes('.milens')) {
|
|
1420
|
+
appendFileSync(gitignorePath, entry);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
else {
|
|
1424
|
+
writeFileSync(gitignorePath, entry.trimStart());
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
catch { /* best-effort */ }
|
|
1394
1428
|
console.log(`\n✓ Milens ${opts.profile} profile bootstrapped for ${root}`);
|
|
1395
1429
|
console.log('Next steps:');
|
|
1396
1430
|
console.log(' 1. Open project in your AI coding agent (Claude Code, OpenCode, etc.)');
|
|
@@ -1404,6 +1438,7 @@ program
|
|
|
1404
1438
|
.option('--repo <path>', 'Repository root path (alias for --path)')
|
|
1405
1439
|
.option('--agent <name>', 'Agent name (for session-start/session-end)')
|
|
1406
1440
|
.option('--hook <hook>', 'Hook name (sessionStart, sessionEnd, preCommit, fileChange, preCompact, postCompact)')
|
|
1441
|
+
.option('--mode <mode>', 'Enforcement mode (warn | strict) for guard-set-mode')
|
|
1407
1442
|
.action(async (action, opts) => {
|
|
1408
1443
|
const { HookManager, defaultOnSessionStart, defaultOnSessionEnd, defaultOnPreCompact, defaultOnPostCompact } = await import('./server/hooks.js');
|
|
1409
1444
|
const manager = new HookManager();
|
|
@@ -1536,8 +1571,42 @@ program
|
|
|
1536
1571
|
console.log(`Hook profile set to "${profileName}" for ${projectPath}`);
|
|
1537
1572
|
break;
|
|
1538
1573
|
}
|
|
1574
|
+
case 'guard-mark-checked': {
|
|
1575
|
+
const chunks = [];
|
|
1576
|
+
for await (const chunk of process.stdin) {
|
|
1577
|
+
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
|
|
1578
|
+
}
|
|
1579
|
+
const rawStdin = Buffer.concat(chunks).toString();
|
|
1580
|
+
const { handleMarkChecked } = await import('./server/guard-hook.js');
|
|
1581
|
+
handleMarkChecked(rawStdin, projectPath);
|
|
1582
|
+
break;
|
|
1583
|
+
}
|
|
1584
|
+
case 'guard-check-edit': {
|
|
1585
|
+
const chunks = [];
|
|
1586
|
+
for await (const chunk of process.stdin) {
|
|
1587
|
+
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
|
|
1588
|
+
}
|
|
1589
|
+
const rawStdin = Buffer.concat(chunks).toString();
|
|
1590
|
+
const { handleCheckEdit } = await import('./server/guard-hook.js');
|
|
1591
|
+
const result = handleCheckEdit(rawStdin, projectPath);
|
|
1592
|
+
process.stdout.write(result.stdout + '\n');
|
|
1593
|
+
if (result.exitCode !== 0)
|
|
1594
|
+
process.exit(result.exitCode);
|
|
1595
|
+
break;
|
|
1596
|
+
}
|
|
1597
|
+
case 'guard-set-mode': {
|
|
1598
|
+
const mode = opts.mode;
|
|
1599
|
+
if (mode !== 'warn' && mode !== 'strict') {
|
|
1600
|
+
console.error(`Invalid mode "${mode}". Use "warn" or "strict".`);
|
|
1601
|
+
process.exit(1);
|
|
1602
|
+
}
|
|
1603
|
+
const { writeMode } = await import('./server/guard-hook.js');
|
|
1604
|
+
writeMode(projectPath, mode);
|
|
1605
|
+
console.log(`Guard enforcement mode set to "${mode}" for ${projectPath}`);
|
|
1606
|
+
break;
|
|
1607
|
+
}
|
|
1539
1608
|
default:
|
|
1540
|
-
console.log(`Unknown action: ${action}. Use: enable, disable, list, profile, session-start, session-end, pre-compact, post-compact`);
|
|
1609
|
+
console.log(`Unknown action: ${action}. Use: enable, disable, list, profile, session-start, session-end, pre-compact, post-compact, guard-mark-checked, guard-check-edit, guard-set-mode`);
|
|
1541
1610
|
}
|
|
1542
1611
|
});
|
|
1543
1612
|
const securityCmd = program
|
|
@@ -1760,467 +1829,467 @@ function generateDashboardHtml(stats, annotationStats, repoFilter) {
|
|
|
1760
1829
|
: 0;
|
|
1761
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);
|
|
1762
1831
|
const confBars = annotationStats ? renderConfBars(annotationStats.confidenceBands, annotationStats.total) : '';
|
|
1763
|
-
const recentAnnots = annotationStats ? annotationStats.recent.map(a => `
|
|
1764
|
-
<li>
|
|
1765
|
-
<div><span class="annot-sym">${a.symbol}</span><span class="annot-key">${a.key}</span></div>
|
|
1766
|
-
<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>
|
|
1767
1836
|
</li>`).join('') : '';
|
|
1768
|
-
const learningStats = annotationStats ? `
|
|
1769
|
-
<div class="card learning-section">
|
|
1770
|
-
<div class="card-header">
|
|
1771
|
-
<div><div class="card-title">Confidence Distribution</div><div class="card-subtitle">${annotationStats.total} total annotations</div></div>
|
|
1772
|
-
</div>
|
|
1773
|
-
<div class="tool-bars">${confBars}</div>
|
|
1774
|
-
</div>
|
|
1775
|
-
<div class="card learning-section">
|
|
1776
|
-
<div class="card-header">
|
|
1777
|
-
<div><div class="card-title">Recent Annotations</div><div class="card-subtitle">Last ${annotationStats.recent.length}</div></div>
|
|
1778
|
-
</div>
|
|
1779
|
-
<ul class="annot-list">${recentAnnots}</ul>
|
|
1780
|
-
</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>
|
|
1781
1850
|
` : '';
|
|
1782
|
-
return `<!DOCTYPE html>
|
|
1783
|
-
<html lang="en">
|
|
1784
|
-
<head>
|
|
1785
|
-
<meta charset="UTF-8">
|
|
1786
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1787
|
-
<title>milens Dashboard</title>
|
|
1788
|
-
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
|
1789
|
-
<style>
|
|
1790
|
-
:root {
|
|
1791
|
-
--bg: #0a0e14; --surface: #12171e; --card: #161d27; --card-hover: #1a2332;
|
|
1792
|
-
--border: #1e2a3a; --border-light: #2a3a4e;
|
|
1793
|
-
--text: #e2e8f0; --text-secondary: #94a3b8; --text-muted: #64748b;
|
|
1794
|
-
--accent: #60a5fa; --accent-dim: #60a5fa22;
|
|
1795
|
-
--green: #34d399; --green-dim: #34d39915;
|
|
1796
|
-
--orange: #fbbf24; --orange-dim: #fbbf2415;
|
|
1797
|
-
--purple: #a78bfa; --purple-dim: #a78bfa15;
|
|
1798
|
-
--red: #f87171;
|
|
1799
|
-
--radius: 16px; --radius-sm: 10px;
|
|
1800
|
-
}
|
|
1801
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
1802
|
-
body {
|
|
1803
|
-
background: var(--bg); color: var(--text);
|
|
1804
|
-
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
1805
|
-
line-height: 1.5; min-height: 100vh;
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
/* ── Layout ── */
|
|
1809
|
-
.wrapper { max-width: 1400px; margin: 0 auto; padding: 32px 24px 80px; }
|
|
1810
|
-
|
|
1811
|
-
/* ── Header ── */
|
|
1812
|
-
.header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 32px; }
|
|
1813
|
-
.header-left { display: flex; align-items: center; gap: 14px; }
|
|
1814
|
-
.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; }
|
|
1815
|
-
.header h1 { font-size: 22px; font-weight: 700; letter-spacing: -0.3px; }
|
|
1816
|
-
.header h1 span { color: var(--text-muted); font-weight: 400; font-size: 14px; margin-left: 8px; }
|
|
1817
|
-
.header-right { display: flex; align-items: center; gap: 12px; }
|
|
1818
|
-
.status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); animation: pulse 2s infinite; }
|
|
1819
|
-
@keyframes pulse { 0%,80%,100% { opacity: 1; } 40% { opacity: 0.4; } }
|
|
1820
|
-
.status-text { font-size: 12px; color: var(--text-muted); }
|
|
1821
|
-
.refresh-btn {
|
|
1822
|
-
background: var(--accent-dim); color: var(--accent); border: 1px solid var(--border);
|
|
1823
|
-
border-radius: var(--radius-sm); padding: 8px 16px; cursor: pointer;
|
|
1824
|
-
font-weight: 500; font-size: 13px; transition: all 0.2s;
|
|
1825
|
-
}
|
|
1826
|
-
.refresh-btn:hover { background: var(--accent); color: #0a0e14; }
|
|
1827
|
-
|
|
1828
|
-
/* ── KPI Cards ── */
|
|
1829
|
-
.kpi-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 28px; }
|
|
1830
|
-
.kpi {
|
|
1831
|
-
background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
|
|
1832
|
-
padding: 24px; position: relative; overflow: hidden; transition: border-color 0.2s;
|
|
1833
|
-
}
|
|
1834
|
-
.kpi:hover { border-color: var(--border-light); }
|
|
1835
|
-
.kpi-icon { width: 40px; height: 40px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 20px; margin-bottom: 16px; }
|
|
1836
|
-
.kpi-icon.blue { background: var(--accent-dim); }
|
|
1837
|
-
.kpi-icon.green { background: var(--green-dim); }
|
|
1838
|
-
.kpi-icon.purple { background: var(--purple-dim); }
|
|
1839
|
-
.kpi-icon.orange { background: var(--orange-dim); }
|
|
1840
|
-
.kpi .value { font-size: 32px; font-weight: 800; letter-spacing: -1px; line-height: 1; }
|
|
1841
|
-
.kpi .value.blue { color: var(--accent); }
|
|
1842
|
-
.kpi .value.green { color: var(--green); }
|
|
1843
|
-
.kpi .value.purple { color: var(--purple); }
|
|
1844
|
-
.kpi .value.orange { color: var(--orange); }
|
|
1845
|
-
.kpi .label { color: var(--text-muted); font-size: 13px; margin-top: 6px; font-weight: 500; }
|
|
1846
|
-
.kpi .sub { color: var(--text-secondary); font-size: 12px; margin-top: 4px; }
|
|
1847
|
-
.kpi-glow {
|
|
1848
|
-
position: absolute; top: -40px; right: -40px; width: 120px; height: 120px;
|
|
1849
|
-
border-radius: 50%; opacity: 0.06; pointer-events: none;
|
|
1850
|
-
}
|
|
1851
|
-
.kpi-glow.blue { background: var(--accent); }
|
|
1852
|
-
.kpi-glow.green { background: var(--green); }
|
|
1853
|
-
.kpi-glow.purple { background: var(--purple); }
|
|
1854
|
-
.kpi-glow.orange { background: var(--orange); }
|
|
1855
|
-
|
|
1856
|
-
/* ── Charts ── */
|
|
1857
|
-
.grid-2 { display: grid; grid-template-columns: 5fr 7fr; gap: 16px; margin-bottom: 16px; }
|
|
1858
|
-
.grid-full { margin-bottom: 16px; }
|
|
1859
|
-
.card {
|
|
1860
|
-
background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
|
|
1861
|
-
padding: 24px; transition: border-color 0.2s;
|
|
1862
|
-
}
|
|
1863
|
-
.card:hover { border-color: var(--border-light); }
|
|
1864
|
-
.card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
|
|
1865
|
-
.card-title { font-size: 14px; font-weight: 600; color: var(--text); }
|
|
1866
|
-
.card-subtitle { font-size: 12px; color: var(--text-muted); }
|
|
1867
|
-
|
|
1868
|
-
/* ── Chart containers ── */
|
|
1869
|
-
.chart-container { position: relative; width: 100%; }
|
|
1870
|
-
.chart-container.h-280 { height: 280px; }
|
|
1871
|
-
.chart-container.h-300 { height: 300px; }
|
|
1872
|
-
|
|
1873
|
-
/* ── Top Tools Bar ── */
|
|
1874
|
-
.tool-bars { display: flex; flex-direction: column; gap: 10px; }
|
|
1875
|
-
.tool-bar-row { display: flex; align-items: center; gap: 12px; }
|
|
1876
|
-
.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; }
|
|
1877
|
-
.tool-bar-track { flex: 1; height: 28px; background: var(--surface); border-radius: 6px; overflow: hidden; position: relative; }
|
|
1878
|
-
.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; }
|
|
1879
|
-
.tool-bar-count { font-size: 12px; color: var(--text-muted); min-width: 36px; text-align: right; }
|
|
1880
|
-
|
|
1881
|
-
/* ── Recent Table ── */
|
|
1882
|
-
.table-wrapper { max-height: 400px; overflow-y: auto; border-radius: var(--radius-sm); }
|
|
1883
|
-
.table-wrapper::-webkit-scrollbar { width: 6px; }
|
|
1884
|
-
.table-wrapper::-webkit-scrollbar-track { background: transparent; }
|
|
1885
|
-
.table-wrapper::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 3px; }
|
|
1886
|
-
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
1887
|
-
thead { position: sticky; top: 0; z-index: 1; }
|
|
1888
|
-
th {
|
|
1889
|
-
text-align: left; color: var(--text-muted); font-weight: 500; padding: 10px 16px;
|
|
1890
|
-
background: var(--card); border-bottom: 1px solid var(--border); font-size: 11px;
|
|
1891
|
-
text-transform: uppercase; letter-spacing: 0.5px;
|
|
1892
|
-
}
|
|
1893
|
-
td { padding: 10px 16px; border-bottom: 1px solid var(--border); }
|
|
1894
|
-
tr:hover td { background: var(--surface); }
|
|
1895
|
-
.tool-badge {
|
|
1896
|
-
display: inline-flex; align-items: center; gap: 4px;
|
|
1897
|
-
background: var(--accent-dim); color: var(--accent); padding: 3px 10px;
|
|
1898
|
-
border-radius: 6px; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px; font-weight: 500;
|
|
1899
|
-
}
|
|
1900
|
-
.when-text { color: var(--text-muted); }
|
|
1901
|
-
.duration-text { color: var(--text-secondary); font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; }
|
|
1902
|
-
.saved-text { color: var(--green); font-weight: 600; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; }
|
|
1903
|
-
|
|
1904
|
-
/* ── Footer ── */
|
|
1905
|
-
.footer { text-align: center; padding: 24px 0 0; color: var(--text-muted); font-size: 12px; }
|
|
1906
|
-
|
|
1907
|
-
/* ── Tabs ── */
|
|
1908
|
-
.tab-nav { display: flex; gap: 4px; margin-bottom: 28px; border-bottom: 2px solid var(--border); padding-bottom: 0; }
|
|
1909
|
-
.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; }
|
|
1910
|
-
.tab:hover { color: var(--text-secondary); }
|
|
1911
|
-
.tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
1912
|
-
.tab-content { display: none; }
|
|
1913
|
-
.tab-content.active { display: block; }
|
|
1914
|
-
|
|
1915
|
-
/* ── Learning ── */
|
|
1916
|
-
.suggestion-box { background: var(--accent-dim); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 20px; text-align: center; margin-bottom: 20px; }
|
|
1917
|
-
.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); }
|
|
1918
|
-
.learning-section { margin-bottom: 16px; }
|
|
1919
|
-
.annot-list { list-style: none; padding: 0; }
|
|
1920
|
-
.annot-list li { padding: 10px 0; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
|
|
1921
|
-
.annot-list li:last-child { border-bottom: none; }
|
|
1922
|
-
.annot-sym { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px; color: var(--accent); }
|
|
1923
|
-
.annot-key { font-size: 11px; color: var(--text-muted); background: var(--surface); padding: 2px 8px; border-radius: 4px; margin-left: 8px; }
|
|
1924
|
-
.annot-conf { font-size: 12px; font-weight: 600; }
|
|
1925
|
-
.annot-conf.hi { color: var(--green); }
|
|
1926
|
-
.annot-conf.md { color: var(--orange); }
|
|
1927
|
-
.annot-conf.lo { color: var(--red); }
|
|
1928
|
-
|
|
1929
|
-
/* ── Responsive ── */
|
|
1930
|
-
@media (max-width: 1024px) { .kpi-grid { grid-template-columns: repeat(2, 1fr); } .grid-2 { grid-template-columns: 1fr; } }
|
|
1931
|
-
@media (max-width: 640px) { .kpi-grid { grid-template-columns: 1fr; } .wrapper { padding: 16px 12px 80px; } }
|
|
1932
|
-
</style>
|
|
1933
|
-
</head>
|
|
1934
|
-
<body>
|
|
1935
|
-
<div class="wrapper">
|
|
1936
|
-
|
|
1937
|
-
<!-- Header -->
|
|
1938
|
-
<div class="header">
|
|
1939
|
-
<div class="header-left">
|
|
1940
|
-
<div class="logo">m</div>
|
|
1941
|
-
<h1>milens <span>dashboard</span></h1>
|
|
1942
|
-
</div>
|
|
1943
|
-
<div class="header-right">
|
|
1944
|
-
<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>
|
|
1945
|
-
<div class="status-dot"></div>
|
|
1946
|
-
<span class="status-text">Live</span>
|
|
1947
|
-
<button class="refresh-btn" onclick="refreshData()">↻ Refresh</button>
|
|
1948
|
-
</div>
|
|
1949
|
-
</div>
|
|
1950
|
-
|
|
1951
|
-
<!-- Tab Navigation -->
|
|
1952
|
-
<div class="tab-nav">
|
|
1953
|
-
<button class="tab active" data-tab="usage" onclick="switchTab('usage')">Usage Analytics</button>
|
|
1954
|
-
<button class="tab" data-tab="learning" onclick="switchTab('learning')">Learning</button>
|
|
1955
|
-
</div>
|
|
1956
|
-
|
|
1957
|
-
<div id="tab-usage" class="tab-content active">
|
|
1958
|
-
|
|
1959
|
-
<!-- KPIs -->
|
|
1960
|
-
<div class="kpi-grid">
|
|
1961
|
-
<div class="kpi">
|
|
1962
|
-
<div class="kpi-icon blue">⚙</div>
|
|
1963
|
-
<div class="value blue" id="kpi-calls">${fmtNum(stats.totalCalls)}</div>
|
|
1964
|
-
<div class="label">Tool Calls</div>
|
|
1965
|
-
<div class="sub" id="kpi-calls-sub">${stats.totalCalls.toLocaleString()} total invocations</div>
|
|
1966
|
-
<div class="kpi-glow blue"></div>
|
|
1967
|
-
</div>
|
|
1968
|
-
<div class="kpi">
|
|
1969
|
-
<div class="kpi-icon green">⚡</div>
|
|
1970
|
-
<div class="value green" id="kpi-saved">${fmtNum(stats.totalTokensSaved)}</div>
|
|
1971
|
-
<div class="label">Tokens Saved</div>
|
|
1972
|
-
<div class="sub" id="kpi-saved-sub">${stats.totalTokensSaved.toLocaleString()} tokens not wasted</div>
|
|
1973
|
-
<div class="kpi-glow green"></div>
|
|
1974
|
-
</div>
|
|
1975
|
-
<div class="kpi">
|
|
1976
|
-
<div class="kpi-icon purple">★</div>
|
|
1977
|
-
<div class="value purple" id="kpi-pct">${savingsPercent}%</div>
|
|
1978
|
-
<div class="label">Token Efficiency</div>
|
|
1979
|
-
<div class="sub">${fmtNum(stats.totalTokensOut)} returned vs ${fmtNum(stats.totalTokensSaved)} saved</div>
|
|
1980
|
-
<div class="kpi-glow purple"></div>
|
|
1981
|
-
</div>
|
|
1982
|
-
<div class="kpi">
|
|
1983
|
-
<div class="kpi-icon orange">⏱</div>
|
|
1984
|
-
<div class="value orange" id="kpi-avg">${stats.totalCalls > 0 ? Math.round(stats.totalDurationMs / stats.totalCalls) : 0}ms</div>
|
|
1985
|
-
<div class="label">Avg Response Time</div>
|
|
1986
|
-
<div class="sub">${(stats.totalDurationMs / 1000).toFixed(1)}s total processing</div>
|
|
1987
|
-
<div class="kpi-glow orange"></div>
|
|
1988
|
-
</div>
|
|
1989
|
-
</div>
|
|
1990
|
-
|
|
1991
|
-
<!-- Charts Row -->
|
|
1992
|
-
<div class="grid-2">
|
|
1993
|
-
<div class="card">
|
|
1994
|
-
<div class="card-header">
|
|
1995
|
-
<div>
|
|
1996
|
-
<div class="card-title">Top Tools</div>
|
|
1997
|
-
<div class="card-subtitle">By number of calls</div>
|
|
1998
|
-
</div>
|
|
1999
|
-
</div>
|
|
2000
|
-
<div id="toolBars" class="tool-bars"></div>
|
|
2001
|
-
</div>
|
|
2002
|
-
<div class="card">
|
|
2003
|
-
<div class="card-header">
|
|
2004
|
-
<div>
|
|
2005
|
-
<div class="card-title">Daily Activity</div>
|
|
2006
|
-
<div class="card-subtitle">Calls & tokens saved over the last 30 days</div>
|
|
2007
|
-
</div>
|
|
2008
|
-
</div>
|
|
2009
|
-
<div class="chart-container h-280"><canvas id="dayChart"></canvas></div>
|
|
2010
|
-
</div>
|
|
2011
|
-
</div>
|
|
2012
|
-
|
|
2013
|
-
<!-- Savings Distribution -->
|
|
2014
|
-
<div class="grid-2" style="grid-template-columns: 7fr 5fr;">
|
|
2015
|
-
<div class="card">
|
|
2016
|
-
<div class="card-header">
|
|
2017
|
-
<div>
|
|
2018
|
-
<div class="card-title">Recent Tool Calls</div>
|
|
2019
|
-
<div class="card-subtitle">Last 50 invocations</div>
|
|
2020
|
-
</div>
|
|
2021
|
-
</div>
|
|
2022
|
-
<div class="table-wrapper">
|
|
2023
|
-
<table>
|
|
2024
|
-
<thead><tr><th>Tool</th><th>When</th><th>Duration</th><th style="text-align:right">Tokens Saved</th></tr></thead>
|
|
2025
|
-
<tbody id="recentBody"></tbody>
|
|
2026
|
-
</table>
|
|
2027
|
-
</div>
|
|
2028
|
-
</div>
|
|
2029
|
-
<div class="card">
|
|
2030
|
-
<div class="card-header">
|
|
2031
|
-
<div>
|
|
2032
|
-
<div class="card-title">Savings by Tool</div>
|
|
2033
|
-
<div class="card-subtitle">Token savings distribution</div>
|
|
2034
|
-
</div>
|
|
2035
|
-
</div>
|
|
2036
|
-
<div class="chart-container h-300"><canvas id="savingsChart"></canvas></div>
|
|
2037
|
-
</div>
|
|
2038
|
-
</div>
|
|
2039
|
-
|
|
2040
|
-
<div class="footer">milens · auto-refreshes every 30s</div>
|
|
2041
|
-
</div><!-- /tab-usage -->
|
|
2042
|
-
|
|
2043
|
-
<div id="tab-learning" class="tab-content">
|
|
2044
|
-
<div class="suggestion-box">
|
|
2045
|
-
<h3 style="margin-bottom:8px;font-weight:600;">Metrics & Insights</h3>
|
|
2046
|
-
<p style="color:var(--text-secondary);font-size:13px;">Run <code>milens metrics</code> for a full code-quality metrics report.</p>
|
|
2047
|
-
</div>
|
|
2048
|
-
${learningStats}
|
|
2049
|
-
</div>
|
|
2050
|
-
|
|
2051
|
-
</div><!-- /wrapper -->
|
|
2052
|
-
|
|
2053
|
-
<script>
|
|
2054
|
-
const COLORS = ['#60a5fa','#34d399','#fbbf24','#a78bfa','#f87171','#2dd4bf','#818cf8','#fb923c','#e879f9','#38bdf8','#4ade80','#facc15','#f472b6','#22d3ee','#a3e635','#c084fc'];
|
|
2055
|
-
const BAR_COLORS = ['#60a5fa','#34d399','#fbbf24','#a78bfa','#f87171','#2dd4bf','#818cf8','#fb923c','#e879f9','#38bdf8'];
|
|
2056
|
-
let byTool = ${byToolJson};
|
|
2057
|
-
let byDay = ${byDayJson};
|
|
2058
|
-
|
|
2059
|
-
function fmtK(n) { return n >= 1e6 ? (n/1e6).toFixed(1)+'M' : n >= 1e3 ? (n/1e3).toFixed(1)+'K' : n; }
|
|
2060
|
-
|
|
2061
|
-
/* ── Top Tools Horizontal Bars ── */
|
|
2062
|
-
function renderToolBars() {
|
|
2063
|
-
const el = document.getElementById('toolBars');
|
|
2064
|
-
const sorted = [...byTool].sort((a,b) => b.calls - a.calls).slice(0, 10);
|
|
2065
|
-
const maxCalls = sorted[0]?.calls || 1;
|
|
2066
|
-
el.innerHTML = sorted.map((t, i) => {
|
|
2067
|
-
const pct = Math.max(8, (t.calls / maxCalls) * 100);
|
|
2068
|
-
const col = BAR_COLORS[i % BAR_COLORS.length];
|
|
2069
|
-
return \`<div class="tool-bar-row">
|
|
2070
|
-
<span class="tool-bar-name">\${t.tool}</span>
|
|
2071
|
-
<div class="tool-bar-track">
|
|
2072
|
-
<div class="tool-bar-fill" style="width:\${pct}%;background:linear-gradient(90deg,\${col}dd,\${col}88)">\${t.calls}</div>
|
|
2073
|
-
</div>
|
|
2074
|
-
<span class="tool-bar-count">\${fmtK(t.tokensSaved)}</span>
|
|
2075
|
-
</div>\`;
|
|
2076
|
-
}).join('');
|
|
2077
|
-
}
|
|
2078
|
-
|
|
2079
|
-
/* ── Daily Activity Chart ── */
|
|
2080
|
-
let dayChartInstance = null;
|
|
2081
|
-
function renderDayChart() {
|
|
2082
|
-
if (dayChartInstance) dayChartInstance.destroy();
|
|
2083
|
-
const ctx = document.getElementById('dayChart');
|
|
2084
|
-
const labels = byDay.map(d => {
|
|
2085
|
-
const parts = d.date.split('-');
|
|
2086
|
-
return parts[1] + '/' + parts[2];
|
|
2087
|
-
});
|
|
2088
|
-
dayChartInstance = new Chart(ctx, {
|
|
2089
|
-
type: 'bar',
|
|
2090
|
-
data: {
|
|
2091
|
-
labels,
|
|
2092
|
-
datasets: [
|
|
2093
|
-
{
|
|
2094
|
-
label: 'Calls', data: byDay.map(d => d.calls),
|
|
2095
|
-
backgroundColor: '#60a5fa44', hoverBackgroundColor: '#60a5fa88',
|
|
2096
|
-
borderRadius: 4, borderSkipped: false, yAxisID: 'y', barPercentage: 0.7,
|
|
2097
|
-
},
|
|
2098
|
-
{
|
|
2099
|
-
label: 'Tokens Saved', data: byDay.map(d => d.tokensSaved),
|
|
2100
|
-
type: 'line', borderColor: '#34d399', pointBackgroundColor: '#34d399',
|
|
2101
|
-
pointRadius: 2, pointHoverRadius: 5, borderWidth: 2.5,
|
|
2102
|
-
yAxisID: 'y1', tension: 0.4, fill: { target: 'origin', above: '#34d39910' },
|
|
2103
|
-
},
|
|
2104
|
-
],
|
|
2105
|
-
},
|
|
2106
|
-
options: {
|
|
2107
|
-
responsive: true, maintainAspectRatio: false,
|
|
2108
|
-
interaction: { mode: 'index', intersect: false },
|
|
2109
|
-
scales: {
|
|
2110
|
-
x: {
|
|
2111
|
-
ticks: { color: '#64748b', font: { size: 11 }, maxRotation: 0, autoSkip: true, maxTicksLimit: 15 },
|
|
2112
|
-
grid: { display: false },
|
|
2113
|
-
},
|
|
2114
|
-
y: {
|
|
2115
|
-
position: 'left', ticks: { color: '#60a5fa', font: { size: 11 } },
|
|
2116
|
-
grid: { color: '#1e2a3a' }, title: { display: true, text: 'Calls', color: '#60a5fa', font: { size: 11 } },
|
|
2117
|
-
},
|
|
2118
|
-
y1: {
|
|
2119
|
-
position: 'right', ticks: { color: '#34d399', font: { size: 11 }, callback: v => fmtK(v) },
|
|
2120
|
-
grid: { drawOnChartArea: false }, title: { display: true, text: 'Tokens Saved', color: '#34d399', font: { size: 11 } },
|
|
2121
|
-
},
|
|
2122
|
-
},
|
|
2123
|
-
plugins: {
|
|
2124
|
-
legend: { labels: { color: '#94a3b8', boxWidth: 12, usePointStyle: true, padding: 16 } },
|
|
2125
|
-
tooltip: {
|
|
2126
|
-
backgroundColor: '#1e293b', borderColor: '#334155', borderWidth: 1, titleColor: '#e2e8f0',
|
|
2127
|
-
bodyColor: '#94a3b8', cornerRadius: 8, padding: 12,
|
|
2128
|
-
callbacks: { label: ctx => ctx.dataset.label + ': ' + (ctx.datasetIndex === 1 ? fmtK(ctx.raw) : ctx.raw) },
|
|
2129
|
-
},
|
|
2130
|
-
},
|
|
2131
|
-
},
|
|
2132
|
-
});
|
|
2133
|
-
}
|
|
2134
|
-
|
|
2135
|
-
/* ── Savings Doughnut ── */
|
|
2136
|
-
function renderSavingsChart() {
|
|
2137
|
-
const ctx = document.getElementById('savingsChart');
|
|
2138
|
-
const sorted = [...byTool].sort((a,b) => b.tokensSaved - a.tokensSaved);
|
|
2139
|
-
const top = sorted.slice(0, 8);
|
|
2140
|
-
const rest = sorted.slice(8);
|
|
2141
|
-
if (rest.length) top.push({ tool: 'others', tokensSaved: rest.reduce((s,t) => s + t.tokensSaved, 0), calls: 0 });
|
|
2142
|
-
new Chart(ctx, {
|
|
2143
|
-
type: 'doughnut',
|
|
2144
|
-
data: {
|
|
2145
|
-
labels: top.map(t => t.tool),
|
|
2146
|
-
datasets: [{
|
|
2147
|
-
data: top.map(t => t.tokensSaved),
|
|
2148
|
-
backgroundColor: top.map((_, i) => COLORS[i]),
|
|
2149
|
-
borderWidth: 0, hoverOffset: 6,
|
|
2150
|
-
}],
|
|
2151
|
-
},
|
|
2152
|
-
options: {
|
|
2153
|
-
responsive: true, cutout: '68%',
|
|
2154
|
-
plugins: {
|
|
2155
|
-
legend: { position: 'right', labels: { color: '#94a3b8', font: { size: 12 }, padding: 8, usePointStyle: true, pointStyleWidth: 10 } },
|
|
2156
|
-
tooltip: {
|
|
2157
|
-
backgroundColor: '#1e293b', borderColor: '#334155', borderWidth: 1, titleColor: '#e2e8f0',
|
|
2158
|
-
bodyColor: '#94a3b8', cornerRadius: 8, padding: 12,
|
|
2159
|
-
callbacks: { label: ctx => ' ' + ctx.label + ': ' + fmtK(ctx.raw) + ' tokens' },
|
|
2160
|
-
},
|
|
2161
|
-
},
|
|
2162
|
-
},
|
|
2163
|
-
});
|
|
2164
|
-
}
|
|
2165
|
-
|
|
2166
|
-
/* ── Recent Calls Table ── */
|
|
2167
|
-
function renderRecent(data) {
|
|
2168
|
-
const tbody = document.getElementById('recentBody');
|
|
2169
|
-
tbody.innerHTML = data.map(r => {
|
|
2170
|
-
const ago = timeAgo(r.calledAt);
|
|
2171
|
-
return \`<tr>
|
|
2172
|
-
<td><span class="tool-badge">\${r.tool}</span></td>
|
|
2173
|
-
<td class="when-text">\${ago}</td>
|
|
2174
|
-
<td class="duration-text">\${r.durationMs}ms</td>
|
|
2175
|
-
<td class="saved-text" style="text-align:right">+\${r.tokensSaved.toLocaleString()}</td>
|
|
2176
|
-
</tr>\`;
|
|
2177
|
-
}).join('');
|
|
2178
|
-
}
|
|
2179
|
-
|
|
2180
|
-
function timeAgo(iso) {
|
|
2181
|
-
const d = new Date(iso.includes('T') ? iso : iso + 'Z');
|
|
2182
|
-
const s = Math.floor((Date.now() - d.getTime()) / 1000);
|
|
2183
|
-
if (s < 0) return 'just now';
|
|
2184
|
-
if (s < 60) return s + 's ago';
|
|
2185
|
-
if (s < 3600) return Math.floor(s/60) + 'm ago';
|
|
2186
|
-
if (s < 86400) return Math.floor(s/3600) + 'h ago';
|
|
2187
|
-
return Math.floor(s/86400) + 'd ago';
|
|
2188
|
-
}
|
|
2189
|
-
|
|
2190
|
-
/* ── Refresh ── */
|
|
2191
|
-
async function refreshData() {
|
|
2192
|
-
const btn = document.querySelector('.refresh-btn');
|
|
2193
|
-
btn.textContent = '⟳ Loading...';
|
|
2194
|
-
try {
|
|
2195
|
-
const res = await fetch('/api/stats');
|
|
2196
|
-
const data = await res.json();
|
|
2197
|
-
byTool = data.byTool; byDay = data.byDay;
|
|
2198
|
-
document.getElementById('kpi-calls').textContent = fmtK(data.totalCalls);
|
|
2199
|
-
document.getElementById('kpi-calls-sub').textContent = data.totalCalls.toLocaleString() + ' total invocations';
|
|
2200
|
-
document.getElementById('kpi-saved').textContent = fmtK(data.totalTokensSaved);
|
|
2201
|
-
document.getElementById('kpi-saved-sub').textContent = data.totalTokensSaved.toLocaleString() + ' tokens not wasted';
|
|
2202
|
-
const pct = data.totalTokensOut > 0 ? Math.round((data.totalTokensSaved / (data.totalTokensOut + data.totalTokensSaved)) * 100) : 0;
|
|
2203
|
-
document.getElementById('kpi-pct').textContent = pct + '%';
|
|
2204
|
-
document.getElementById('kpi-avg').textContent = (data.totalCalls > 0 ? Math.round(data.totalDurationMs / data.totalCalls) : 0) + 'ms';
|
|
2205
|
-
renderToolBars(); renderDayChart(); renderRecent(data.recentCalls);
|
|
2206
|
-
} catch(e) { console.error('Refresh failed', e); }
|
|
2207
|
-
btn.innerHTML = '↻ Refresh';
|
|
2208
|
-
}
|
|
2209
|
-
|
|
2210
|
-
/* ── Tab Switching ── */
|
|
2211
|
-
function switchTab(tab) {
|
|
2212
|
-
document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab));
|
|
2213
|
-
document.querySelectorAll('.tab-content').forEach(c => c.classList.toggle('active', c.id === 'tab-' + tab));
|
|
2214
|
-
}
|
|
2215
|
-
|
|
2216
|
-
/* ── Init ── */
|
|
2217
|
-
renderToolBars();
|
|
2218
|
-
renderDayChart();
|
|
2219
|
-
renderSavingsChart();
|
|
2220
|
-
renderRecent(${recentJson});
|
|
2221
|
-
setInterval(refreshData, 30000);
|
|
2222
|
-
</script>
|
|
2223
|
-
</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>
|
|
2224
2293
|
</html>`;
|
|
2225
2294
|
}
|
|
2226
2295
|
function renderConfBars(bands, total) {
|