cchubber 0.5.5 → 0.5.7
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/analyzers/value-tracker.js +157 -0
- package/src/cli/index.js +5 -0
- package/src/renderers/html-report.js +66 -7
package/package.json
CHANGED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Value Tracker — measures whether you're getting less for your money over time.
|
|
3
|
+
*
|
|
4
|
+
* Two core metrics:
|
|
5
|
+
* - Output per message: are Claude's responses getting shorter? (model degradation)
|
|
6
|
+
* - Output per dollar: are you getting less work per dollar? (pricing/caching changes)
|
|
7
|
+
*
|
|
8
|
+
* Uses z-score anomaly detection (2 SD) to flag days where value drops significantly.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export function analyzeValueTrend(dailyFromJSONL, calculatedDailyCosts, inflectionDate) {
|
|
12
|
+
if (!dailyFromJSONL || dailyFromJSONL.length < 3) {
|
|
13
|
+
return { available: false };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Build cost lookup from calculated costs (not raw costUSD which is 0 for subscription)
|
|
17
|
+
const costByDate = {};
|
|
18
|
+
if (calculatedDailyCosts) {
|
|
19
|
+
for (const d of calculatedDailyCosts) {
|
|
20
|
+
costByDate[d.date] = d.cost || 0;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Calculate per-day metrics
|
|
25
|
+
const daily = dailyFromJSONL
|
|
26
|
+
.filter(d => d.outputTokens > 0 && d.messageCount > 0)
|
|
27
|
+
.map(d => {
|
|
28
|
+
const cost = costByDate[d.date] || 0;
|
|
29
|
+
return {
|
|
30
|
+
date: d.date,
|
|
31
|
+
outputPerMsg: Math.round(d.outputTokens / d.messageCount),
|
|
32
|
+
outputPerDollar: cost > 0 ? Math.round(d.outputTokens / cost) : 0,
|
|
33
|
+
wordsPerMsg: Math.round(d.outputTokens / d.messageCount / 1.3), // ~1.3 tokens per word
|
|
34
|
+
outputTokens: d.outputTokens,
|
|
35
|
+
messageCount: d.messageCount,
|
|
36
|
+
cost,
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (daily.length < 3) return { available: false };
|
|
41
|
+
|
|
42
|
+
// Overall averages
|
|
43
|
+
const avgOutputPerMsg = Math.round(daily.reduce((s, d) => s + d.outputPerMsg, 0) / daily.length);
|
|
44
|
+
const avgOutputPerDollar = daily.filter(d => d.outputPerDollar > 0).length > 0
|
|
45
|
+
? Math.round(daily.filter(d => d.outputPerDollar > 0).reduce((s, d) => s + d.outputPerDollar, 0) / daily.filter(d => d.outputPerDollar > 0).length)
|
|
46
|
+
: 0;
|
|
47
|
+
|
|
48
|
+
// Recent 7 days vs older — is value declining?
|
|
49
|
+
const sorted = [...daily].sort((a, b) => a.date.localeCompare(b.date));
|
|
50
|
+
const recent = sorted.slice(-7);
|
|
51
|
+
// If inflection date exists, use pre-inflection as the "before" baseline
|
|
52
|
+
// This gives a clean comparison: healthy baseline vs current reality
|
|
53
|
+
const older = inflectionDate
|
|
54
|
+
? sorted.filter(d => d.date < inflectionDate)
|
|
55
|
+
: sorted.slice(0, -7);
|
|
56
|
+
|
|
57
|
+
let trend = 'stable';
|
|
58
|
+
let trendDetail = '';
|
|
59
|
+
let comparison = null;
|
|
60
|
+
|
|
61
|
+
if (older.length >= 3) {
|
|
62
|
+
const recentAvgPerMsg = Math.round(recent.reduce((s, d) => s + d.outputPerMsg, 0) / recent.length);
|
|
63
|
+
const olderAvgPerMsg = Math.round(older.reduce((s, d) => s + d.outputPerMsg, 0) / older.length);
|
|
64
|
+
|
|
65
|
+
const recentAvgPerDollar = recent.filter(d => d.outputPerDollar > 0).length > 0
|
|
66
|
+
? Math.round(recent.filter(d => d.outputPerDollar > 0).reduce((s, d) => s + d.outputPerDollar, 0) / recent.filter(d => d.outputPerDollar > 0).length)
|
|
67
|
+
: 0;
|
|
68
|
+
const olderAvgPerDollar = older.filter(d => d.outputPerDollar > 0).length > 0
|
|
69
|
+
? Math.round(older.filter(d => d.outputPerDollar > 0).reduce((s, d) => s + d.outputPerDollar, 0) / older.filter(d => d.outputPerDollar > 0).length)
|
|
70
|
+
: 0;
|
|
71
|
+
|
|
72
|
+
const msgChange = olderAvgPerMsg > 0 ? ((recentAvgPerMsg - olderAvgPerMsg) / olderAvgPerMsg * 100) : 0;
|
|
73
|
+
const dollarChange = olderAvgPerDollar > 0 ? ((recentAvgPerDollar - olderAvgPerDollar) / olderAvgPerDollar * 100) : 0;
|
|
74
|
+
|
|
75
|
+
if (msgChange < -20) {
|
|
76
|
+
trend = 'declining';
|
|
77
|
+
trendDetail = `Output per message dropped ${Math.abs(Math.round(msgChange))}% recently (${olderAvgPerMsg} → ${recentAvgPerMsg} tokens/msg)`;
|
|
78
|
+
} else if (msgChange > 20) {
|
|
79
|
+
trend = 'improving';
|
|
80
|
+
trendDetail = `Output per message increased ${Math.round(msgChange)}% recently (${olderAvgPerMsg} → ${recentAvgPerMsg} tokens/msg)`;
|
|
81
|
+
} else {
|
|
82
|
+
trendDetail = `Output per message stable (${recentAvgPerMsg} tokens/msg, was ${olderAvgPerMsg})`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Expose before/after for visual comparison
|
|
86
|
+
comparison = {
|
|
87
|
+
olderAvgPerMsg, recentAvgPerMsg, msgChangePct: Math.round(msgChange),
|
|
88
|
+
olderAvgPerDollar, recentAvgPerDollar, dollarChangePct: Math.round(dollarChange),
|
|
89
|
+
olderWords: Math.round(olderAvgPerMsg / 1.3),
|
|
90
|
+
recentWords: Math.round(recentAvgPerMsg / 1.3),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Z-score anomaly detection — flag days where value drops >2 SD
|
|
95
|
+
const values = daily.map(d => d.outputPerMsg);
|
|
96
|
+
const mean = values.reduce((s, v) => s + v, 0) / values.length;
|
|
97
|
+
const variance = values.reduce((s, v) => s + Math.pow(v - mean, 2), 0) / values.length;
|
|
98
|
+
const stdDev = Math.sqrt(variance);
|
|
99
|
+
|
|
100
|
+
const anomalies = [];
|
|
101
|
+
if (stdDev > 0) {
|
|
102
|
+
for (const d of daily) {
|
|
103
|
+
const z = (d.outputPerMsg - mean) / stdDev;
|
|
104
|
+
if (z < -2) {
|
|
105
|
+
anomalies.push({
|
|
106
|
+
date: d.date,
|
|
107
|
+
type: 'low_output',
|
|
108
|
+
outputPerMsg: d.outputPerMsg,
|
|
109
|
+
expected: avgOutputPerMsg,
|
|
110
|
+
deviation: Math.abs(Math.round(z * 10) / 10),
|
|
111
|
+
detail: `${d.outputPerMsg} tokens/msg vs ${avgOutputPerMsg} avg (${Math.abs(Math.round(z * 10) / 10)}σ below)`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Cost per message anomalies — days where each message cost way more than usual (caching broke)
|
|
118
|
+
const costPerMsgValues = daily.filter(d => d.cost > 0).map(d => d.cost / d.messageCount);
|
|
119
|
+
const cpmMean = costPerMsgValues.length > 0 ? costPerMsgValues.reduce((s, v) => s + v, 0) / costPerMsgValues.length : 0;
|
|
120
|
+
const cpmVariance = costPerMsgValues.length > 0 ? costPerMsgValues.reduce((s, v) => s + Math.pow(v - cpmMean, 2), 0) / costPerMsgValues.length : 0;
|
|
121
|
+
const cpmStdDev = Math.sqrt(cpmVariance);
|
|
122
|
+
|
|
123
|
+
// Use median as baseline — more robust to outliers than mean/z-score
|
|
124
|
+
const sortedCpm = [...costPerMsgValues].sort((a, b) => a - b);
|
|
125
|
+
const cpmMedian = sortedCpm.length > 0 ? sortedCpm[Math.floor(sortedCpm.length / 2)] : 0;
|
|
126
|
+
|
|
127
|
+
const costAnomalies = [];
|
|
128
|
+
if (cpmMedian > 0) {
|
|
129
|
+
for (const d of daily.filter(dd => dd.cost > 0 && dd.messageCount >= 10)) {
|
|
130
|
+
const cpm = d.cost / d.messageCount;
|
|
131
|
+
const multiplier = cpm / cpmMedian;
|
|
132
|
+
if (multiplier >= 1.8) { // 80%+ above median cost per message
|
|
133
|
+
costAnomalies.push({
|
|
134
|
+
date: d.date,
|
|
135
|
+
costPerMsg: Math.round(cpm * 100) / 100,
|
|
136
|
+
medianCostPerMsg: Math.round(cpmMedian * 100) / 100,
|
|
137
|
+
multiplier: Math.round(multiplier * 10) / 10,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
available: true,
|
|
145
|
+
daily: daily.map(d => ({ date: d.date, outputPerMsg: d.outputPerMsg, outputPerDollar: d.outputPerDollar, wordsPerMsg: d.wordsPerMsg })),
|
|
146
|
+
avgWordsPerMsg: Math.round(daily.reduce((s, d) => s + d.wordsPerMsg, 0) / daily.length),
|
|
147
|
+
avgOutputPerMsg,
|
|
148
|
+
avgOutputPerDollar,
|
|
149
|
+
trend,
|
|
150
|
+
trendDetail,
|
|
151
|
+
comparison,
|
|
152
|
+
anomalies,
|
|
153
|
+
costAnomalies,
|
|
154
|
+
avgCostPerMsg: Math.round(cpmMean * 100) / 100,
|
|
155
|
+
dayCount: daily.length,
|
|
156
|
+
};
|
|
157
|
+
}
|
package/src/cli/index.js
CHANGED
|
@@ -25,6 +25,7 @@ import { generateRecommendations } from '../analyzers/recommendations.js';
|
|
|
25
25
|
import { detectInflectionPoints } from '../analyzers/inflection-detector.js';
|
|
26
26
|
import { analyzeSessionIntelligence } from '../analyzers/session-intelligence.js';
|
|
27
27
|
import { analyzeModelRouting } from '../analyzers/model-routing.js';
|
|
28
|
+
import { analyzeValueTrend } from '../analyzers/value-tracker.js';
|
|
28
29
|
import { renderHTML } from '../renderers/html-report.js';
|
|
29
30
|
import { renderTerminal } from '../renderers/terminal-summary.js';
|
|
30
31
|
import { shouldSendTelemetry, sendTelemetry } from '../telemetry.js';
|
|
@@ -134,6 +135,9 @@ async function main() {
|
|
|
134
135
|
const inflection = detectInflectionPoints(dailyFromJSONL);
|
|
135
136
|
const sessionIntel = analyzeSessionIntelligence(sessionMeta, jsonlEntries);
|
|
136
137
|
const modelRouting = analyzeModelRouting(costAnalysis, jsonlEntries);
|
|
138
|
+
const valueTrend = analyzeValueTrend(dailyFromJSONL, costAnalysis.dailyCosts, inflection?.date);
|
|
139
|
+
if (valueTrend.available) console.log(` ✓ Value trend: ${valueTrend.avgOutputPerMsg} tokens/msg avg (${valueTrend.trend})`);
|
|
140
|
+
|
|
137
141
|
const recommendations = generateRecommendations(costAnalysis, cacheHealth, claudeMdStack, anomalies, inflection, sessionIntel, modelRouting, projectBreakdown);
|
|
138
142
|
|
|
139
143
|
if (inflection) console.log(` ✓ Inflection: ${inflection.summary}`);
|
|
@@ -166,6 +170,7 @@ async function main() {
|
|
|
166
170
|
claudeMdStack,
|
|
167
171
|
oauthUsage,
|
|
168
172
|
recommendations,
|
|
173
|
+
valueTrend,
|
|
169
174
|
communityStats,
|
|
170
175
|
history: getHistory(),
|
|
171
176
|
};
|
|
@@ -18,6 +18,7 @@ export function renderHTML(report) {
|
|
|
18
18
|
|
|
19
19
|
const dailyCostsJSON = JSON.stringify(dailyCosts.map(d => ({
|
|
20
20
|
date: d.date, cost: d.cost, cacheOutputRatio: d.cacheOutputRatio || 0, isAnomaly: anomalyDates.has(d.date),
|
|
21
|
+
out: d.outputTokens || 0, inp: d.inputTokens || 0, cr: d.cacheReadTokens || 0,
|
|
21
22
|
})));
|
|
22
23
|
|
|
23
24
|
const projectsJSON = JSON.stringify((projectBreakdown || []).map(p => ({
|
|
@@ -353,6 +354,7 @@ ${inflection && inflection.multiplier >= 1.5 ? `
|
|
|
353
354
|
<span class="font-mono text-2xl font-bold block text-[#e3e2e3]">${costAnalysis.sessions?.total || 0}</span>
|
|
354
355
|
<span class="text-[10px] text-[#908fa0] mt-1 block font-mono">${costAnalysis.sessions?.avgDurationMinutes ? Math.round(costAnalysis.sessions.avgDurationMinutes) + ' min avg' : ''}</span>
|
|
355
356
|
</div>`}
|
|
357
|
+
|
|
356
358
|
</section>
|
|
357
359
|
|
|
358
360
|
<!-- 4. COST TREND CHART -->
|
|
@@ -372,6 +374,62 @@ ${inflection && inflection.multiplier >= 1.5 ? `
|
|
|
372
374
|
<svg id="cost-chart-svg" viewBox="0 0 900 200" preserveAspectRatio="xMidYMid meet"></svg>
|
|
373
375
|
</section>
|
|
374
376
|
|
|
377
|
+
${report.valueTrend?.available ? (() => {
|
|
378
|
+
const vt = report.valueTrend;
|
|
379
|
+
const comp = vt.comparison;
|
|
380
|
+
const trendColor = vt.trend === 'declining' ? '#ffb4ab' : vt.trend === 'improving' ? '#10b981' : '#908fa0';
|
|
381
|
+
|
|
382
|
+
// Build verdict around output per dollar — the metric people actually care about
|
|
383
|
+
let verdict, verdictColor, verdictDetail;
|
|
384
|
+
if (!comp || !comp.olderAvgPerDollar) {
|
|
385
|
+
verdict = 'Not enough data to compare yet. Run again next week.';
|
|
386
|
+
verdictColor = '#908fa0';
|
|
387
|
+
verdictDetail = '';
|
|
388
|
+
} else if (comp.dollarChangePct < -20) {
|
|
389
|
+
verdict = 'Yes. ' + Math.abs(comp.dollarChangePct) + '% less output per dollar recently.';
|
|
390
|
+
verdictColor = '#ffb4ab';
|
|
391
|
+
var limitImpact = Math.round(100 / (100 - Math.abs(comp.dollarChangePct)) * 100 - 100);
|
|
392
|
+
verdictDetail = 'Before: ' + comp.olderAvgPerDollar.toLocaleString() + ' tokens/$1. Now: ' + comp.recentAvgPerDollar.toLocaleString() + ' tokens/$1. In practice, your usage limits run out ~' + limitImpact + '% faster than before. This could be caused by cache changes, heavier Opus usage, or longer sessions.';
|
|
393
|
+
} else if (comp.dollarChangePct > 20) {
|
|
394
|
+
verdict = 'No. Actually getting ' + comp.dollarChangePct + '% more output per dollar recently.';
|
|
395
|
+
verdictColor = '#10b981';
|
|
396
|
+
verdictDetail = 'Before: ' + comp.olderAvgPerDollar.toLocaleString() + ' tokens/$1. Now: ' + comp.recentAvgPerDollar.toLocaleString() + ' tokens/$1.';
|
|
397
|
+
} else {
|
|
398
|
+
verdict = 'No. Your output per dollar is stable.';
|
|
399
|
+
verdictColor = '#10b981';
|
|
400
|
+
verdictDetail = comp.recentAvgPerDollar.toLocaleString() + ' output tokens per $1 (was ' + comp.olderAvgPerDollar.toLocaleString() + ').';
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return `
|
|
404
|
+
<!-- VALUE TREND -->
|
|
405
|
+
<section class="bg-[#1b1c1d] p-8 rounded-xl border border-[rgba(70,69,84,0.15)]">
|
|
406
|
+
<h3 class="text-xl font-bold text-[#e3e2e3] mb-4">Are you getting less for your money?</h3>
|
|
407
|
+
<p class="text-[15px] font-semibold mb-1" style="color:${verdictColor}">${verdict}</p>
|
|
408
|
+
${verdictDetail ? '<p class="text-[12px] text-[#908fa0] mb-2">' + verdictDetail + '</p>' : ''}
|
|
409
|
+
${inflection ? '<p class="text-[11px] text-[#908fa0] mb-4">A cache change was detected on ' + esc(inflection.date) + '. Your recent 7 days reflect where you are now.</p>' : ''}
|
|
410
|
+
|
|
411
|
+
${(vt.anomalies?.length > 0 || vt.costAnomalies?.length > 0) ? `
|
|
412
|
+
<div class="mt-6 space-y-2">
|
|
413
|
+
${vt.anomalies?.length > 0 ? `
|
|
414
|
+
<span class="text-[10px] uppercase tracking-[0.05em] text-[#908fa0] block mb-1">Days with unusually short responses</span>
|
|
415
|
+
${vt.anomalies.slice(0, 3).map(a => `
|
|
416
|
+
<div class="p-3 bg-[#0d0e0f] rounded-lg flex items-center justify-between">
|
|
417
|
+
<span class="text-[11px] font-mono text-[#908fa0]">${a.date}</span>
|
|
418
|
+
<span class="text-[11px] text-[#e3e2e3]">~${Math.round(a.outputPerMsg / 1.3)} words/response</span>
|
|
419
|
+
<span class="text-[10px] font-mono px-2 py-0.5 rounded" style="background:rgba(255,180,171,0.15);color:#ffb4ab">${a.deviation}σ below avg</span>
|
|
420
|
+
</div>`).join('')}` : ''}
|
|
421
|
+
${vt.costAnomalies?.length > 0 ? `
|
|
422
|
+
<span class="text-[10px] uppercase tracking-[0.05em] text-[#908fa0] block mt-3 mb-1">Days where each message cost more than usual (${vt.costAnomalies.length} days)</span>
|
|
423
|
+
${vt.costAnomalies.slice(0, 5).map(a => `
|
|
424
|
+
<div class="p-3 bg-[#0d0e0f] rounded-lg grid" style="grid-template-columns:100px 1fr auto;gap:12px;align-items:center">
|
|
425
|
+
<span class="text-[11px] font-mono text-[#908fa0]">${a.date}</span>
|
|
426
|
+
<span class="text-[11px] text-[#e3e2e3]">$${a.costPerMsg}/msg vs $${a.medianCostPerMsg} median</span>
|
|
427
|
+
<span class="text-[10px] font-mono px-2 py-0.5 rounded" style="background:rgba(255,180,171,0.15);color:#ffb4ab">${a.multiplier}x</span>
|
|
428
|
+
</div>`).join('')}` : ''}
|
|
429
|
+
</div>` : ''}
|
|
430
|
+
</section>`;
|
|
431
|
+
})() : ''}
|
|
432
|
+
|
|
375
433
|
<!-- 5. SESSION INTELLIGENCE + MODEL DISTRIBUTION -->
|
|
376
434
|
<section class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
377
435
|
|
|
@@ -521,9 +579,9 @@ ${inflection && inflection.multiplier >= 1.5 ? `
|
|
|
521
579
|
<th class="px-8 py-3 text-[10px] uppercase font-bold tracking-[0.05em] text-[#908fa0] text-left">#</th>
|
|
522
580
|
<th class="px-4 py-3 text-[10px] uppercase font-bold tracking-[0.05em] text-[#908fa0]">Grade</th>
|
|
523
581
|
<th class="px-4 py-3 text-[10px] uppercase font-bold tracking-[0.05em] text-[#908fa0] text-right">Ratio</th>
|
|
524
|
-
<th class="px-4 py-3 text-[10px] uppercase font-bold tracking-[0.05em] text-[#908fa0]">Cost</th>
|
|
582
|
+
<th class="px-4 py-3 text-[10px] uppercase font-bold tracking-[0.05em] text-[#908fa0] text-left">Cost</th>
|
|
525
583
|
<th class="px-4 py-3 text-[10px] uppercase font-bold tracking-[0.05em] text-[#908fa0] text-right">Opus %</th>
|
|
526
|
-
<th class="px-
|
|
584
|
+
<th class="px-4 py-3 text-[10px] uppercase font-bold tracking-[0.05em] text-[#908fa0] text-left">Country</th>
|
|
527
585
|
</tr>
|
|
528
586
|
</thead>
|
|
529
587
|
<tbody id="leaderboard-body"></tbody>
|
|
@@ -784,13 +842,14 @@ ${cacheHealth.totalCacheBreaks > 0 ? `
|
|
|
784
842
|
// hover targets
|
|
785
843
|
d.forEach(function(x,j){
|
|
786
844
|
var px=PD.l+(d.length===1?cW/2:j*step),py=PD.t+cH-(x.cost/mx)*cH;
|
|
787
|
-
s+='<circle cx="'+px+'" cy="'+py+'" r="14" fill="transparent" data-d="'+x.date+'" data-c="'+x.cost+'" data-a="'+(x.isAnomaly?1:0)+'" class="hov" style="cursor:crosshair"/>';
|
|
845
|
+
s+='<circle cx="'+px+'" cy="'+py+'" r="14" fill="transparent" data-d="'+x.date+'" data-c="'+x.cost+'" data-a="'+(x.isAnomaly?1:0)+'" data-o="'+(x.out||0)+'" class="hov" style="cursor:crosshair"/>';
|
|
788
846
|
});
|
|
789
847
|
svg.innerHTML=s;
|
|
790
848
|
svg.querySelectorAll('.hov').forEach(function(el){
|
|
791
849
|
el.addEventListener('mouseenter',function(e){
|
|
792
850
|
ttd.textContent=e.target.dataset.d;
|
|
793
|
-
|
|
851
|
+
var ot=parseInt(e.target.dataset.o||0);
|
|
852
|
+
ttc.textContent=fc(parseFloat(e.target.dataset.c))+(ot>0?' — '+ft(ot)+' output':'');
|
|
794
853
|
tta.textContent=e.target.dataset.a==='1'?'ANOMALY':'';
|
|
795
854
|
tta.style.display=e.target.dataset.a==='1'?'block':'none';
|
|
796
855
|
tt.classList.add('on');
|
|
@@ -805,7 +864,7 @@ ${cacheHealth.totalCacheBreaks > 0 ? `
|
|
|
805
864
|
function setR(r){
|
|
806
865
|
var f=filt(r);chart(f);
|
|
807
866
|
var ci=document.getElementById('chart-info');
|
|
808
|
-
if(ci&&f.length){var t=f.reduce(function(s,x){return s+x.cost},0),a=f.filter(function(x){return x.cost>0}).length;ci.textContent=a+' days \u00b7 '+fc(t)}
|
|
867
|
+
if(ci&&f.length){var t=f.reduce(function(s,x){return s+x.cost},0),a=f.filter(function(x){return x.cost>0}).length,tok=f.reduce(function(s,x){return s+(x.out||0)},0);ci.textContent=a+' days \u00b7 '+fc(t)+' \u00b7 '+ft(tok)+' output'}
|
|
809
868
|
var rl=document.getElementById('range-lbl');if(rl)rl.textContent=RL[r]||'All time';
|
|
810
869
|
CARD.range=RL[r]||'All time';
|
|
811
870
|
var cr=document.getElementById('card-range');if(cr)cr.textContent=CARD.range;
|
|
@@ -1164,9 +1223,9 @@ ${cacheHealth.totalCacheBreaks > 0 ? `
|
|
|
1164
1223
|
html += '<td class="px-8 py-3 text-sm font-mono '+(isMe?'text-[#c0c1ff] font-bold':'text-[#908fa0]')+'">#'+(idx+1)+(isMe?' ← you':'')+'</td>';
|
|
1165
1224
|
html += '<td class="px-4 py-3 text-sm font-bold text-center" style="color:'+gradeColors[g]+'">'+g+'</td>';
|
|
1166
1225
|
html += '<td class="px-4 py-3 text-sm font-mono text-[#c7c4d7] text-right">'+(entry.ratio||'?')+':1</td>';
|
|
1167
|
-
html += '<td class="px-4 py-3 text-sm font-mono text-[#908fa0]">'+(entry.cost
|
|
1226
|
+
html += '<td class="px-4 py-3 text-sm font-mono text-[#908fa0]">'+(entry.cost?'$'+entry.cost:'?')+'</td>';
|
|
1168
1227
|
html += '<td class="px-4 py-3 text-sm font-mono text-[#c7c4d7] text-right">'+(entry.opus!=null?entry.opus:'?')+'%</td>';
|
|
1169
|
-
html += '<td class="px-
|
|
1228
|
+
html += '<td class="px-4 py-3 text-sm text-[#908fa0]">'+(isMe?'You':(entry.country||'?'))+'</td>';
|
|
1170
1229
|
html += '</tr>';
|
|
1171
1230
|
}
|
|
1172
1231
|
tbody.innerHTML = html;
|