@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.
@@ -15,7 +15,7 @@
15
15
  }
16
16
  </script>
17
17
  <!--BOARD_DATA-->
18
- <script>window.__BUILD_STAMP__ = "20260418-075248-39e5267";</script>
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;
@@ -1 +1 @@
1
- 20260418-075248-39e5267
1
+ 20260418-125234-6f01092
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beastmode-develeap/beastmode",
3
- "version": "0.1.109",
3
+ "version": "0.1.111",
4
4
  "description": "BeastMode Dark Factory — turn intent into verified software",
5
5
  "type": "module",
6
6
  "bin": {