claude-roi 0.8.6 → 0.8.8
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/dashboard.html +127 -17
- package/src/index.js +29 -16
- package/src/server.js +3 -3
package/package.json
CHANGED
package/src/dashboard.html
CHANGED
|
@@ -2245,6 +2245,8 @@ let sortCol = 'startTime';
|
|
|
2245
2245
|
let sortOrder = -1;
|
|
2246
2246
|
let timelineChart = null;
|
|
2247
2247
|
let timelineLogScale = false;
|
|
2248
|
+
let tokenBurnChart = null;
|
|
2249
|
+
let tokenBurnPercentMode = false;
|
|
2248
2250
|
|
|
2249
2251
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
2250
2252
|
initTheme();
|
|
@@ -2308,6 +2310,7 @@ function render() {
|
|
|
2308
2310
|
<div class="chart-card full-width">
|
|
2309
2311
|
<div class="chart-header">
|
|
2310
2312
|
<h3>Token Burn Rate <i class="info-tip" data-tip="Daily token consumption stacked by type (input/output/cache). Dashed orange line shows cumulative total — watch it climb.">i</i></h3>
|
|
2313
|
+
<button class="scale-toggle" onclick="toggleTokenBurnPercent()">Split view</button>
|
|
2311
2314
|
</div>
|
|
2312
2315
|
<div class="chart-container"><canvas id="chart-token-burn"></canvas></div>
|
|
2313
2316
|
</div>
|
|
@@ -2327,8 +2330,8 @@ function render() {
|
|
|
2327
2330
|
<div class="chart-container"><canvas id="chart-tools"></canvas></div>
|
|
2328
2331
|
</div>
|
|
2329
2332
|
<div class="chart-card">
|
|
2330
|
-
<h3>
|
|
2331
|
-
<div class="chart-container"><canvas id="chart-
|
|
2333
|
+
<h3>Cost per Commit by Model <i class="info-tip" data-tip="Compare how much each model costs per commit. Lower blue bars = better value. Purple bars show commit volume for confidence. Use this to decide which model to use for different tasks.">i</i></h3>
|
|
2334
|
+
<div class="chart-container"><canvas id="chart-model-efficiency"></canvas></div>
|
|
2332
2335
|
</div>
|
|
2333
2336
|
<div class="chart-card">
|
|
2334
2337
|
<h3>Productivity Heatmap <i class="info-tip" data-tip="Each cell = commits produced during that hour and day of week. Darker green = more commits. Find your peak productivity windows.">i</i></h3>
|
|
@@ -2891,7 +2894,7 @@ function initCharts() {
|
|
|
2891
2894
|
let cumulative = 0;
|
|
2892
2895
|
const cumulativeData = dailyTokens.map(d => { cumulative += (d.totalTokens || 0); return cumulative; });
|
|
2893
2896
|
|
|
2894
|
-
new Chart(document.getElementById('chart-token-burn'), {
|
|
2897
|
+
tokenBurnChart = new Chart(document.getElementById('chart-token-burn'), {
|
|
2895
2898
|
type: 'bar',
|
|
2896
2899
|
data: {
|
|
2897
2900
|
labels: dailyTokens.map(d => new Date(d.date + 'T12:00:00').toLocaleDateString(undefined, { month: 'short', day: 'numeric' })),
|
|
@@ -3123,25 +3126,29 @@ function initCharts() {
|
|
|
3123
3126
|
},
|
|
3124
3127
|
});
|
|
3125
3128
|
|
|
3126
|
-
//
|
|
3127
|
-
const
|
|
3128
|
-
const
|
|
3129
|
-
|
|
3129
|
+
// Cost per Commit by Model
|
|
3130
|
+
const modelEff = DATA.modelBreakdown;
|
|
3131
|
+
const modelEffLabels = Object.keys(modelEff).filter(k => modelEff[k].commits > 0);
|
|
3132
|
+
const modelEffColors = { opus: '#a855f7', sonnet: '#3b82f6', haiku: '#22d3a8', unknown: '#94a3b8' };
|
|
3133
|
+
new Chart(document.getElementById('chart-model-efficiency'), {
|
|
3130
3134
|
type: 'bar',
|
|
3131
3135
|
data: {
|
|
3132
|
-
labels:
|
|
3136
|
+
labels: modelEffLabels.map(m => m.charAt(0).toUpperCase() + m.slice(1) + ' ($' + modelEff[m].cost.toFixed(0) + ' total)'),
|
|
3133
3137
|
datasets: [
|
|
3134
3138
|
{
|
|
3135
3139
|
label: 'Avg $/Commit',
|
|
3136
|
-
data:
|
|
3137
|
-
backgroundColor: '#
|
|
3140
|
+
data: modelEffLabels.map(m => modelEff[m].avgCostPerCommit),
|
|
3141
|
+
backgroundColor: modelEffLabels.map(m => modelEffColors[m] || '#94a3b8'),
|
|
3138
3142
|
borderRadius: 4,
|
|
3139
3143
|
yAxisID: 'y',
|
|
3140
3144
|
},
|
|
3141
3145
|
{
|
|
3142
|
-
label: '
|
|
3143
|
-
data:
|
|
3144
|
-
backgroundColor:
|
|
3146
|
+
label: 'Commits',
|
|
3147
|
+
data: modelEffLabels.map(m => Math.round(modelEff[m].commits)),
|
|
3148
|
+
backgroundColor: modelEffLabels.map(m => {
|
|
3149
|
+
const c = modelEffColors[m] || '#94a3b8';
|
|
3150
|
+
return c + '40';
|
|
3151
|
+
}),
|
|
3145
3152
|
borderRadius: 4,
|
|
3146
3153
|
yAxisID: 'y1',
|
|
3147
3154
|
},
|
|
@@ -3150,10 +3157,30 @@ function initCharts() {
|
|
|
3150
3157
|
options: {
|
|
3151
3158
|
responsive: true,
|
|
3152
3159
|
maintainAspectRatio: false,
|
|
3153
|
-
plugins: {
|
|
3160
|
+
plugins: {
|
|
3161
|
+
legend: { position: 'top' },
|
|
3162
|
+
tooltip: {
|
|
3163
|
+
callbacks: {
|
|
3164
|
+
label: ctx => {
|
|
3165
|
+
if (ctx.datasetIndex === 0) return ` Avg $/Commit: $${ctx.parsed.y.toFixed(2)}`;
|
|
3166
|
+
return ` Commits: ${ctx.parsed.y}`;
|
|
3167
|
+
},
|
|
3168
|
+
afterBody: (items) => {
|
|
3169
|
+
const idx = items[0].dataIndex;
|
|
3170
|
+
const m = modelEffLabels[idx];
|
|
3171
|
+
const d = modelEff[m];
|
|
3172
|
+
const lines = [];
|
|
3173
|
+
lines.push(`Sessions: ${d.sessions}`);
|
|
3174
|
+
lines.push(`Total: $${d.cost.toFixed(2)} | ${formatTokens(d.tokens)} tokens`);
|
|
3175
|
+
if (d.tokensPerCommit) lines.push(`${formatTokens(d.tokensPerCommit)} tokens/commit`);
|
|
3176
|
+
return lines;
|
|
3177
|
+
},
|
|
3178
|
+
},
|
|
3179
|
+
},
|
|
3180
|
+
},
|
|
3154
3181
|
scales: {
|
|
3155
|
-
y: { type: 'linear', position: 'left', title: { display: true, text: 'Avg $/Commit' } },
|
|
3156
|
-
y1: { type: 'linear', position: 'right', title: { display: true, text: '
|
|
3182
|
+
y: { type: 'linear', position: 'left', title: { display: true, text: 'Avg $/Commit' }, ticks: { callback: v => '$' + v.toFixed(1) } },
|
|
3183
|
+
y1: { type: 'linear', position: 'right', title: { display: true, text: 'Commits' }, grid: { drawOnChartArea: false } },
|
|
3157
3184
|
},
|
|
3158
3185
|
},
|
|
3159
3186
|
});
|
|
@@ -3263,10 +3290,93 @@ window.toggleTimelineScale = function() {
|
|
|
3263
3290
|
timelineChart.options.scales.y.type = scaleType;
|
|
3264
3291
|
timelineChart.options.scales.y1.type = scaleType;
|
|
3265
3292
|
timelineChart.update();
|
|
3266
|
-
const btn = document.querySelector('.scale-toggle');
|
|
3293
|
+
const btn = document.querySelector('#chart-timeline').closest('.chart-card').querySelector('.scale-toggle');
|
|
3267
3294
|
if (btn) btn.textContent = timelineLogScale ? 'Linear scale' : 'Log scale';
|
|
3268
3295
|
};
|
|
3269
3296
|
|
|
3297
|
+
window.toggleTokenBurnPercent = function() {
|
|
3298
|
+
if (!tokenBurnChart) return;
|
|
3299
|
+
tokenBurnPercentMode = !tokenBurnPercentMode;
|
|
3300
|
+
const dailyTokens = DATA.daily;
|
|
3301
|
+
if (tokenBurnPercentMode) {
|
|
3302
|
+
// Switch to split line view — each token type gets its own line
|
|
3303
|
+
tokenBurnChart.data.datasets[0].type = 'line';
|
|
3304
|
+
tokenBurnChart.data.datasets[0].borderColor = 'rgba(59, 130, 246, 1)';
|
|
3305
|
+
tokenBurnChart.data.datasets[0].backgroundColor = 'rgba(59, 130, 246, 0.1)';
|
|
3306
|
+
tokenBurnChart.data.datasets[0].fill = true;
|
|
3307
|
+
tokenBurnChart.data.datasets[0].stack = undefined;
|
|
3308
|
+
tokenBurnChart.data.datasets[0].borderWidth = 2;
|
|
3309
|
+
tokenBurnChart.data.datasets[0].pointRadius = 2;
|
|
3310
|
+
tokenBurnChart.data.datasets[0].tension = 0.3;
|
|
3311
|
+
|
|
3312
|
+
tokenBurnChart.data.datasets[1].type = 'line';
|
|
3313
|
+
tokenBurnChart.data.datasets[1].borderColor = 'rgba(168, 85, 247, 1)';
|
|
3314
|
+
tokenBurnChart.data.datasets[1].backgroundColor = 'rgba(168, 85, 247, 0.1)';
|
|
3315
|
+
tokenBurnChart.data.datasets[1].fill = true;
|
|
3316
|
+
tokenBurnChart.data.datasets[1].stack = undefined;
|
|
3317
|
+
tokenBurnChart.data.datasets[1].borderWidth = 2;
|
|
3318
|
+
tokenBurnChart.data.datasets[1].pointRadius = 2;
|
|
3319
|
+
tokenBurnChart.data.datasets[1].tension = 0.3;
|
|
3320
|
+
|
|
3321
|
+
tokenBurnChart.data.datasets[2].type = 'line';
|
|
3322
|
+
tokenBurnChart.data.datasets[2].borderColor = 'rgba(6, 182, 212, 1)';
|
|
3323
|
+
tokenBurnChart.data.datasets[2].backgroundColor = 'rgba(6, 182, 212, 0.1)';
|
|
3324
|
+
tokenBurnChart.data.datasets[2].fill = true;
|
|
3325
|
+
tokenBurnChart.data.datasets[2].stack = undefined;
|
|
3326
|
+
tokenBurnChart.data.datasets[2].borderWidth = 2;
|
|
3327
|
+
tokenBurnChart.data.datasets[2].pointRadius = 2;
|
|
3328
|
+
tokenBurnChart.data.datasets[2].tension = 0.3;
|
|
3329
|
+
|
|
3330
|
+
tokenBurnChart.data.datasets[3].hidden = true;
|
|
3331
|
+
tokenBurnChart.options.scales.x.stacked = false;
|
|
3332
|
+
tokenBurnChart.options.scales.y.stacked = false;
|
|
3333
|
+
tokenBurnChart.options.scales.y.type = 'logarithmic';
|
|
3334
|
+
tokenBurnChart.options.scales.y.title.text = 'Tokens / Day (log)';
|
|
3335
|
+
tokenBurnChart.options.scales.y1.display = false;
|
|
3336
|
+
} else {
|
|
3337
|
+
// Restore stacked bar view
|
|
3338
|
+
tokenBurnChart.data.datasets[0].type = 'bar';
|
|
3339
|
+
tokenBurnChart.data.datasets[0].backgroundColor = 'rgba(59, 130, 246, 0.8)';
|
|
3340
|
+
tokenBurnChart.data.datasets[0].stack = 'tokens';
|
|
3341
|
+
tokenBurnChart.data.datasets[0].borderRadius = 2;
|
|
3342
|
+
delete tokenBurnChart.data.datasets[0].borderColor;
|
|
3343
|
+
delete tokenBurnChart.data.datasets[0].fill;
|
|
3344
|
+
delete tokenBurnChart.data.datasets[0].borderWidth;
|
|
3345
|
+
delete tokenBurnChart.data.datasets[0].pointRadius;
|
|
3346
|
+
delete tokenBurnChart.data.datasets[0].tension;
|
|
3347
|
+
|
|
3348
|
+
tokenBurnChart.data.datasets[1].type = 'bar';
|
|
3349
|
+
tokenBurnChart.data.datasets[1].backgroundColor = 'rgba(168, 85, 247, 0.8)';
|
|
3350
|
+
tokenBurnChart.data.datasets[1].stack = 'tokens';
|
|
3351
|
+
tokenBurnChart.data.datasets[1].borderRadius = 2;
|
|
3352
|
+
delete tokenBurnChart.data.datasets[1].borderColor;
|
|
3353
|
+
delete tokenBurnChart.data.datasets[1].fill;
|
|
3354
|
+
delete tokenBurnChart.data.datasets[1].borderWidth;
|
|
3355
|
+
delete tokenBurnChart.data.datasets[1].pointRadius;
|
|
3356
|
+
delete tokenBurnChart.data.datasets[1].tension;
|
|
3357
|
+
|
|
3358
|
+
tokenBurnChart.data.datasets[2].type = 'bar';
|
|
3359
|
+
tokenBurnChart.data.datasets[2].backgroundColor = 'rgba(6, 182, 212, 0.6)';
|
|
3360
|
+
tokenBurnChart.data.datasets[2].stack = 'tokens';
|
|
3361
|
+
tokenBurnChart.data.datasets[2].borderRadius = 2;
|
|
3362
|
+
delete tokenBurnChart.data.datasets[2].borderColor;
|
|
3363
|
+
delete tokenBurnChart.data.datasets[2].fill;
|
|
3364
|
+
delete tokenBurnChart.data.datasets[2].borderWidth;
|
|
3365
|
+
delete tokenBurnChart.data.datasets[2].pointRadius;
|
|
3366
|
+
delete tokenBurnChart.data.datasets[2].tension;
|
|
3367
|
+
|
|
3368
|
+
tokenBurnChart.data.datasets[3].hidden = false;
|
|
3369
|
+
tokenBurnChart.options.scales.x.stacked = true;
|
|
3370
|
+
tokenBurnChart.options.scales.y.stacked = true;
|
|
3371
|
+
tokenBurnChart.options.scales.y.type = 'linear';
|
|
3372
|
+
tokenBurnChart.options.scales.y.title.text = 'Tokens / Day';
|
|
3373
|
+
tokenBurnChart.options.scales.y1.display = true;
|
|
3374
|
+
}
|
|
3375
|
+
tokenBurnChart.update();
|
|
3376
|
+
const btn = document.querySelector('#chart-token-burn').closest('.chart-card').querySelector('.scale-toggle');
|
|
3377
|
+
if (btn) btn.textContent = tokenBurnPercentMode ? 'Stacked' : 'Split view';
|
|
3378
|
+
};
|
|
3379
|
+
|
|
3270
3380
|
/* ── Share Report Card ─────────────────────────── */
|
|
3271
3381
|
|
|
3272
3382
|
const GRADE_HEX = { A: '#22d3a8', B: '#3b82f6', C: '#f59e0b', D: '#f0883e', F: '#ef4444' };
|
package/src/index.js
CHANGED
|
@@ -15,6 +15,20 @@ const { version: VERSION } = JSON.parse(
|
|
|
15
15
|
readFileSync(new URL('../package.json', import.meta.url), 'utf8')
|
|
16
16
|
);
|
|
17
17
|
|
|
18
|
+
// ── pretty CLI output helpers ──
|
|
19
|
+
const c = {
|
|
20
|
+
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
21
|
+
green: '\x1b[32m', yellow: '\x1b[33m', cyan: '\x1b[36m', red: '\x1b[31m',
|
|
22
|
+
};
|
|
23
|
+
const icon = {
|
|
24
|
+
ok: `${c.green}✔${c.reset}`,
|
|
25
|
+
dot: `${c.cyan}◆${c.reset}`,
|
|
26
|
+
arrow: `${c.cyan}▸${c.reset}`,
|
|
27
|
+
warn: `${c.yellow}⚠${c.reset}`,
|
|
28
|
+
err: `${c.red}✖${c.reset}`,
|
|
29
|
+
};
|
|
30
|
+
const fmt = (ms) => ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`;
|
|
31
|
+
|
|
18
32
|
async function buildPayload(claudeDir, days, project, forceRefresh = false) {
|
|
19
33
|
// Step 1: Parse sessions (with caching)
|
|
20
34
|
let sessions;
|
|
@@ -23,7 +37,7 @@ async function buildPayload(claudeDir, days, project, forceRefresh = false) {
|
|
|
23
37
|
|
|
24
38
|
if (forceRefresh) {
|
|
25
39
|
deleteCache();
|
|
26
|
-
console.log(
|
|
40
|
+
console.log(` ${icon.arrow} Cache cleared, performing full parse...`);
|
|
27
41
|
}
|
|
28
42
|
|
|
29
43
|
const cached = forceRefresh ? null : loadCache();
|
|
@@ -38,18 +52,18 @@ async function buildPayload(claudeDir, days, project, forceRefresh = false) {
|
|
|
38
52
|
if (newCount === 0 && modifiedCount === 0 && deletedCount === 0) {
|
|
39
53
|
sessions = cached.sessions;
|
|
40
54
|
fileIndex = cached.fileIndex;
|
|
41
|
-
console.log(`Parsing sessions
|
|
55
|
+
console.log(` ${icon.ok} Parsing sessions ${c.dim}── ${cached.sessions.length} cached (${fmt(Date.now() - startParse)})${c.reset}`);
|
|
42
56
|
} else {
|
|
43
57
|
const { sessions: freshSessions, fileIndex: freshIndex } = await parseAllProjects(claudeDir, days, project);
|
|
44
58
|
sessions = freshSessions;
|
|
45
59
|
fileIndex = freshIndex;
|
|
46
|
-
console.log(`Parsing sessions
|
|
60
|
+
console.log(` ${icon.ok} Parsing sessions ${c.dim}── ${newCount} new, ${modifiedCount} updated, ${Math.max(0, cachedCount)} cached (${fmt(Date.now() - startParse)})${c.reset}`);
|
|
47
61
|
}
|
|
48
62
|
} else {
|
|
49
63
|
const result = await parseAllProjects(claudeDir, days, project);
|
|
50
64
|
sessions = result.sessions;
|
|
51
65
|
fileIndex = result.fileIndex;
|
|
52
|
-
console.log(`Parsing sessions
|
|
66
|
+
console.log(` ${icon.ok} Parsing sessions ${c.dim}── ${sessions.length} parsed (${fmt(Date.now() - startParse)})${c.reset}`);
|
|
53
67
|
}
|
|
54
68
|
|
|
55
69
|
if (sessions.length === 0) {
|
|
@@ -63,11 +77,11 @@ async function buildPayload(claudeDir, days, project, forceRefresh = false) {
|
|
|
63
77
|
for (const repoPath of repoPathsSet) {
|
|
64
78
|
commitsByRepo[repoPath] = analyzeGitRepo(repoPath, days);
|
|
65
79
|
}
|
|
66
|
-
console.log(`Analyzing ${repoPathsSet.size}
|
|
80
|
+
console.log(` ${icon.ok} Analyzing git repos ${c.dim}── ${repoPathsSet.size} repos (${fmt(Date.now() - startGit)})${c.reset}`);
|
|
67
81
|
|
|
68
82
|
// Step 3: Correlate sessions with commits
|
|
69
83
|
const { correlatedSessions, organicCommits } = correlateSessions(sessions, commitsByRepo);
|
|
70
|
-
console.log(
|
|
84
|
+
console.log(` ${icon.ok} Correlating sessions ${c.dim}── done${c.reset}`);
|
|
71
85
|
|
|
72
86
|
// Step 4: Compute metrics
|
|
73
87
|
const payload = computeMetrics(correlatedSessions, organicCommits, commitsByRepo, days);
|
|
@@ -100,11 +114,10 @@ async function main() {
|
|
|
100
114
|
|
|
101
115
|
const invokedAs = path.basename(process.argv[1]);
|
|
102
116
|
if (invokedAs.includes('claude-roi')) {
|
|
103
|
-
console.log(
|
|
104
|
-
console.log(`
|
|
105
|
-
console.log('');
|
|
117
|
+
console.log(` ${icon.warn} ${c.yellow}claude-roi has been renamed to codelens-ai${c.reset}`);
|
|
118
|
+
console.log(` Switch to: ${c.cyan}npx codelens-ai${c.reset}\n`);
|
|
106
119
|
}
|
|
107
|
-
console.log(
|
|
120
|
+
console.log(`${icon.dot} ${c.bold}${c.cyan}codelens-ai${c.reset} v${VERSION}\n`);
|
|
108
121
|
|
|
109
122
|
const claudeDir = path.join(os.homedir(), '.claude', 'projects');
|
|
110
123
|
|
|
@@ -112,8 +125,8 @@ async function main() {
|
|
|
112
125
|
if (payload) payload.meta.invokedAs = invokedAs.includes('claude-roi') ? 'claude-roi' : 'codelens-ai';
|
|
113
126
|
|
|
114
127
|
if (!payload) {
|
|
115
|
-
console.log(
|
|
116
|
-
console.log(
|
|
128
|
+
console.log(` ${icon.warn} ${c.yellow}No Claude Code sessions found.${c.reset}`);
|
|
129
|
+
console.log(` Make sure you have used Claude Code and session files exist in ~/.claude/projects/`);
|
|
117
130
|
process.exit(0);
|
|
118
131
|
}
|
|
119
132
|
|
|
@@ -138,7 +151,7 @@ async function main() {
|
|
|
138
151
|
console.log(` ${line}`);
|
|
139
152
|
if (am.topVerificationCommands.length > 0) {
|
|
140
153
|
const top3 = am.topVerificationCommands.slice(0, 3)
|
|
141
|
-
.map(
|
|
154
|
+
.map(cmd => `${cmd.command} (${cmd.count})`).join(', ');
|
|
142
155
|
console.log(` Top Tests: ${top3}`);
|
|
143
156
|
}
|
|
144
157
|
console.log('');
|
|
@@ -150,7 +163,7 @@ async function main() {
|
|
|
150
163
|
const app = createServer(payload, rebuild);
|
|
151
164
|
const server = app.listen(port, () => {
|
|
152
165
|
const url = `http://localhost:${port}`;
|
|
153
|
-
console.log(`\
|
|
166
|
+
console.log(`\n ${icon.ok} ${c.green}Dashboard:${c.reset} ${c.bold}${url}${c.reset}`);
|
|
154
167
|
|
|
155
168
|
if (opts.open !== false) {
|
|
156
169
|
import('open').then(mod => mod.default(url)).catch(() => {
|
|
@@ -161,7 +174,7 @@ async function main() {
|
|
|
161
174
|
|
|
162
175
|
server.on('error', (err) => {
|
|
163
176
|
if (err.code === 'EADDRINUSE') {
|
|
164
|
-
console.error(
|
|
177
|
+
console.error(` ${icon.err} ${c.red}Port ${port} is already in use.${c.reset} Try: codelens-ai --port ${port + 1}`);
|
|
165
178
|
process.exit(1);
|
|
166
179
|
}
|
|
167
180
|
throw err;
|
|
@@ -169,6 +182,6 @@ async function main() {
|
|
|
169
182
|
}
|
|
170
183
|
|
|
171
184
|
main().catch(err => {
|
|
172
|
-
console.error(
|
|
185
|
+
console.error(` ${icon.err} ${c.red}Error:${c.reset}`, err.message);
|
|
173
186
|
process.exit(1);
|
|
174
187
|
});
|
package/src/server.js
CHANGED
|
@@ -25,14 +25,14 @@ export function createServer(initialPayload, rebuildFn) {
|
|
|
25
25
|
app.post('/api/refresh', async (req, res) => {
|
|
26
26
|
if (!rebuildFn) return res.status(501).json({ error: 'Refresh not available' });
|
|
27
27
|
try {
|
|
28
|
-
console.log('\x1b[36m[refresh]\x1b[0m Re-parsing sessions and recomputing metrics...');
|
|
28
|
+
console.log(' \x1b[36m▸\x1b[0m \x1b[36m[refresh]\x1b[0m Re-parsing sessions and recomputing metrics...');
|
|
29
29
|
const newPayload = await rebuildFn();
|
|
30
30
|
if (!newPayload) return res.status(404).json({ error: 'No sessions found after refresh' });
|
|
31
31
|
payload = newPayload;
|
|
32
|
-
console.log('\x1b[32m[refresh]\x1b[0m Done');
|
|
32
|
+
console.log(' \x1b[32m✔\x1b[0m \x1b[32m[refresh]\x1b[0m Done');
|
|
33
33
|
res.json({ ok: true });
|
|
34
34
|
} catch (err) {
|
|
35
|
-
console.error('\x1b[31m[refresh]\x1b[0m Error:', err.message);
|
|
35
|
+
console.error(' \x1b[31m✖\x1b[0m \x1b[31m[refresh]\x1b[0m Error:', err.message);
|
|
36
36
|
res.status(500).json({ error: err.message });
|
|
37
37
|
}
|
|
38
38
|
});
|