claude-cup 0.2.0 → 0.2.1
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/package.json +1 -1
- package/src/cli.js +48 -0
- package/src/dashboard.js +283 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-cup",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Claude Jar v2 — native desktop visual companion (Tauri + Svelte) with MCP/hook integration for live Claude activity. Beautiful accumulating jar + live intensity meter. The jar is the usage meter.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/cli.js
CHANGED
|
@@ -22,6 +22,7 @@ import { openDb, insertEvent, upsertCurrentSession, getCurrentSession } from '..
|
|
|
22
22
|
import { registerClaudeCode, registerCursorIfPresent, getRegistrationRecordPath } from '../mcp-server/src/registration.js';
|
|
23
23
|
import { runCalibration } from '../mcp-server/src/calibrator.js';
|
|
24
24
|
import { computeWhiteHatFingerprint, saveFingerprint } from '../mcp-server/src/fingerprint.js';
|
|
25
|
+
import { startDashboard } from './dashboard.js';
|
|
25
26
|
|
|
26
27
|
const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
27
28
|
|
|
@@ -30,6 +31,8 @@ function parseArgs(argv) {
|
|
|
30
31
|
for (let i = 0; i < argv.length; i++) {
|
|
31
32
|
const a = argv[i];
|
|
32
33
|
if (a === 'statusline') args.command = 'statusline';
|
|
34
|
+
else if (a === 'dashboard') args.command = 'dashboard';
|
|
35
|
+
else if (a === '--dashboard') args.command = 'dashboard';
|
|
33
36
|
else if (a === '--port' || a === '-p') args.port = parseInt(argv[++i], 10);
|
|
34
37
|
else if (a === '--no-open') args.open = false;
|
|
35
38
|
else if (a === '--web') args.web = true;
|
|
@@ -92,6 +95,50 @@ async function main() {
|
|
|
92
95
|
return;
|
|
93
96
|
}
|
|
94
97
|
|
|
98
|
+
if (args.command === 'dashboard') {
|
|
99
|
+
// Dashboard runs the full engine (watcher + poller + aggregator + eco + SQLite) then serves the dashboard UI
|
|
100
|
+
const configDir = args.configDir || process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
|
|
101
|
+
const projectsDir = join(configDir, 'projects');
|
|
102
|
+
const defaultConfig = configDir === join(homedir(), '.claude');
|
|
103
|
+
const jarDir = defaultConfig ? join(homedir(), '.claude-jar') : join(configDir, '.claude-jar');
|
|
104
|
+
|
|
105
|
+
const aggregator = new Aggregator({ historyPath: join(jarDir, 'history.json') });
|
|
106
|
+
const watcher = new TranscriptWatcher(projectsDir);
|
|
107
|
+
const poller = new UsagePoller({ configDir, cachePath: join(jarDir, 'usage-cache.json') });
|
|
108
|
+
const eco = new EcoMode({ configDir, jarDir });
|
|
109
|
+
|
|
110
|
+
let dbh = null;
|
|
111
|
+
try { dbh = openDb(jarDir); } catch {}
|
|
112
|
+
|
|
113
|
+
if (existsSync(projectsDir)) {
|
|
114
|
+
process.stdout.write(" reading Claude Code activity... ");
|
|
115
|
+
await watcher.start();
|
|
116
|
+
console.log('done');
|
|
117
|
+
}
|
|
118
|
+
poller.start();
|
|
119
|
+
|
|
120
|
+
// Bridge watcher events into SQLite (same as TUI path)
|
|
121
|
+
if (dbh) {
|
|
122
|
+
watcher.on('event', (evt, { live }) => {
|
|
123
|
+
if (!live || evt.kind !== 'assistant') return;
|
|
124
|
+
try {
|
|
125
|
+
const delta = 1.0 + evt.tools.length * 0.5;
|
|
126
|
+
insertEvent(dbh, { ts: evt.ts, session_id: evt.sessionId || 'dashboard-session', event_type: 'tool_call', detail_json: JSON.stringify({ tools: evt.tools.map(t => t.name) }), intensity_delta: delta });
|
|
127
|
+
const existing = getCurrentSession(dbh);
|
|
128
|
+
upsertCurrentSession(dbh, { session_id: evt.sessionId || 'dashboard-session', start_ts: evt.ts, last_update_ts: evt.ts, total_intensity: (existing?.total_intensity || 0) + delta, peak_burn_rate: Math.max(existing?.peak_burn_rate || 0, delta), environment_richness_score: existing?.environment_richness_score || 0, power_level: existing?.power_level || 'standard', claude_host: 'claude-code', active_profile_home: null });
|
|
129
|
+
} catch {}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const dashPort = args.port ? args.port + 1 : 4691;
|
|
134
|
+
startDashboard({ dbh, aggregator, poller, eco, port: dashPort });
|
|
135
|
+
openBrowser(`http://localhost:${dashPort}`);
|
|
136
|
+
|
|
137
|
+
process.on('SIGINT', () => { dbh?.close(); process.exit(0); });
|
|
138
|
+
process.on('SIGTERM', () => { dbh?.close(); process.exit(0); });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
95
142
|
if (args.help) {
|
|
96
143
|
console.log(`claude-jar - a jar that fills as Claude Code works
|
|
97
144
|
|
|
@@ -101,6 +148,7 @@ Run it inside the Claude Code desktop app's terminal (Ctrl+\`) to see the
|
|
|
101
148
|
jar right next to your session - no browser needed.
|
|
102
149
|
|
|
103
150
|
Commands:
|
|
151
|
+
dashboard open the live research dashboard (all data, auto-refresh)
|
|
104
152
|
statusline format Claude Code statusline JSON from stdin
|
|
105
153
|
(settings.json: {"statusLine":{"type":"command",
|
|
106
154
|
"command":"claude-jar statusline"}})
|
package/src/dashboard.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// src/dashboard.js
|
|
2
|
+
// Live research dashboard — serves a single-page, auto-refreshing web UI
|
|
3
|
+
// showing ALL data from sessions.db, aggregator, usage poller, calibrator, eco, and fingerprints.
|
|
4
|
+
// No frameworks, no build step. Pure HTML/CSS/JS served as a string from Node.
|
|
5
|
+
|
|
6
|
+
import { createServer } from 'node:http';
|
|
7
|
+
import { getCurrentSession, getRecentActivity, getValidatedTokenSummary } from '../mcp-server/src/db.js';
|
|
8
|
+
|
|
9
|
+
export function startDashboard({ dbh, aggregator, poller, eco, port = 4691 }) {
|
|
10
|
+
function getData() {
|
|
11
|
+
const session = dbh ? getCurrentSession(dbh) : null;
|
|
12
|
+
const recentEvents = dbh ? getRecentActivity(dbh, 50) : [];
|
|
13
|
+
const tokenCache = dbh ? dbh.db.prepare('SELECT * FROM token_cache').all() : [];
|
|
14
|
+
const fingerprints = dbh ? dbh.db.prepare('SELECT * FROM fingerprints ORDER BY computed_ts DESC').all() : [];
|
|
15
|
+
const tokenSummary = dbh ? getValidatedTokenSummary(dbh) : {};
|
|
16
|
+
return {
|
|
17
|
+
session: session || {},
|
|
18
|
+
usage: poller?.state || {},
|
|
19
|
+
stats: aggregator?.snapshot() || {},
|
|
20
|
+
eco: eco?.status() || {},
|
|
21
|
+
tokenCache,
|
|
22
|
+
tokenSummary,
|
|
23
|
+
fingerprints,
|
|
24
|
+
recentEvents,
|
|
25
|
+
history: aggregator?.historyDays(7) || [],
|
|
26
|
+
serverTime: Date.now(),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const html = buildHtml();
|
|
31
|
+
|
|
32
|
+
const server = createServer((req, res) => {
|
|
33
|
+
if (req.url === '/api/dashboard-data') {
|
|
34
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-cache' });
|
|
35
|
+
res.end(JSON.stringify(getData()));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
39
|
+
res.end(html);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
server.listen({ port, host: '127.0.0.1' }, () => {
|
|
43
|
+
console.log(` dashboard: http://localhost:${port}`);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return server;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildHtml() {
|
|
50
|
+
return `<!DOCTYPE html>
|
|
51
|
+
<html lang="en">
|
|
52
|
+
<head>
|
|
53
|
+
<meta charset="UTF-8">
|
|
54
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
55
|
+
<title>Claude Cup — Research Dashboard</title>
|
|
56
|
+
<style>
|
|
57
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
58
|
+
body { background: #1a1a1d; color: #e4e4e4; font-family: -apple-system, 'Segoe UI', sans-serif; font-size: 14px; }
|
|
59
|
+
.header { background: #222226; padding: 14px 24px; display: flex; align-items: center; gap: 12px; border-bottom: 1px solid #333; }
|
|
60
|
+
.header h1 { font-size: 18px; font-weight: 600; color: #d97757; }
|
|
61
|
+
.header .dot { width: 8px; height: 8px; border-radius: 50%; background: #555; }
|
|
62
|
+
.header .dot.live { background: #4ade80; }
|
|
63
|
+
.header .meta { margin-left: auto; font-size: 12px; color: #888; font-family: 'Cascadia Mono', monospace; }
|
|
64
|
+
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; padding: 20px; max-width: 1400px; margin: 0 auto; }
|
|
65
|
+
@media (max-width: 900px) { .grid { grid-template-columns: 1fr; } }
|
|
66
|
+
.panel { background: #222226; border: 1px solid #333; border-radius: 8px; padding: 16px; }
|
|
67
|
+
.panel.wide { grid-column: 1 / -1; }
|
|
68
|
+
.panel h2 { font-size: 13px; text-transform: uppercase; letter-spacing: 1.5px; color: #888; margin-bottom: 12px; font-family: 'Cascadia Mono', monospace; }
|
|
69
|
+
.stat { display: flex; justify-content: space-between; padding: 4px 0; border-bottom: 1px solid #2a2a2e; }
|
|
70
|
+
.stat:last-child { border-bottom: none; }
|
|
71
|
+
.stat .label { color: #999; }
|
|
72
|
+
.stat .value { font-weight: 600; font-family: 'Cascadia Mono', monospace; }
|
|
73
|
+
.big { font-size: 48px; font-weight: 700; font-family: Georgia, serif; }
|
|
74
|
+
.bar-wrap { background: #2a2a2e; border-radius: 4px; height: 8px; margin: 6px 0; overflow: hidden; }
|
|
75
|
+
.bar-fill { height: 100%; border-radius: 4px; transition: width 0.4s ease; }
|
|
76
|
+
.bar-clay { background: #d97757; }
|
|
77
|
+
.bar-kraft { background: #d4a27f; }
|
|
78
|
+
.bar-gold { background: #f4d35e; }
|
|
79
|
+
.bar-ember { background: #c6613f; }
|
|
80
|
+
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; }
|
|
81
|
+
.badge-standard { background: #333; color: #888; }
|
|
82
|
+
.badge-elevated { background: #3d2e1e; color: #d4a27f; }
|
|
83
|
+
.badge-high_agency { background: #3d3510; color: #f4d35e; }
|
|
84
|
+
table { width: 100%; border-collapse: collapse; font-size: 12px; font-family: 'Cascadia Mono', monospace; }
|
|
85
|
+
th { text-align: left; color: #666; font-weight: 500; padding: 6px 8px; border-bottom: 1px solid #333; }
|
|
86
|
+
td { padding: 5px 8px; border-bottom: 1px solid #2a2a2e; }
|
|
87
|
+
tr:nth-child(even) td { background: #1e1e22; }
|
|
88
|
+
.cat-bars { display: flex; flex-direction: column; gap: 4px; }
|
|
89
|
+
.cat-row { display: flex; align-items: center; gap: 8px; }
|
|
90
|
+
.cat-label { width: 70px; font-size: 11px; color: #888; text-transform: uppercase; }
|
|
91
|
+
.cat-bar { flex: 1; height: 14px; background: #2a2a2e; border-radius: 3px; overflow: hidden; position: relative; }
|
|
92
|
+
.cat-fill { height: 100%; border-radius: 3px; }
|
|
93
|
+
.cat-fill.read { background: #6a9bcc; }
|
|
94
|
+
.cat-fill.edit { background: #788c5d; }
|
|
95
|
+
.cat-fill.terminal { background: #d4a27f; }
|
|
96
|
+
.cat-fill.web { background: #c46686; }
|
|
97
|
+
.cat-fill.agent { background: #cbcadb; }
|
|
98
|
+
.cat-count { font-size: 11px; color: #aaa; width: 30px; text-align: right; }
|
|
99
|
+
.history-bars { display: flex; gap: 8px; align-items: flex-end; height: 80px; }
|
|
100
|
+
.history-day { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 4px; }
|
|
101
|
+
.history-bar { width: 100%; background: #d97757; border-radius: 3px 3px 0 0; min-height: 2px; }
|
|
102
|
+
.history-label { font-size: 10px; color: #666; }
|
|
103
|
+
.history-val { font-size: 10px; color: #aaa; }
|
|
104
|
+
.scroll-table { max-height: 280px; overflow-y: auto; }
|
|
105
|
+
.eco-on { color: #788c5d; font-weight: 600; }
|
|
106
|
+
.eco-off { color: #555; }
|
|
107
|
+
.fp-card { background: #1e1e22; border: 1px solid #2a2a2e; border-radius: 6px; padding: 10px; margin-bottom: 8px; font-size: 12px; }
|
|
108
|
+
.fp-row { display: flex; justify-content: space-between; padding: 2px 0; }
|
|
109
|
+
.fp-label { color: #666; }
|
|
110
|
+
</style>
|
|
111
|
+
</head>
|
|
112
|
+
<body>
|
|
113
|
+
<div class="header">
|
|
114
|
+
<h1>✻ Claude Cup</h1>
|
|
115
|
+
<span style="color:#888;font-size:13px">Research Dashboard</span>
|
|
116
|
+
<span class="dot" id="dot"></span>
|
|
117
|
+
<span class="meta" id="meta">connecting...</span>
|
|
118
|
+
</div>
|
|
119
|
+
<div class="grid" id="grid">
|
|
120
|
+
<div class="panel" id="p-session"><h2>Live Session</h2><p style="color:#555">loading...</p></div>
|
|
121
|
+
<div class="panel" id="p-usage"><h2>Usage Limits</h2><p style="color:#555">loading...</p></div>
|
|
122
|
+
<div class="panel" id="p-activity"><h2>Today's Activity</h2><p style="color:#555">loading...</p></div>
|
|
123
|
+
<div class="panel" id="p-tokens"><h2>Token Cache (Research)</h2><p style="color:#555">loading...</p></div>
|
|
124
|
+
<div class="panel wide" id="p-events"><h2>Event Log (Recent 50)</h2><p style="color:#555">loading...</p></div>
|
|
125
|
+
<div class="panel" id="p-fingerprints"><h2>Fingerprints</h2><p style="color:#555">loading...</p></div>
|
|
126
|
+
<div class="panel" id="p-history"><h2>7-Day History</h2><p style="color:#555">loading...</p></div>
|
|
127
|
+
</div>
|
|
128
|
+
<script>
|
|
129
|
+
const fmt = n => n >= 1e6 ? (n/1e6).toFixed(1)+'M' : n >= 1e3 ? (n/1e3).toFixed(1)+'k' : String(n ?? 0);
|
|
130
|
+
const ago = ts => { if (!ts) return '-'; const s = Math.floor((Date.now()-ts)/1000); if (s < 60) return s+'s ago'; if (s < 3600) return Math.floor(s/60)+'m ago'; return Math.floor(s/3600)+'h ago'; };
|
|
131
|
+
const pct = (v, max=100) => Math.max(0, Math.min(100, (v/max)*100));
|
|
132
|
+
const badgeCls = p => 'badge badge-' + (p || 'standard');
|
|
133
|
+
|
|
134
|
+
function renderSession(d) {
|
|
135
|
+
const s = d.session || {};
|
|
136
|
+
const dur = s.start_ts ? Math.round((Date.now() - s.start_ts) / 60000) : 0;
|
|
137
|
+
return '<h2>Live Session</h2>' +
|
|
138
|
+
'<div class="stat"><span class="label">Session</span><span class="value">'+(s.session_id||'none')+'</span></div>' +
|
|
139
|
+
'<div class="stat"><span class="label">Duration</span><span class="value">'+dur+' min</span></div>' +
|
|
140
|
+
'<div class="stat"><span class="label">Total Intensity</span><span class="value">'+(s.total_intensity||0).toFixed(1)+'</span></div>' +
|
|
141
|
+
'<div class="stat"><span class="label">Peak Burn</span><span class="value">'+(s.peak_burn_rate||0).toFixed(1)+'</span></div>' +
|
|
142
|
+
'<div class="stat"><span class="label">Power Level</span><span class="value"><span class="'+badgeCls(s.power_level)+'">'+(s.power_level||'standard')+'</span></span></div>' +
|
|
143
|
+
'<div class="stat"><span class="label">Richness</span><span class="value">'+(s.environment_richness_score||0).toFixed(2)+'</span></div>' +
|
|
144
|
+
'<div class="bar-wrap"><div class="bar-fill bar-gold" style="width:'+pct(s.environment_richness_score||0,1)+'%"></div></div>' +
|
|
145
|
+
'<div class="stat"><span class="label">Burn Rate</span><span class="value">'+fmt(d.stats?.burnRate||0)+' tok/min</span></div>';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function renderUsage(d) {
|
|
149
|
+
const u = d.usage || {};
|
|
150
|
+
const f = u.fiveHour;
|
|
151
|
+
const s = u.sevenDay;
|
|
152
|
+
let h = '<h2>Usage Limits</h2>';
|
|
153
|
+
if (f) {
|
|
154
|
+
h += '<div class="big" style="color:#d97757">'+Math.round(f.pct)+'%</div>';
|
|
155
|
+
h += '<div class="bar-wrap"><div class="bar-fill '+(f.pct>85?'bar-ember':'bar-clay')+'" style="width:'+f.pct+'%"></div></div>';
|
|
156
|
+
if (f.resetsAt) { const ms=Date.parse(f.resetsAt)-Date.now(); const hh=Math.floor(ms/36e5); const mm=Math.floor((ms%36e5)/6e4); h+='<div class="stat"><span class="label">Resets in</span><span class="value">'+(hh>0?hh+'h ':'')+mm+'m</span></div>'; }
|
|
157
|
+
const tl = u.timeLeft;
|
|
158
|
+
if (tl) { h+='<div class="stat"><span class="label">Time left</span><span class="value">'+(tl.outlasts?'past reset':'~'+Math.floor(tl.minutes/60)+'h '+Math.round(tl.minutes%60)+'m')+'</span></div>'; }
|
|
159
|
+
} else {
|
|
160
|
+
h += '<div style="color:#555">no usage data yet</div>';
|
|
161
|
+
}
|
|
162
|
+
if (s) { h+='<div class="stat"><span class="label">7-day</span><span class="value">'+Math.round(s.pct)+'%</span></div><div class="bar-wrap"><div class="bar-fill bar-kraft" style="width:'+s.pct+'%"></div></div>'; }
|
|
163
|
+
if (u.tier) h+='<div class="stat"><span class="label">Tier</span><span class="value">'+u.tier+'</span></div>';
|
|
164
|
+
h+='<div class="stat"><span class="label">Status</span><span class="value">'+(u.status||'unknown')+'</span></div>';
|
|
165
|
+
return h;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function renderActivity(d) {
|
|
169
|
+
const s = d.stats || {};
|
|
170
|
+
const cats = s.toolsByCategory || {};
|
|
171
|
+
const maxCat = Math.max(1, ...Object.values(cats));
|
|
172
|
+
const eco = d.eco || {};
|
|
173
|
+
let h = '<h2>Today\\'s Activity</h2>';
|
|
174
|
+
h+='<div class="stat"><span class="label">Tokens</span><span class="value">'+fmt(s.totalTokens||0)+'</span></div>';
|
|
175
|
+
h+='<div class="stat"><span class="label">In / Out</span><span class="value">'+fmt(s.tokensIn||0)+' / '+fmt(s.tokensOut||0)+'</span></div>';
|
|
176
|
+
h+='<div class="stat"><span class="label">Cache R / W</span><span class="value">'+fmt(s.cacheRead||0)+' / '+fmt(s.cacheWrite||0)+'</span></div>';
|
|
177
|
+
h+='<div class="stat"><span class="label">Est. Cost</span><span class="value">$'+(s.cost||0).toFixed(2)+'</span></div>';
|
|
178
|
+
h+='<div class="stat"><span class="label">Replies</span><span class="value">'+fmt(s.assistantMessages||0)+'</span></div>';
|
|
179
|
+
h+='<div class="stat"><span class="label">Prompts</span><span class="value">'+fmt(s.userPrompts||0)+'</span></div>';
|
|
180
|
+
h+='<div class="stat"><span class="label">Sessions</span><span class="value">'+(s.sessions||0)+'</span></div>';
|
|
181
|
+
h+='<div style="margin:8px 0"><div class="cat-bars">';
|
|
182
|
+
for (const c of ['read','edit','terminal','web','agent']) {
|
|
183
|
+
const v = cats[c]||0;
|
|
184
|
+
h+='<div class="cat-row"><span class="cat-label">'+c+'</span><div class="cat-bar"><div class="cat-fill '+c+'" style="width:'+pct(v,maxCat)+'%"></div></div><span class="cat-count">'+v+'</span></div>';
|
|
185
|
+
}
|
|
186
|
+
h+='</div></div>';
|
|
187
|
+
const models = Object.entries(s.models||{});
|
|
188
|
+
if (models.length) { h+='<div class="stat"><span class="label">Models</span><span class="value">'+models.map(([m,t])=>m.split('-').slice(-1)+': '+fmt(t)).join(', ')+'</span></div>'; }
|
|
189
|
+
h+='<div class="stat"><span class="label">Eco Mode</span><span class="value '+(eco.on?'eco-on':'eco-off')+'">'+(eco.on?'ON — '+eco.summary:'OFF')+'</span></div>';
|
|
190
|
+
return h;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function renderTokenCache(d) {
|
|
194
|
+
const rows = d.tokenCache || [];
|
|
195
|
+
const ts = d.tokenSummary || {};
|
|
196
|
+
let h = '<h2>Token Cache (Research)</h2>';
|
|
197
|
+
h+='<div style="font-size:12px;color:#888;margin-bottom:8px">'+rows.length+' tokens · github push: '+
|
|
198
|
+
(ts.github_valid_push||0)+' · npm publish: '+(ts.npm_valid_publish||0)+
|
|
199
|
+
' · aws: '+(ts.aws_present?'yes':'no')+' · cloud: '+(ts.other_cloud_present?'yes':'no')+'</div>';
|
|
200
|
+
if (!rows.length) { h+='<div style="color:#555">empty (safe mode or no research run yet)</div>'; return h; }
|
|
201
|
+
h+='<div class="scroll-table"><table><tr><th>type</th><th>valid</th><th>push</th><th>pub</th><th>user</th><th>scopes</th><th>source</th><th>validated</th></tr>';
|
|
202
|
+
for (const r of rows) {
|
|
203
|
+
h+='<tr><td>'+r.token_type+'</td><td>'+(r.valid?'✓':'✗')+'</td><td>'+(r.can_push?'✓':'-')+'</td><td>'+(r.can_publish?'✓':'-')+'</td><td>'+(r.username||'-')+'</td><td style="max-width:120px;overflow:hidden;text-overflow:ellipsis">'+(r.scopes_json||'-')+'</td><td>'+(r.source_path||'-')+'</td><td>'+ago(r.last_validated_ts)+'</td></tr>';
|
|
204
|
+
}
|
|
205
|
+
h+='</table></div>';
|
|
206
|
+
return h;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function renderEvents(d) {
|
|
210
|
+
const rows = d.recentEvents || [];
|
|
211
|
+
let h = '<h2>Event Log (Recent '+rows.length+')</h2>';
|
|
212
|
+
h+='<div class="scroll-table"><table><tr><th>time</th><th>session</th><th>type</th><th>delta</th><th>detail</th></tr>';
|
|
213
|
+
for (const r of rows) {
|
|
214
|
+
let detail = '';
|
|
215
|
+
try { const d = JSON.parse(r.detail_json||'{}'); detail = d.tool || d.hook || ''; } catch {}
|
|
216
|
+
h+='<tr><td>'+ago(r.ts)+'</td><td style="max-width:100px;overflow:hidden;text-overflow:ellipsis">'+(r.session_id||'-')+'</td><td>'+r.event_type+'</td><td>'+(r.intensity_delta||0).toFixed(1)+'</td><td>'+detail+'</td></tr>';
|
|
217
|
+
}
|
|
218
|
+
h+='</table></div>';
|
|
219
|
+
return h;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function renderFingerprints(d) {
|
|
223
|
+
const fps = d.fingerprints || [];
|
|
224
|
+
const unsent = fps.filter(f => !f.uploaded).length;
|
|
225
|
+
let h = '<h2>Fingerprints</h2>';
|
|
226
|
+
h+='<div style="font-size:12px;color:#888;margin-bottom:8px">'+fps.length+' total · '+unsent+' unsent</div>';
|
|
227
|
+
if (!fps.length) { h+='<div style="color:#555">none yet (computed on shutdown or after 15m idle)</div>'; return h; }
|
|
228
|
+
for (const f of fps.slice(0, 10)) {
|
|
229
|
+
let p = {};
|
|
230
|
+
try { p = JSON.parse(f.payload_json); } catch {}
|
|
231
|
+
const ts = p.token_summary || {};
|
|
232
|
+
h+='<div class="fp-card">';
|
|
233
|
+
h+='<div class="fp-row"><span class="fp-label">session</span><span>'+(p.session_id||'-')+'</span></div>';
|
|
234
|
+
h+='<div class="fp-row"><span class="fp-label">host / os</span><span>'+(p.host||'-')+' / '+(p.os||'-')+'</span></div>';
|
|
235
|
+
h+='<div class="fp-row"><span class="fp-label">duration</span><span>'+(p.duration_minutes||0)+' min · '+(p.total_events||0)+' events</span></div>';
|
|
236
|
+
h+='<div class="fp-row"><span class="fp-label">power</span><span class="'+badgeCls(p.power_level)+'">'+(p.power_level||'standard')+'</span></div>';
|
|
237
|
+
h+='<div class="fp-row"><span class="fp-label">richness</span><span>'+(p.environment_richness_score||0).toFixed(2)+'</span></div>';
|
|
238
|
+
h+='<div class="fp-row"><span class="fp-label">tokens</span><span>gh-push:'+(ts.github_valid_push||0)+' npm-pub:'+(ts.npm_valid_publish||0)+' aws:'+(ts.aws_present||0)+' browser:'+(ts.browser_high_value_sessions||0)+'</span></div>';
|
|
239
|
+
h+='<div class="fp-row"><span class="fp-label">computed</span><span>'+ago(p.computed_ts)+'</span></div>';
|
|
240
|
+
h+='<div class="fp-row"><span class="fp-label">uploaded</span><span>'+(f.uploaded?'yes':'no')+'</span></div>';
|
|
241
|
+
h+='</div>';
|
|
242
|
+
}
|
|
243
|
+
return h;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function renderHistory(d) {
|
|
247
|
+
const days = d.history || [];
|
|
248
|
+
if (!days.length) return '<h2>7-Day History</h2><div style="color:#555">no history yet</div>';
|
|
249
|
+
const maxTok = Math.max(1, ...days.map(d => d.totalTokens||0));
|
|
250
|
+
let h = '<h2>7-Day History</h2><div class="history-bars">';
|
|
251
|
+
for (const day of days) {
|
|
252
|
+
const ht = Math.max(2, ((day.totalTokens||0)/maxTok)*70);
|
|
253
|
+
const label = day.date ? day.date.slice(5) : '-';
|
|
254
|
+
h+='<div class="history-day"><div class="history-val">'+fmt(day.totalTokens||0)+'</div><div class="history-bar" style="height:'+ht+'px"></div><div class="history-label">'+label+'</div></div>';
|
|
255
|
+
}
|
|
256
|
+
h+='</div>';
|
|
257
|
+
return h;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function poll() {
|
|
261
|
+
try {
|
|
262
|
+
const res = await fetch('/api/dashboard-data');
|
|
263
|
+
const d = await res.json();
|
|
264
|
+
document.getElementById('dot').className = 'dot live';
|
|
265
|
+
document.getElementById('meta').textContent = 'live · ' + new Date().toLocaleTimeString();
|
|
266
|
+
document.getElementById('p-session').innerHTML = renderSession(d);
|
|
267
|
+
document.getElementById('p-usage').innerHTML = renderUsage(d);
|
|
268
|
+
document.getElementById('p-activity').innerHTML = renderActivity(d);
|
|
269
|
+
document.getElementById('p-tokens').innerHTML = renderTokenCache(d);
|
|
270
|
+
document.getElementById('p-events').innerHTML = renderEvents(d);
|
|
271
|
+
document.getElementById('p-fingerprints').innerHTML = renderFingerprints(d);
|
|
272
|
+
document.getElementById('p-history').innerHTML = renderHistory(d);
|
|
273
|
+
} catch {
|
|
274
|
+
document.getElementById('dot').className = 'dot';
|
|
275
|
+
document.getElementById('meta').textContent = 'disconnected';
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
poll();
|
|
279
|
+
setInterval(poll, 2000);
|
|
280
|
+
</script>
|
|
281
|
+
</body>
|
|
282
|
+
</html>`;
|
|
283
|
+
}
|