@beastmode-develeap/beastmode 0.1.109 → 0.1.111
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/dist/index.js +101 -6
- package/dist/index.js.map +1 -1
- package/dist/web/board.html +406 -1
- package/dist/web/build-stamp.txt +1 -1
- package/package.json +1 -1
package/dist/web/board.html
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
}
|
|
16
16
|
</script>
|
|
17
17
|
<!--BOARD_DATA-->
|
|
18
|
-
<script>window.__BUILD_STAMP__ = "20260418-
|
|
18
|
+
<script>window.__BUILD_STAMP__ = "20260418-125234-6f01092";</script>
|
|
19
19
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
20
20
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
21
21
|
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
@@ -2149,6 +2149,115 @@ input[type="range"]::-webkit-slider-thumb {
|
|
|
2149
2149
|
gap: 8px;
|
|
2150
2150
|
}
|
|
2151
2151
|
|
|
2152
|
+
/* ================================================================
|
|
2153
|
+
COSTS PAGE
|
|
2154
|
+
================================================================ */
|
|
2155
|
+
|
|
2156
|
+
.costs-table-wrapper {
|
|
2157
|
+
overflow-x: auto;
|
|
2158
|
+
-webkit-overflow-scrolling: touch;
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
.costs-table {
|
|
2162
|
+
width: 100%;
|
|
2163
|
+
border-collapse: collapse;
|
|
2164
|
+
font-family: var(--font-sans);
|
|
2165
|
+
font-size: 13px;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
.costs-th {
|
|
2169
|
+
text-align: left;
|
|
2170
|
+
padding: 10px 12px;
|
|
2171
|
+
font-size: 11px;
|
|
2172
|
+
font-weight: 600;
|
|
2173
|
+
text-transform: uppercase;
|
|
2174
|
+
letter-spacing: 0.5px;
|
|
2175
|
+
color: var(--text-muted);
|
|
2176
|
+
border-bottom: 1px solid var(--border);
|
|
2177
|
+
cursor: pointer;
|
|
2178
|
+
user-select: none;
|
|
2179
|
+
white-space: nowrap;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
.costs-th:hover {
|
|
2183
|
+
color: var(--accent);
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
.costs-tr {
|
|
2187
|
+
cursor: pointer;
|
|
2188
|
+
transition: background 0.15s ease;
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
.costs-tr:hover {
|
|
2192
|
+
background: var(--bg-card-hover);
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
.costs-td {
|
|
2196
|
+
padding: 10px 12px;
|
|
2197
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
2198
|
+
color: var(--text);
|
|
2199
|
+
vertical-align: middle;
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
.costs-td-mono {
|
|
2203
|
+
font-family: var(--font-mono);
|
|
2204
|
+
font-size: 13px;
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
.costs-td-name {
|
|
2208
|
+
font-weight: 500;
|
|
2209
|
+
max-width: 300px;
|
|
2210
|
+
overflow: hidden;
|
|
2211
|
+
text-overflow: ellipsis;
|
|
2212
|
+
white-space: nowrap;
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
.costs-phase-chart {
|
|
2216
|
+
padding: 16px 0;
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
.costs-phase-row {
|
|
2220
|
+
display: flex;
|
|
2221
|
+
align-items: center;
|
|
2222
|
+
gap: 12px;
|
|
2223
|
+
padding: 6px 16px;
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
.costs-phase-label {
|
|
2227
|
+
width: 100px;
|
|
2228
|
+
font-size: 13px;
|
|
2229
|
+
font-weight: 500;
|
|
2230
|
+
color: var(--text-secondary);
|
|
2231
|
+
text-transform: capitalize;
|
|
2232
|
+
flex-shrink: 0;
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
.costs-phase-bar-bg {
|
|
2236
|
+
flex: 1;
|
|
2237
|
+
height: 24px;
|
|
2238
|
+
background: var(--accent-subtle);
|
|
2239
|
+
border-radius: var(--radius-xs);
|
|
2240
|
+
overflow: hidden;
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
.costs-phase-bar-fill {
|
|
2244
|
+
height: 100%;
|
|
2245
|
+
background: var(--accent);
|
|
2246
|
+
border-radius: var(--radius-xs);
|
|
2247
|
+
transition: width 0.4s ease;
|
|
2248
|
+
min-width: 2px;
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
.costs-phase-amount {
|
|
2252
|
+
width: 80px;
|
|
2253
|
+
text-align: right;
|
|
2254
|
+
font-family: var(--font-mono);
|
|
2255
|
+
font-size: 13px;
|
|
2256
|
+
font-weight: 500;
|
|
2257
|
+
color: var(--text);
|
|
2258
|
+
flex-shrink: 0;
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2152
2261
|
/* ================================================================
|
|
2153
2262
|
RESPONSIVE
|
|
2154
2263
|
================================================================ */
|
|
@@ -2176,6 +2285,10 @@ input[type="range"]::-webkit-slider-thumb {
|
|
|
2176
2285
|
/* Detail sidebar: full-width on tablets */
|
|
2177
2286
|
.detail-sidebar { width: 100vw !important; max-width: 100vw; }
|
|
2178
2287
|
.detail-resize-handle { display: none; }
|
|
2288
|
+
/* Costs page */
|
|
2289
|
+
.costs-phase-label { width: 70px; font-size: 12px; }
|
|
2290
|
+
.costs-phase-amount { width: 60px; font-size: 12px; }
|
|
2291
|
+
.costs-td-name { max-width: 150px; }
|
|
2179
2292
|
}
|
|
2180
2293
|
|
|
2181
2294
|
@media (max-width: 600px) {
|
|
@@ -2192,6 +2305,9 @@ input[type="range"]::-webkit-slider-thumb {
|
|
|
2192
2305
|
.detail-sidebar { padding: 16px; }
|
|
2193
2306
|
.detail-header h3 { font-size: 14px; }
|
|
2194
2307
|
.update-body { font-size: 12px; }
|
|
2308
|
+
/* Costs page */
|
|
2309
|
+
.costs-table { font-size: 12px; }
|
|
2310
|
+
.costs-th, .costs-td { padding: 8px 6px; }
|
|
2195
2311
|
}
|
|
2196
2312
|
|
|
2197
2313
|
/* ================================================================
|
|
@@ -2516,6 +2632,7 @@ function Icon({ name, size = 18, className = '' }) {
|
|
|
2516
2632
|
lightbulb: html`<path d="M8 1a5 5 0 00-3 9c.5.6.8 1.2 1 1.8V13h4v-1.2c.2-.6.5-1.2 1-1.8A5 5 0 008 1z"/><line x1="6" y1="13.5" x2="10" y2="13.5"/><line x1="6.5" y1="15" x2="9.5" y2="15"/>`,
|
|
2517
2633
|
help: html`<circle cx="8" cy="8" r="6.5"/><path d="M6 6.5a2 2 0 013.7 1c0 1.5-1.7 1.5-1.7 3"/><circle cx="8" cy="12" r="0.5" fill="currentColor" stroke="none"/>`,
|
|
2518
2634
|
filter: html`<polygon points="1.5,2 14.5,2 9.5,8.5 9.5,13 6.5,14.5 6.5,8.5"/>`,
|
|
2635
|
+
costs: html`<circle cx="8" cy="8" r="6.5"/><path d="M8 3.5v1m0 7v1M6 10.5c0 .8.9 1.5 2 1.5s2-.7 2-1.5S9.1 9 8 9s-2-.7-2-1.5S6.9 6 8 6s2 .7 2 1.5"/>`,
|
|
2519
2636
|
};
|
|
2520
2637
|
|
|
2521
2638
|
return html`
|
|
@@ -2606,6 +2723,7 @@ function Sidebar({ currentHash, factoryName, theme, onToggleTheme, selectedProje
|
|
|
2606
2723
|
...(selectedProject !== 'all' ? [{ path: '#/board', icon: 'board', label: 'Board' }] : []),
|
|
2607
2724
|
...(selectedProject !== 'all' ? [{ path: '#/strategy', icon: 'strategy', label: 'Strategy' }] : []),
|
|
2608
2725
|
{ path: '#/runs', icon: 'runs', label: 'Runs' },
|
|
2726
|
+
{ path: '#/costs', icon: 'costs', label: 'Costs' },
|
|
2609
2727
|
{ path: '#/learnings', icon: 'lightbulb', label: 'Learnings' },
|
|
2610
2728
|
{ path: '#/projects', icon: 'projects', label: 'Projects' },
|
|
2611
2729
|
{ path: '#/extensions', icon: 'extensions', label: 'Extensions' },
|
|
@@ -5585,6 +5703,79 @@ function HelpPage() {
|
|
|
5585
5703
|
// Settings Page
|
|
5586
5704
|
// ================================================================
|
|
5587
5705
|
|
|
5706
|
+
// Gap 15: opt-in anonymous telemetry status surface. Reads
|
|
5707
|
+
// /api/telemetry/status (proxied to the board, which reads the daemon's
|
|
5708
|
+
// last heartbeat). Always renders — shows "Disabled" with a red dot
|
|
5709
|
+
// when telemetry is off, full metrics when on. See docs/telemetry.md.
|
|
5710
|
+
function TelemetrySettings() {
|
|
5711
|
+
const [status, setStatus] = useState(null);
|
|
5712
|
+
const [loading, setLoading] = useState(true);
|
|
5713
|
+
const [loadError, setLoadError] = useState(null);
|
|
5714
|
+
|
|
5715
|
+
useEffect(() => {
|
|
5716
|
+
api('GET', '/api/telemetry/status')
|
|
5717
|
+
.then(data => { setStatus(data); setLoading(false); })
|
|
5718
|
+
.catch(e => { setLoadError(e && e.message ? e.message : String(e)); setLoading(false); });
|
|
5719
|
+
}, []);
|
|
5720
|
+
|
|
5721
|
+
const enabled = !!(status && status.enabled);
|
|
5722
|
+
const dotColor = enabled ? 'var(--success)' : 'var(--danger)';
|
|
5723
|
+
const eventTypes = (status && Array.isArray(status.collect_events)) ? status.collect_events : [];
|
|
5724
|
+
|
|
5725
|
+
return html`
|
|
5726
|
+
<div class="settings-section">
|
|
5727
|
+
<h3>Telemetry</h3>
|
|
5728
|
+
${loading ? html`<p style="font-size:13px;color:var(--text-muted);">Loading telemetry status...</p>` : null}
|
|
5729
|
+
${loadError ? html`<p style="font-size:13px;color:var(--danger);">Telemetry status unavailable: ${loadError}</p>` : null}
|
|
5730
|
+
${!loading && !loadError ? html`
|
|
5731
|
+
<div class="setting-row">
|
|
5732
|
+
<div>
|
|
5733
|
+
<div class="setting-label">Status</div>
|
|
5734
|
+
<div class="setting-desc">Anonymous pipeline metrics (no code, no PII)</div>
|
|
5735
|
+
</div>
|
|
5736
|
+
<div class="setting-control">
|
|
5737
|
+
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${dotColor};" />
|
|
5738
|
+
<span class="mono" style="font-weight:600;color:${enabled ? 'var(--success)' : 'var(--danger)'};">${enabled ? 'Enabled' : 'Disabled'}</span>
|
|
5739
|
+
</div>
|
|
5740
|
+
</div>
|
|
5741
|
+
${enabled ? html`
|
|
5742
|
+
<div class="setting-row">
|
|
5743
|
+
<div><div class="setting-label">Anonymous ID</div></div>
|
|
5744
|
+
<div class="setting-control"><span class="mono">${(status && status.anonymous_id_prefix) || ''}</span></div>
|
|
5745
|
+
</div>
|
|
5746
|
+
<div class="setting-row">
|
|
5747
|
+
<div>
|
|
5748
|
+
<div class="setting-label">Sentry</div>
|
|
5749
|
+
<div class="setting-desc">Error reporting SDK</div>
|
|
5750
|
+
</div>
|
|
5751
|
+
<div class="setting-control"><span class="mono">${status && status.sentry_configured ? 'Configured' : 'Not configured'}</span></div>
|
|
5752
|
+
</div>
|
|
5753
|
+
<div class="setting-row">
|
|
5754
|
+
<div><div class="setting-label">Events buffered</div></div>
|
|
5755
|
+
<div class="setting-control"><span class="mono">${status && status.events_buffered != null ? status.events_buffered : 0}</span></div>
|
|
5756
|
+
</div>
|
|
5757
|
+
<div class="setting-row">
|
|
5758
|
+
<div><div class="setting-label">Events collected</div></div>
|
|
5759
|
+
<div class="setting-control"><span class="mono">${status && status.events_total != null ? status.events_total : 0}</span></div>
|
|
5760
|
+
</div>
|
|
5761
|
+
<div class="setting-row">
|
|
5762
|
+
<div><div class="setting-label">Last flush</div></div>
|
|
5763
|
+
<div class="setting-control"><span class="mono">${(status && status.last_flush_at) || 'Never'}</span></div>
|
|
5764
|
+
</div>
|
|
5765
|
+
<div class="setting-row">
|
|
5766
|
+
<div><div class="setting-label">Event types</div></div>
|
|
5767
|
+
<div class="setting-control"><span class="mono" style="text-align:right;">${eventTypes.length > 0 ? eventTypes.join(', ') : '—'}</span></div>
|
|
5768
|
+
</div>
|
|
5769
|
+
` : null}
|
|
5770
|
+
` : null}
|
|
5771
|
+
<p style="font-size:12px;color:var(--text-muted);margin-top:12px;">
|
|
5772
|
+
Configure via <span class="mono">config/beastmode.daemon.json</span> (<span class="mono">telemetry</span> section) or
|
|
5773
|
+
the <span class="mono">BEASTMODE_TELEMETRY_ENABLED</span> env var. See <span class="mono">docs/telemetry.md</span>.
|
|
5774
|
+
</p>
|
|
5775
|
+
</div>
|
|
5776
|
+
`;
|
|
5777
|
+
}
|
|
5778
|
+
|
|
5588
5779
|
function SettingsPage() {
|
|
5589
5780
|
const [config, setConfig] = useState(null);
|
|
5590
5781
|
const [loading, setLoading] = useState(true);
|
|
@@ -6031,6 +6222,9 @@ function SettingsPage() {
|
|
|
6031
6222
|
</div>
|
|
6032
6223
|
</div>
|
|
6033
6224
|
</div>
|
|
6225
|
+
|
|
6226
|
+
<!-- Telemetry Section (Gap 15) -->
|
|
6227
|
+
<${TelemetrySettings} />
|
|
6034
6228
|
</div>
|
|
6035
6229
|
</div>
|
|
6036
6230
|
`;
|
|
@@ -6819,6 +7013,216 @@ function StrategyPage({ selectedProject }) {
|
|
|
6819
7013
|
`;
|
|
6820
7014
|
}
|
|
6821
7015
|
|
|
7016
|
+
// ================================================================
|
|
7017
|
+
// Costs Page
|
|
7018
|
+
// ================================================================
|
|
7019
|
+
|
|
7020
|
+
function CostsPage({ selectedProject }) {
|
|
7021
|
+
const [costsByItems, setCostsByItems] = useState({});
|
|
7022
|
+
const [boardItems, setBoardItems] = useState([]);
|
|
7023
|
+
const [phaseData, setPhaseData] = useState([]);
|
|
7024
|
+
const [loading, setLoading] = useState(true);
|
|
7025
|
+
const [error, setError] = useState(null);
|
|
7026
|
+
const [sortCol, setSortCol] = useState('cost');
|
|
7027
|
+
const [sortDir, setSortDir] = useState('desc');
|
|
7028
|
+
|
|
7029
|
+
useEffect(() => {
|
|
7030
|
+
let cancelled = false;
|
|
7031
|
+
|
|
7032
|
+
async function fetchCosts() {
|
|
7033
|
+
try {
|
|
7034
|
+
const costQs = (selectedProject && selectedProject !== 'all')
|
|
7035
|
+
? '?board=' + encodeURIComponent(selectedProject)
|
|
7036
|
+
: '';
|
|
7037
|
+
|
|
7038
|
+
const [costsData, boardData] = await Promise.all([
|
|
7039
|
+
api('GET', '/api/costs/by-items' + costQs),
|
|
7040
|
+
api('GET', '/api/board/items'),
|
|
7041
|
+
]);
|
|
7042
|
+
|
|
7043
|
+
if (cancelled) return;
|
|
7044
|
+
setCostsByItems(costsData || {});
|
|
7045
|
+
setBoardItems((boardData && boardData.items) || []);
|
|
7046
|
+
|
|
7047
|
+
const itemIds = Object.keys(costsData || {});
|
|
7048
|
+
if (itemIds.length > 0) {
|
|
7049
|
+
const phaseMap = {};
|
|
7050
|
+
const summaryResults = await Promise.allSettled(
|
|
7051
|
+
itemIds.map(id => {
|
|
7052
|
+
const qs = (selectedProject && selectedProject !== 'all')
|
|
7053
|
+
? '?board=' + encodeURIComponent(selectedProject)
|
|
7054
|
+
: '';
|
|
7055
|
+
return api('GET', '/api/items/' + id + '/costs/summary' + qs);
|
|
7056
|
+
})
|
|
7057
|
+
);
|
|
7058
|
+
summaryResults.forEach(result => {
|
|
7059
|
+
if (result.status === 'fulfilled' && result.value && result.value.phases) {
|
|
7060
|
+
for (const [phaseName, phaseInfo] of Object.entries(result.value.phases)) {
|
|
7061
|
+
phaseMap[phaseName] = (phaseMap[phaseName] || 0) + (phaseInfo.cost_usd || 0);
|
|
7062
|
+
}
|
|
7063
|
+
}
|
|
7064
|
+
});
|
|
7065
|
+
const sorted = Object.entries(phaseMap)
|
|
7066
|
+
.map(([phase, cost]) => ({ phase, cost }))
|
|
7067
|
+
.sort((a, b) => b.cost - a.cost);
|
|
7068
|
+
if (!cancelled) setPhaseData(sorted);
|
|
7069
|
+
}
|
|
7070
|
+
} catch (e) {
|
|
7071
|
+
if (!cancelled) setError(e.message || 'Failed to load cost data');
|
|
7072
|
+
} finally {
|
|
7073
|
+
if (!cancelled) setLoading(false);
|
|
7074
|
+
}
|
|
7075
|
+
}
|
|
7076
|
+
|
|
7077
|
+
setLoading(true);
|
|
7078
|
+
setError(null);
|
|
7079
|
+
fetchCosts();
|
|
7080
|
+
const interval = setInterval(fetchCosts, 30000);
|
|
7081
|
+
return () => { cancelled = true; clearInterval(interval); };
|
|
7082
|
+
}, [selectedProject]);
|
|
7083
|
+
|
|
7084
|
+
function formatTokens(n) {
|
|
7085
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
|
|
7086
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
|
|
7087
|
+
return String(n);
|
|
7088
|
+
}
|
|
7089
|
+
|
|
7090
|
+
function handleSort(col) {
|
|
7091
|
+
if (sortCol === col) {
|
|
7092
|
+
setSortDir(sortDir === 'asc' ? 'desc' : 'asc');
|
|
7093
|
+
} else {
|
|
7094
|
+
setSortCol(col);
|
|
7095
|
+
setSortDir(col === 'name' || col === 'status' ? 'asc' : 'desc');
|
|
7096
|
+
}
|
|
7097
|
+
}
|
|
7098
|
+
|
|
7099
|
+
if (loading) return html`<${SkeletonLoader} type="stats" />`;
|
|
7100
|
+
if (error) return html`<div class="page-content"><div class="error-msg">${error}</div></div>`;
|
|
7101
|
+
|
|
7102
|
+
const mergedRows = Object.entries(costsByItems).map(([itemId, costs]) => {
|
|
7103
|
+
const item = boardItems.find(i => String(i.id) === String(itemId));
|
|
7104
|
+
return {
|
|
7105
|
+
id: itemId,
|
|
7106
|
+
name: item ? item.name : 'Item #' + itemId,
|
|
7107
|
+
status: item ? item.status : 'Unknown',
|
|
7108
|
+
totalTokens: (costs.total_input_tokens || 0) + (costs.total_output_tokens || 0),
|
|
7109
|
+
costUsd: costs.total_cost_usd || 0,
|
|
7110
|
+
records: costs.record_count || 0,
|
|
7111
|
+
};
|
|
7112
|
+
});
|
|
7113
|
+
|
|
7114
|
+
if (mergedRows.length === 0) {
|
|
7115
|
+
return html`<div class="page-content">
|
|
7116
|
+
<${EmptyState} icon="costs" title="No Cost Data" description="Cost data will appear here as tasks are processed by the pipeline." />
|
|
7117
|
+
</div>`;
|
|
7118
|
+
}
|
|
7119
|
+
|
|
7120
|
+
const totalSpend = mergedRows.reduce((sum, r) => sum + r.costUsd, 0);
|
|
7121
|
+
const totalTokens = mergedRows.reduce((sum, r) => sum + r.totalTokens, 0);
|
|
7122
|
+
const tasksTracked = mergedRows.length;
|
|
7123
|
+
const avgCostPerTask = tasksTracked > 0 ? totalSpend / tasksTracked : 0;
|
|
7124
|
+
const maxPhaseCost = phaseData.length > 0 ? Math.max(...phaseData.map(p => p.cost)) : 0;
|
|
7125
|
+
|
|
7126
|
+
const sortedRows = [...mergedRows].sort((a, b) => {
|
|
7127
|
+
let cmp = 0;
|
|
7128
|
+
switch (sortCol) {
|
|
7129
|
+
case 'id': cmp = Number(a.id) - Number(b.id); break;
|
|
7130
|
+
case 'name': cmp = a.name.localeCompare(b.name); break;
|
|
7131
|
+
case 'status': cmp = a.status.localeCompare(b.status); break;
|
|
7132
|
+
case 'tokens': cmp = a.totalTokens - b.totalTokens; break;
|
|
7133
|
+
case 'cost': cmp = a.costUsd - b.costUsd; break;
|
|
7134
|
+
case 'records': cmp = a.records - b.records; break;
|
|
7135
|
+
}
|
|
7136
|
+
return sortDir === 'asc' ? cmp : -cmp;
|
|
7137
|
+
});
|
|
7138
|
+
|
|
7139
|
+
const sortIndicator = (col) => sortCol === col ? (sortDir === 'asc' ? ' \u25B2' : ' \u25BC') : '';
|
|
7140
|
+
|
|
7141
|
+
return html`
|
|
7142
|
+
<div class="page-content">
|
|
7143
|
+
<div class="page-header">
|
|
7144
|
+
<h2>Factory Costs</h2>
|
|
7145
|
+
<p>Token usage and cost analytics across all tasks</p>
|
|
7146
|
+
</div>
|
|
7147
|
+
|
|
7148
|
+
<div class="stat-grid mb-24">
|
|
7149
|
+
<div class="stat-card">
|
|
7150
|
+
<div class="stat-value">$${totalSpend.toFixed(2)}</div>
|
|
7151
|
+
<div class="stat-label">Total Spend</div>
|
|
7152
|
+
<div class="stat-dot"></div>
|
|
7153
|
+
</div>
|
|
7154
|
+
<div class="stat-card">
|
|
7155
|
+
<div class="stat-value">${formatTokens(totalTokens)}</div>
|
|
7156
|
+
<div class="stat-label">Total Tokens</div>
|
|
7157
|
+
<div class="stat-dot"></div>
|
|
7158
|
+
</div>
|
|
7159
|
+
<div class="stat-card">
|
|
7160
|
+
<div class="stat-value">${tasksTracked}</div>
|
|
7161
|
+
<div class="stat-label">Tasks Tracked</div>
|
|
7162
|
+
<div class="stat-dot"></div>
|
|
7163
|
+
</div>
|
|
7164
|
+
<div class="stat-card">
|
|
7165
|
+
<div class="stat-value">$${avgCostPerTask.toFixed(2)}</div>
|
|
7166
|
+
<div class="stat-label">Avg Cost / Task</div>
|
|
7167
|
+
<div class="stat-dot"></div>
|
|
7168
|
+
</div>
|
|
7169
|
+
</div>
|
|
7170
|
+
|
|
7171
|
+
<div class="card">
|
|
7172
|
+
<div class="card-header">
|
|
7173
|
+
<h3>Cost by Task</h3>
|
|
7174
|
+
<span class="badge badge-accent">${sortedRows.length} tasks</span>
|
|
7175
|
+
</div>
|
|
7176
|
+
<div class="costs-table-wrapper">
|
|
7177
|
+
<table class="costs-table">
|
|
7178
|
+
<thead>
|
|
7179
|
+
<tr>
|
|
7180
|
+
<th class="costs-th" onClick=${() => handleSort('id')}>ID${sortIndicator('id')}</th>
|
|
7181
|
+
<th class="costs-th" onClick=${() => handleSort('name')}>Task${sortIndicator('name')}</th>
|
|
7182
|
+
<th class="costs-th" onClick=${() => handleSort('status')}>Status${sortIndicator('status')}</th>
|
|
7183
|
+
<th class="costs-th" onClick=${() => handleSort('tokens')}>Tokens${sortIndicator('tokens')}</th>
|
|
7184
|
+
<th class="costs-th" onClick=${() => handleSort('cost')}>Cost (USD)${sortIndicator('cost')}</th>
|
|
7185
|
+
<th class="costs-th" onClick=${() => handleSort('records')}>Records${sortIndicator('records')}</th>
|
|
7186
|
+
</tr>
|
|
7187
|
+
</thead>
|
|
7188
|
+
<tbody>
|
|
7189
|
+
${sortedRows.map(row => html`
|
|
7190
|
+
<tr key=${row.id} class="costs-tr" onClick=${() => navigate('#/board?item=' + row.id)}>
|
|
7191
|
+
<td class="costs-td costs-td-mono">#${row.id}</td>
|
|
7192
|
+
<td class="costs-td costs-td-name">${row.name}</td>
|
|
7193
|
+
<td class="costs-td"><span class=${'badge ' + statusBadgeClass(row.status)}>${row.status}</span></td>
|
|
7194
|
+
<td class="costs-td costs-td-mono">${formatTokens(row.totalTokens)}</td>
|
|
7195
|
+
<td class="costs-td costs-td-mono">$${row.costUsd.toFixed(2)}</td>
|
|
7196
|
+
<td class="costs-td costs-td-mono">${row.records}</td>
|
|
7197
|
+
</tr>
|
|
7198
|
+
`)}
|
|
7199
|
+
</tbody>
|
|
7200
|
+
</table>
|
|
7201
|
+
</div>
|
|
7202
|
+
</div>
|
|
7203
|
+
|
|
7204
|
+
${phaseData.length > 0 && html`
|
|
7205
|
+
<div class="card" style="margin-top:24px;">
|
|
7206
|
+
<div class="card-header">
|
|
7207
|
+
<h3>Cost by Phase</h3>
|
|
7208
|
+
</div>
|
|
7209
|
+
<div class="costs-phase-chart">
|
|
7210
|
+
${phaseData.map(p => html`
|
|
7211
|
+
<div key=${p.phase} class="costs-phase-row">
|
|
7212
|
+
<div class="costs-phase-label">${p.phase}</div>
|
|
7213
|
+
<div class="costs-phase-bar-bg">
|
|
7214
|
+
<div class="costs-phase-bar-fill" style=${'width:' + (maxPhaseCost > 0 ? (p.cost / maxPhaseCost * 100) : 0) + '%'}></div>
|
|
7215
|
+
</div>
|
|
7216
|
+
<div class="costs-phase-amount">$${p.cost.toFixed(2)}</div>
|
|
7217
|
+
</div>
|
|
7218
|
+
`)}
|
|
7219
|
+
</div>
|
|
7220
|
+
</div>
|
|
7221
|
+
`}
|
|
7222
|
+
</div>
|
|
7223
|
+
`;
|
|
7224
|
+
}
|
|
7225
|
+
|
|
6822
7226
|
// ================================================================
|
|
6823
7227
|
// Learnings Page
|
|
6824
7228
|
// ================================================================
|
|
@@ -7076,6 +7480,7 @@ function App() {
|
|
|
7076
7480
|
case '#/board': page = html`<${BoardPage} selectedProject=${selectedProject} />`; break;
|
|
7077
7481
|
case '#/strategy': page = html`<${StrategyPage} selectedProject=${selectedProject} />`; break;
|
|
7078
7482
|
case '#/runs': page = html`<${RunsPage} selectedProject=${selectedProject} />`; break;
|
|
7483
|
+
case '#/costs': page = html`<${CostsPage} selectedProject=${selectedProject} />`; break;
|
|
7079
7484
|
case '#/learnings': page = html`<${LearningsPage} selectedProject=${selectedProject} />`; break;
|
|
7080
7485
|
case '#/projects': page = html`<${ProjectsPage} selectedProject=${selectedProject} onProjectChange=${setSelectedProject} />`; break;
|
|
7081
7486
|
case '#/extensions': page = html`<${ExtensionsPage} selectedProject=${selectedProject} />`; break;
|
package/dist/web/build-stamp.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
20260418-
|
|
1
|
+
20260418-125234-6f01092
|