@beastmode-develeap/beastmode 0.1.284 → 0.1.285
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/web/board.html +155 -310
- package/dist/web/build-commit.txt +1 -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__ = "20260516-
|
|
18
|
+
<script>window.__BUILD_STAMP__ = "20260516-174800-2440824";</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">
|
|
@@ -1259,11 +1259,6 @@ input[type="range"]::-webkit-slider-thumb {
|
|
|
1259
1259
|
.card-status-age.status-age-info { color: var(--accent); background: var(--accent-subtle); }
|
|
1260
1260
|
.card-status-age.status-age-warn { color: #fb923c; background: rgba(249, 115, 22, 0.15); }
|
|
1261
1261
|
.card-status-age.status-age-stale { color: #f87171; background: rgba(239, 68, 68, 0.15); }
|
|
1262
|
-
.card-badge-wrap {
|
|
1263
|
-
/* Flex child wrapper: blockified by parent flex, but sized to content.
|
|
1264
|
-
The inner .card-badge is NOT a direct flex item, so it keeps display:inline-block. */
|
|
1265
|
-
line-height: 0;
|
|
1266
|
-
}
|
|
1267
1262
|
.card-badge {
|
|
1268
1263
|
display: inline-block;
|
|
1269
1264
|
padding: 2px 8px;
|
|
@@ -5661,10 +5656,12 @@ function ItemDetailSidebar({ item, onClose, onStatusChange, selectedProject, all
|
|
|
5661
5656
|
const refreshCostSummary = useCallback(() => {
|
|
5662
5657
|
if (!item) return;
|
|
5663
5658
|
const proj = localStorage.getItem('beastmode-selected-project') || '';
|
|
5659
|
+
const sep = (q) => q ? '&' : '?';
|
|
5664
5660
|
const boardParam = (proj && proj !== 'all') ? '?board=' + encodeURIComponent(proj) : '';
|
|
5665
|
-
|
|
5661
|
+
const summaryQs = boardParam + sep(boardParam) + 'include_descendants=true';
|
|
5662
|
+
fetch('/api/items/' + item.id + '/costs/summary' + summaryQs)
|
|
5666
5663
|
.then(r => r.ok ? r.json() : null)
|
|
5667
|
-
.then(data => setCostSummary(data && data.record_count > 0 ? data : null))
|
|
5664
|
+
.then(data => setCostSummary(data && (data.record_count > 0 || (data.descendant_items && data.descendant_items.length > 0)) ? data : null))
|
|
5668
5665
|
.catch(() => setCostSummary(null));
|
|
5669
5666
|
if (isNonTerminal) {
|
|
5670
5667
|
fetch('/api/items/' + item.id + '/costs/live' + boardParam)
|
|
@@ -5726,10 +5723,12 @@ function ItemDetailSidebar({ item, onClose, onStatusChange, selectedProject, all
|
|
|
5726
5723
|
.finally(() => setLoadingAttachments(false));
|
|
5727
5724
|
setLoadingCost(true);
|
|
5728
5725
|
const proj = localStorage.getItem('beastmode-selected-project') || '';
|
|
5726
|
+
const sep = (q) => q ? '&' : '?';
|
|
5729
5727
|
const boardParam = (proj && proj !== 'all') ? '?board=' + encodeURIComponent(proj) : '';
|
|
5730
|
-
|
|
5728
|
+
const summaryQs = boardParam + sep(boardParam) + 'include_descendants=true';
|
|
5729
|
+
fetch('/api/items/' + item.id + '/costs/summary' + summaryQs)
|
|
5731
5730
|
.then(r => r.ok ? r.json() : null)
|
|
5732
|
-
.then(data => setCostSummary(data && data.record_count > 0 ? data : null))
|
|
5731
|
+
.then(data => setCostSummary(data && (data.record_count > 0 || (data.descendant_items && data.descendant_items.length > 0)) ? data : null))
|
|
5733
5732
|
.catch(() => setCostSummary(null))
|
|
5734
5733
|
.finally(() => setLoadingCost(false));
|
|
5735
5734
|
if (isNonTerminal) {
|
|
@@ -5988,6 +5987,45 @@ function ItemDetailSidebar({ item, onClose, onStatusChange, selectedProject, all
|
|
|
5988
5987
|
</table>
|
|
5989
5988
|
`)
|
|
5990
5989
|
}
|
|
5990
|
+
${costSummary.categories && Object.keys(costSummary.categories).length > 0 && html`
|
|
5991
|
+
<div data-testid="category-breakdown" style="margin-top:12px;">
|
|
5992
|
+
<div style="font-weight:600;margin-bottom:6px;color:var(--text-dim);font-size:12px;">By Category</div>
|
|
5993
|
+
<table class="cost-phase-table">
|
|
5994
|
+
<thead>
|
|
5995
|
+
<tr>
|
|
5996
|
+
<th>Category</th>
|
|
5997
|
+
<th>Cost</th>
|
|
5998
|
+
<th>%</th>
|
|
5999
|
+
</tr>
|
|
6000
|
+
</thead>
|
|
6001
|
+
<tbody>
|
|
6002
|
+
${(() => {
|
|
6003
|
+
const cats = costSummary.categories || {};
|
|
6004
|
+
const totalCat = Object.values(cats).reduce((s, v) => s + (v || 0), 0);
|
|
6005
|
+
return Object.entries(cats)
|
|
6006
|
+
.sort((a, b) => b[1] - a[1])
|
|
6007
|
+
.map(([cat, amount]) => html`
|
|
6008
|
+
<tr key=${cat}>
|
|
6009
|
+
<td class="cost-phase-name" data-testid=${'category-row-' + cat}>${cat}</td>
|
|
6010
|
+
<td class="cost-phase-value">${formatCost(amount) || '$0.00'}</td>
|
|
6011
|
+
<td class="cost-phase-value">${totalCat > 0 ? ((amount / totalCat) * 100).toFixed(0) + '%' : '—'}</td>
|
|
6012
|
+
</tr>
|
|
6013
|
+
`);
|
|
6014
|
+
})()}
|
|
6015
|
+
</tbody>
|
|
6016
|
+
</table>
|
|
6017
|
+
</div>
|
|
6018
|
+
`}
|
|
6019
|
+
${costSummary.descendant_items && costSummary.descendant_items.length > 0 && html`
|
|
6020
|
+
<div data-testid="bug-gc-descendants" style="margin-top:12px;padding:8px 10px;border-radius:4px;background:rgba(245,158,11,0.08);border-left:3px solid #f59e0b;">
|
|
6021
|
+
<div style="font-weight:600;font-size:12px;color:#f59e0b;margin-bottom:4px;">Including Bug GC costs</div>
|
|
6022
|
+
<div style="font-size:11px;color:var(--text-dim);">
|
|
6023
|
+
${costSummary.descendant_items.length} child task${costSummary.descendant_items.length === 1 ? '' : 's'}
|
|
6024
|
+
${costSummary.descendant_cost_usd != null ? html` · ${formatCost(costSummary.descendant_cost_usd)} from children` : ''}
|
|
6025
|
+
${costSummary.combined_cost_usd != null ? html` · combined ${formatCost(costSummary.combined_cost_usd)}` : ''}
|
|
6026
|
+
</div>
|
|
6027
|
+
</div>
|
|
6028
|
+
`}
|
|
5991
6029
|
</div>
|
|
5992
6030
|
`)}
|
|
5993
6031
|
${envTimeline && (envTimeline.entries.length > 0 || envTimeline.promotion_chain.length > 0) && html`
|
|
@@ -6324,7 +6362,7 @@ function PipelineView({
|
|
|
6324
6362
|
${(() => {
|
|
6325
6363
|
const cost = costsByItem && costsByItem[String(item.id)];
|
|
6326
6364
|
const label = cost ? formatCost(cost.total_cost_usd) : null;
|
|
6327
|
-
return label ? html`<span class="card-badge
|
|
6365
|
+
return label ? html`<span class="card-badge badge-cost" title=${'Total cost: ' + label}>${label}</span>` : null;
|
|
6328
6366
|
})()}
|
|
6329
6367
|
${(() => {
|
|
6330
6368
|
const env = item.extra && item.extra.current_env;
|
|
@@ -6741,7 +6779,8 @@ function BoardPage({ selectedProject }) {
|
|
|
6741
6779
|
// append ?board=<proj> here. Errors are swallowed: if the cost
|
|
6742
6780
|
// batch endpoint is unreachable, cards render without badges.
|
|
6743
6781
|
const fetchCosts = useCallback(() => {
|
|
6744
|
-
const
|
|
6782
|
+
const proj = localStorage.getItem('beastmode-selected-project') || '';
|
|
6783
|
+
const boardParam = (proj && proj !== 'all') ? '?board=' + encodeURIComponent(proj) : '';
|
|
6745
6784
|
fetch('/api/costs/by-items' + boardParam)
|
|
6746
6785
|
.then(r => r.ok ? r.json() : {})
|
|
6747
6786
|
.then(data => setCostsByItem(data || {}))
|
|
@@ -7535,7 +7574,7 @@ function BoardPage({ selectedProject }) {
|
|
|
7535
7574
|
${(() => {
|
|
7536
7575
|
const cost = costsByItem && costsByItem[String(item.id)];
|
|
7537
7576
|
const label = cost ? formatCost(cost.total_cost_usd) : null;
|
|
7538
|
-
return label ? html`<span class="card-badge
|
|
7577
|
+
return label ? html`<span class="card-badge badge-cost" title=${'Total cost: ' + label}>${label}</span>` : null;
|
|
7539
7578
|
})()}
|
|
7540
7579
|
${(() => {
|
|
7541
7580
|
const env = item.extra && item.extra.current_env;
|
|
@@ -8518,18 +8557,6 @@ function ProjectsPage({ selectedProject, onProjectChange }) {
|
|
|
8518
8557
|
const [onboardingTier, setOnboardingTier] = useState('standard');
|
|
8519
8558
|
const [showPicker, setShowPicker] = useState(false);
|
|
8520
8559
|
const [adding, setAdding] = useState(false);
|
|
8521
|
-
// Per-project analysis state (status pill + last-analyzed timestamp).
|
|
8522
|
-
// Keyed by project name; value is the /analyze/status JSON response.
|
|
8523
|
-
const [analysisStatuses, setAnalysisStatuses] = useState({});
|
|
8524
|
-
// Which project card is currently expanded (only one at a time).
|
|
8525
|
-
const [expandedProject, setExpandedProject] = useState(null);
|
|
8526
|
-
// Cached guide content keyed by project name; each entry holds optional l1/l2 strings.
|
|
8527
|
-
const [guideContent, setGuideContent] = useState({});
|
|
8528
|
-
// Per-project transient errors surfaced inside the card body.
|
|
8529
|
-
const [guideErrors, setGuideErrors] = useState({});
|
|
8530
|
-
// Per-project "show full guide" toggle (l2 visible).
|
|
8531
|
-
const [showFullGuide, setShowFullGuide] = useState({});
|
|
8532
|
-
const pollTimersRef = useRef({});
|
|
8533
8560
|
|
|
8534
8561
|
const fetchProjects = useCallback(() => {
|
|
8535
8562
|
api('GET', '/api/projects')
|
|
@@ -8540,117 +8567,6 @@ function ProjectsPage({ selectedProject, onProjectChange }) {
|
|
|
8540
8567
|
|
|
8541
8568
|
useEffect(() => { fetchProjects(); }, []);
|
|
8542
8569
|
|
|
8543
|
-
const fetchAnalysisStatus = useCallback(async (name) => {
|
|
8544
|
-
try {
|
|
8545
|
-
const status = await api('GET', '/api/projects/' + name + '/analyze/status');
|
|
8546
|
-
setAnalysisStatuses(prev => ({ ...prev, [name]: status }));
|
|
8547
|
-
return status;
|
|
8548
|
-
} catch (e) {
|
|
8549
|
-
setAnalysisStatuses(prev => ({ ...prev, [name]: { status: 'idle', error: e.message } }));
|
|
8550
|
-
return null;
|
|
8551
|
-
}
|
|
8552
|
-
}, []);
|
|
8553
|
-
|
|
8554
|
-
// Refresh per-project analysis status whenever the project list changes.
|
|
8555
|
-
useEffect(() => {
|
|
8556
|
-
projects.forEach(p => { fetchAnalysisStatus(p.name); });
|
|
8557
|
-
}, [projects, fetchAnalysisStatus]);
|
|
8558
|
-
|
|
8559
|
-
// Clean up any running poll intervals on unmount.
|
|
8560
|
-
useEffect(() => () => {
|
|
8561
|
-
Object.values(pollTimersRef.current).forEach(t => clearInterval(t));
|
|
8562
|
-
pollTimersRef.current = {};
|
|
8563
|
-
}, []);
|
|
8564
|
-
|
|
8565
|
-
const fetchGuide = useCallback(async (name, level) => {
|
|
8566
|
-
try {
|
|
8567
|
-
const resp = await api('GET', '/api/projects/' + name + '/codebase-guide?level=' + level);
|
|
8568
|
-
setGuideContent(prev => ({
|
|
8569
|
-
...prev,
|
|
8570
|
-
[name]: { ...(prev[name] || {}), [level]: resp.content || '' },
|
|
8571
|
-
}));
|
|
8572
|
-
setGuideErrors(prev => ({ ...prev, [name]: null }));
|
|
8573
|
-
return resp;
|
|
8574
|
-
} catch (e) {
|
|
8575
|
-
// 404 means no guide exists yet — treat as empty sentinel so the
|
|
8576
|
-
// render shows the status-based fallback instead of a raw error.
|
|
8577
|
-
const noGuide = e.message && (
|
|
8578
|
-
e.message.includes('No codebase guide') ||
|
|
8579
|
-
e.message.includes('Guide file not found') ||
|
|
8580
|
-
e.message === 'HTTP 404'
|
|
8581
|
-
);
|
|
8582
|
-
if (noGuide) {
|
|
8583
|
-
setGuideContent(prev => ({
|
|
8584
|
-
...prev,
|
|
8585
|
-
[name]: { ...(prev[name] || {}), [level]: '' },
|
|
8586
|
-
}));
|
|
8587
|
-
} else {
|
|
8588
|
-
setGuideErrors(prev => ({ ...prev, [name]: e.message }));
|
|
8589
|
-
}
|
|
8590
|
-
return null;
|
|
8591
|
-
}
|
|
8592
|
-
}, []);
|
|
8593
|
-
|
|
8594
|
-
const toggleExpand = useCallback(async (name) => {
|
|
8595
|
-
if (expandedProject === name) {
|
|
8596
|
-
setExpandedProject(null);
|
|
8597
|
-
return;
|
|
8598
|
-
}
|
|
8599
|
-
setExpandedProject(name);
|
|
8600
|
-
// Optimistically fetch L1 guide on expand regardless of analysis status.
|
|
8601
|
-
// Guide files exist even when a re-analysis is in progress (previous run's
|
|
8602
|
-
// output persists). fetchGuide silently treats 404 as empty so this is
|
|
8603
|
-
// safe to call unconditionally — it never shows a raw HTTP error.
|
|
8604
|
-
if (!(guideContent[name] && guideContent[name].l1)) {
|
|
8605
|
-
await fetchGuide(name, 'l1');
|
|
8606
|
-
}
|
|
8607
|
-
}, [expandedProject, guideContent, fetchGuide]);
|
|
8608
|
-
|
|
8609
|
-
const stopPolling = useCallback((name) => {
|
|
8610
|
-
if (pollTimersRef.current[name]) {
|
|
8611
|
-
clearInterval(pollTimersRef.current[name]);
|
|
8612
|
-
delete pollTimersRef.current[name];
|
|
8613
|
-
}
|
|
8614
|
-
}, []);
|
|
8615
|
-
|
|
8616
|
-
const startPolling = useCallback((name) => {
|
|
8617
|
-
stopPolling(name);
|
|
8618
|
-
pollTimersRef.current[name] = setInterval(async () => {
|
|
8619
|
-
const st = await fetchAnalysisStatus(name);
|
|
8620
|
-
if (!st) return;
|
|
8621
|
-
if (st.status === 'complete') {
|
|
8622
|
-
stopPolling(name);
|
|
8623
|
-
// Drop stale L1/L2 caches so the next expand re-fetches them.
|
|
8624
|
-
setGuideContent(prev => { const n = { ...prev }; delete n[name]; return n; });
|
|
8625
|
-
setShowFullGuide(prev => ({ ...prev, [name]: false }));
|
|
8626
|
-
if (expandedProject === name) {
|
|
8627
|
-
await fetchGuide(name, 'l1');
|
|
8628
|
-
}
|
|
8629
|
-
} else if (st.status === 'failed' || st.status === 'idle') {
|
|
8630
|
-
stopPolling(name);
|
|
8631
|
-
}
|
|
8632
|
-
}, 5000);
|
|
8633
|
-
}, [fetchAnalysisStatus, fetchGuide, stopPolling, expandedProject]);
|
|
8634
|
-
|
|
8635
|
-
const reanalyze = useCallback(async (name) => {
|
|
8636
|
-
setGuideErrors(prev => ({ ...prev, [name]: null }));
|
|
8637
|
-
setAnalysisStatuses(prev => ({ ...prev, [name]: { status: 'running' } }));
|
|
8638
|
-
try {
|
|
8639
|
-
await api('POST', '/api/projects/' + name + '/analyze?force=true', {});
|
|
8640
|
-
startPolling(name);
|
|
8641
|
-
} catch (e) {
|
|
8642
|
-
setAnalysisStatuses(prev => ({ ...prev, [name]: { status: 'failed', error: e.message } }));
|
|
8643
|
-
}
|
|
8644
|
-
}, [startPolling]);
|
|
8645
|
-
|
|
8646
|
-
const toggleFullGuide = useCallback(async (name) => {
|
|
8647
|
-
const next = !showFullGuide[name];
|
|
8648
|
-
setShowFullGuide(prev => ({ ...prev, [name]: next }));
|
|
8649
|
-
if (next && !(guideContent[name] && guideContent[name].l2)) {
|
|
8650
|
-
await fetchGuide(name, 'l2');
|
|
8651
|
-
}
|
|
8652
|
-
}, [showFullGuide, guideContent, fetchGuide]);
|
|
8653
|
-
|
|
8654
8570
|
const addProject = async () => {
|
|
8655
8571
|
const isGithub = addMode === 'github';
|
|
8656
8572
|
if (isGithub && !githubUrl.trim()) return;
|
|
@@ -8667,38 +8583,22 @@ function ProjectsPage({ selectedProject, onProjectChange }) {
|
|
|
8667
8583
|
setNewPath('');
|
|
8668
8584
|
setGithubUrl('');
|
|
8669
8585
|
setCloneTarget('');
|
|
8670
|
-
|
|
8671
|
-
// analyze call. The analyze API returns immediately (async job) so the
|
|
8672
|
-
// delay is negligible, but showing 'Project added successfully' first
|
|
8673
|
-
// causes the verifier (and real users) to read the wrong message if
|
|
8674
|
-
// they look before the API responds. Only show the final accurate message.
|
|
8675
|
-
if (isGithub) {
|
|
8676
|
-
setSuccess(`Cloned and added: ${registeredPath}`);
|
|
8677
|
-
} else if (onboardingTier !== 'standard' && onboardingTier !== 'full') {
|
|
8678
|
-
setSuccess('Project added successfully');
|
|
8679
|
-
}
|
|
8586
|
+
setSuccess(isGithub ? `Cloned and added: ${registeredPath}` : 'Project added successfully');
|
|
8680
8587
|
|
|
8681
8588
|
if (onboardingTier === 'standard' || onboardingTier === 'full') {
|
|
8589
|
+
setSuccess('Project added — running codebase analysis...');
|
|
8682
8590
|
try {
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
} else {
|
|
8687
|
-
setSuccess('Project added — analyzing codebase…');
|
|
8688
|
-
// Begin polling so the operator sees the status pill flip to
|
|
8689
|
-
// "Analyzed" on the new project card in real time. Card may
|
|
8690
|
-
// render slightly later (after fetchProjects), but startPolling
|
|
8691
|
-
// is idempotent and the interval keeps refreshing status.
|
|
8692
|
-
startPolling(projName);
|
|
8693
|
-
}
|
|
8694
|
-
} catch {
|
|
8695
|
-
setSuccess('Project added (analysis failed — can retry later)');
|
|
8696
|
-
}
|
|
8591
|
+
await api('POST', '/api/projects/' + projName + '/analyze', {});
|
|
8592
|
+
setSuccess('Project added and analyzed');
|
|
8593
|
+
} catch { setSuccess('Project added (analysis failed — can retry later)'); }
|
|
8697
8594
|
}
|
|
8698
8595
|
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
|
|
8596
|
+
if (onboardingTier === 'full') {
|
|
8597
|
+
setTimeout(() => {
|
|
8598
|
+
if (onProjectChange) onProjectChange(projName);
|
|
8599
|
+
navigate('#/new-session');
|
|
8600
|
+
}, 1000);
|
|
8601
|
+
}
|
|
8702
8602
|
|
|
8703
8603
|
setTimeout(() => setSuccess(null), 5000);
|
|
8704
8604
|
fetchProjects();
|
|
@@ -8792,128 +8692,22 @@ function ProjectsPage({ selectedProject, onProjectChange }) {
|
|
|
8792
8692
|
icon="projects"
|
|
8793
8693
|
title="No projects yet"
|
|
8794
8694
|
description="Add a project path to start building." />`
|
|
8795
|
-
: projects.map(proj =>
|
|
8796
|
-
|
|
8797
|
-
|
|
8798
|
-
|
|
8799
|
-
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
|
|
8803
|
-
: pillState === 'running' ? '\u23f3 Analyzing\u2026'
|
|
8804
|
-
: pillState === 'failed' ? '\u2717 Failed'
|
|
8805
|
-
: '\u2014 Not analyzed';
|
|
8806
|
-
const pillColor = pillState === 'complete' ? '#22c55e'
|
|
8807
|
-
: pillState === 'running' ? '#f59e0b'
|
|
8808
|
-
: pillState === 'failed' ? '#ef4444'
|
|
8809
|
-
: '#94a3b8';
|
|
8810
|
-
const tsLabel = pillState === 'complete' && st.analyzed_at
|
|
8811
|
-
? 'Analyzed ' + timeAgo(st.analyzed_at)
|
|
8812
|
-
: pillState === 'failed' ? 'Analysis failed'
|
|
8813
|
-
: pillState === 'running' ? 'Analyzing\u2026'
|
|
8814
|
-
: 'Not analyzed';
|
|
8815
|
-
const guide = guideContent[proj.name] || {};
|
|
8816
|
-
const guideErr = guideErrors[proj.name];
|
|
8817
|
-
const isBusy = pillState === 'running';
|
|
8818
|
-
return html`
|
|
8819
|
-
<div class="ext-item" key=${proj.name} style="flex-wrap:wrap;flex-direction:column;align-items:stretch;">
|
|
8820
|
-
<div style="display:flex;align-items:center;gap:12px;width:100%;cursor:pointer;"
|
|
8821
|
-
onClick=${() => toggleExpand(proj.name)}>
|
|
8822
|
-
<div style="flex:1;min-width:0;">
|
|
8823
|
-
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
|
|
8824
|
-
<div class="ext-name">${proj.name}</div>
|
|
8825
|
-
<span style="display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;
|
|
8826
|
-
background:${pillColor};color:#fff;white-space:nowrap;"
|
|
8827
|
-
title=${st.error || ''}
|
|
8828
|
-
data-testid="project-status-pill"
|
|
8829
|
-
data-state=${pillState}>${pillLabel}</span>
|
|
8830
|
-
${proj.stack && (proj.stack.detected || proj.stack.framework)
|
|
8831
|
-
? html`<span style="display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;
|
|
8832
|
-
background:var(--bg-subtle);color:var(--text-secondary);">
|
|
8833
|
-
${proj.stack.detected || proj.stack.framework}
|
|
8834
|
-
</span>`
|
|
8835
|
-
: null}
|
|
8836
|
-
</div>
|
|
8837
|
-
<div class="ext-meta" style="margin-top:4px;">
|
|
8838
|
-
${proj.repo ? proj.repo + ' \u2014 ' : ''}
|
|
8839
|
-
${proj.path || ''}
|
|
8840
|
-
</div>
|
|
8841
|
-
<div class="ext-meta" style="margin-top:2px;font-size:11px;color:var(--text-secondary);"
|
|
8842
|
-
data-testid="project-analyzed-timestamp">${tsLabel}</div>
|
|
8843
|
-
</div>
|
|
8844
|
-
<div class="ext-actions" style="display:flex;gap:6px;flex-wrap:wrap;"
|
|
8845
|
-
onClick=${(e) => e.stopPropagation()}>
|
|
8846
|
-
<button class="btn btn-sm btn-secondary"
|
|
8847
|
-
onClick=${() => reanalyze(proj.name)}
|
|
8848
|
-
disabled=${isBusy}
|
|
8849
|
-
data-testid="project-reanalyze-btn">
|
|
8850
|
-
${isBusy ? 'Analyzing\u2026' : 'Re-analyze'}
|
|
8851
|
-
</button>
|
|
8852
|
-
<button class="btn btn-sm btn-danger" onClick=${() => removeProject(proj.name)}>Remove</button>
|
|
8853
|
-
<button class="btn btn-sm btn-secondary"
|
|
8854
|
-
onClick=${() => toggleExpand(proj.name)}
|
|
8855
|
-
data-testid="project-expand-btn">${expanded ? '\u25b4' : '\u25be'}</button>
|
|
8695
|
+
: projects.map(proj => html`
|
|
8696
|
+
<div class="ext-item" key=${proj.name} style="flex-wrap:wrap;">
|
|
8697
|
+
<div class="ext-info">
|
|
8698
|
+
<div class="ext-name">${proj.name}</div>
|
|
8699
|
+
<div class="ext-meta">
|
|
8700
|
+
${proj.repo ? proj.repo + ' \u2014 ' : ''}
|
|
8701
|
+
${proj.stack ? proj.stack.detected || proj.stack.framework || '' : ''}
|
|
8702
|
+
${proj.path ? ' \u2014 ' + proj.path : ''}
|
|
8856
8703
|
</div>
|
|
8857
8704
|
</div>
|
|
8858
|
-
|
|
8859
|
-
<
|
|
8860
|
-
|
|
8861
|
-
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
|
|
8862
|
-
<strong style="font-size:13px;">Codebase summary</strong>
|
|
8863
|
-
${guide.l1 ? html`
|
|
8864
|
-
<button class="btn btn-sm btn-secondary"
|
|
8865
|
-
onClick=${() => toggleFullGuide(proj.name)}
|
|
8866
|
-
data-testid="project-show-full-guide">
|
|
8867
|
-
${showFullGuide[proj.name] ? 'Hide full guide' : 'Show full guide'}
|
|
8868
|
-
</button>
|
|
8869
|
-
` : null}
|
|
8870
|
-
</div>
|
|
8871
|
-
${guideErr ? html`<div class="error-msg" style="margin-bottom:8px;">${guideErr}</div>` : null}
|
|
8872
|
-
${guide.l1 === undefined
|
|
8873
|
-
? html`<div style="color:var(--text-secondary);font-size:12px;">Loading guide\u2026</div>`
|
|
8874
|
-
: guide.l1
|
|
8875
|
-
? html`
|
|
8876
|
-
${isBusy ? html`<div style="font-size:11px;color:var(--text-secondary);margin-bottom:6px;">Re-analysis in progress\u2026</div>` : null}
|
|
8877
|
-
<pre style="white-space:pre-wrap;font-family:inherit;font-size:13px;line-height:1.5;
|
|
8878
|
-
background:var(--bg-subtle);padding:10px;border-radius:6px;margin:0;"
|
|
8879
|
-
data-testid="codebase-guide-l1">${guide.l1}</pre>
|
|
8880
|
-
${showFullGuide[proj.name]
|
|
8881
|
-
? (guide.l2 === undefined
|
|
8882
|
-
? html`<div style="margin-top:8px;color:var(--text-secondary);font-size:12px;">Loading full guide\u2026</div>`
|
|
8883
|
-
: html`
|
|
8884
|
-
<pre style="white-space:pre-wrap;font-family:inherit;font-size:12px;line-height:1.5;
|
|
8885
|
-
background:var(--bg-subtle);padding:10px;border-radius:6px;margin-top:8px;"
|
|
8886
|
-
data-testid="codebase-guide-l2">${guide.l2}</pre>
|
|
8887
|
-
`)
|
|
8888
|
-
: null}
|
|
8889
|
-
`
|
|
8890
|
-
: pillState === 'running'
|
|
8891
|
-
? html`<div style="display:flex;align-items:center;gap:8px;color:var(--text-secondary);">
|
|
8892
|
-
<span class="spinner" style="display:inline-block;width:12px;height:12px;border:2px solid var(--border);
|
|
8893
|
-
border-top-color:var(--accent);border-radius:50%;
|
|
8894
|
-
animation:spin 0.8s linear infinite;"></span>
|
|
8895
|
-
Analyzing codebase\u2026
|
|
8896
|
-
</div>`
|
|
8897
|
-
: pillState === 'failed'
|
|
8898
|
-
? html`<div>
|
|
8899
|
-
<div class="error-msg" style="margin-bottom:8px;">${st.error || 'Analysis failed'}</div>
|
|
8900
|
-
<button class="btn btn-sm btn-primary"
|
|
8901
|
-
onClick=${() => reanalyze(proj.name)}
|
|
8902
|
-
data-testid="project-retry-btn">Retry</button>
|
|
8903
|
-
</div>`
|
|
8904
|
-
: html`<div style="color:var(--text-secondary);">
|
|
8905
|
-
Not analyzed yet \u2014
|
|
8906
|
-
<button class="btn btn-sm btn-primary"
|
|
8907
|
-
style="margin-left:6px;"
|
|
8908
|
-
onClick=${() => reanalyze(proj.name)}
|
|
8909
|
-
data-testid="project-run-analysis-btn">Run analysis</button>
|
|
8910
|
-
</div>`}
|
|
8911
|
-
</div>
|
|
8912
|
-
` : null}
|
|
8705
|
+
<div class="ext-actions">
|
|
8706
|
+
<button class="btn btn-sm btn-danger" onClick=${() => removeProject(proj.name)}>Remove</button>
|
|
8707
|
+
</div>
|
|
8913
8708
|
<${EnvironmentPanel} projectName=${proj.name} />
|
|
8914
8709
|
</div>
|
|
8915
|
-
|
|
8916
|
-
})}
|
|
8710
|
+
`)}
|
|
8917
8711
|
</div>
|
|
8918
8712
|
${showPicker && html`<${DirectoryPicker} initialPath=${newPath || null} onClose=${() => setShowPicker(false)} onSelect=${(p) => { setNewPath(p); setShowPicker(false); }} />`}
|
|
8919
8713
|
</div>
|
|
@@ -11038,8 +10832,8 @@ function CostsPage({ selectedProject }) {
|
|
|
11038
10832
|
async function fetchCosts() {
|
|
11039
10833
|
try {
|
|
11040
10834
|
const costQs = (selectedProject && selectedProject !== 'all')
|
|
11041
|
-
? '?board=' + encodeURIComponent(selectedProject)
|
|
11042
|
-
: '';
|
|
10835
|
+
? '?board=' + encodeURIComponent(selectedProject) + '&include_descendants=true'
|
|
10836
|
+
: '?include_descendants=true';
|
|
11043
10837
|
const inFlightQs = (selectedProject && selectedProject !== 'all')
|
|
11044
10838
|
? '?board=' + encodeURIComponent(selectedProject) + '&threshold=5'
|
|
11045
10839
|
: '?threshold=5';
|
|
@@ -11132,13 +10926,33 @@ function CostsPage({ selectedProject }) {
|
|
|
11132
10926
|
|
|
11133
10927
|
const mergedRows = Object.entries(costsByItems).map(([itemId, costs]) => {
|
|
11134
10928
|
const item = boardItems.find(i => String(i.id) === String(itemId));
|
|
10929
|
+
const categories = costs.categories || {};
|
|
10930
|
+
const ownCost = costs.total_cost_usd || 0;
|
|
10931
|
+
const descCost = costs.descendant_cost_usd || 0;
|
|
10932
|
+
const combinedCost = costs.combined_cost_usd != null
|
|
10933
|
+
? costs.combined_cost_usd
|
|
10934
|
+
: (ownCost + descCost);
|
|
10935
|
+
let dominantCategory = '';
|
|
10936
|
+
let dominantShare = 0;
|
|
10937
|
+
const catTotal = Object.values(categories).reduce((s, v) => s + (v || 0), 0);
|
|
10938
|
+
if (catTotal > 0) {
|
|
10939
|
+
const sortedCats = Object.entries(categories).sort((a, b) => b[1] - a[1]);
|
|
10940
|
+
const top = sortedCats[0];
|
|
10941
|
+
dominantShare = top[1] / catTotal;
|
|
10942
|
+
dominantCategory = dominantShare >= 0.6 ? top[0] : 'mixed';
|
|
10943
|
+
}
|
|
11135
10944
|
return {
|
|
11136
10945
|
id: itemId,
|
|
11137
10946
|
name: item ? item.name : 'Item #' + itemId,
|
|
11138
10947
|
status: item ? item.status : 'Unknown',
|
|
11139
10948
|
totalTokens: (costs.total_input_tokens || 0) + (costs.total_output_tokens || 0),
|
|
11140
|
-
costUsd:
|
|
10949
|
+
costUsd: ownCost,
|
|
10950
|
+
descendantCost: descCost,
|
|
10951
|
+
combinedCost: combinedCost,
|
|
10952
|
+
categories: categories,
|
|
10953
|
+
dominantCategory: dominantCategory,
|
|
11141
10954
|
records: costs.record_count || 0,
|
|
10955
|
+
parentTask: item ? (item.parent_task || '') : '',
|
|
11142
10956
|
byIteration: byIterationMap[itemId] || [],
|
|
11143
10957
|
};
|
|
11144
10958
|
});
|
|
@@ -11155,15 +10969,37 @@ function CostsPage({ selectedProject }) {
|
|
|
11155
10969
|
const avgCostPerTask = tasksTracked > 0 ? totalSpend / tasksTracked : 0;
|
|
11156
10970
|
const maxPhaseCost = phaseData.length > 0 ? Math.max(...phaseData.map(p => p.cost)) : 0;
|
|
11157
10971
|
|
|
10972
|
+
const categoryTotals = {};
|
|
10973
|
+
mergedRows.forEach(r => {
|
|
10974
|
+
for (const [cat, amount] of Object.entries(r.categories || {})) {
|
|
10975
|
+
categoryTotals[cat] = (categoryTotals[cat] || 0) + (amount || 0);
|
|
10976
|
+
}
|
|
10977
|
+
});
|
|
10978
|
+
const categoryPills = Object.entries(categoryTotals)
|
|
10979
|
+
.sort((a, b) => b[1] - a[1])
|
|
10980
|
+
.map(([cat, amount]) => ({ cat, amount }));
|
|
10981
|
+
|
|
10982
|
+
const categoryColors = {
|
|
10983
|
+
spec: '#3b82f6',
|
|
10984
|
+
build: '#10b981',
|
|
10985
|
+
verify: '#f59e0b',
|
|
10986
|
+
review: '#8b5cf6',
|
|
10987
|
+
epic: '#ec4899',
|
|
10988
|
+
heal: '#ef4444',
|
|
10989
|
+
infra: '#06b6d4',
|
|
10990
|
+
mixed: 'var(--text-muted)',
|
|
10991
|
+
};
|
|
10992
|
+
|
|
11158
10993
|
const sortedRows = [...mergedRows].sort((a, b) => {
|
|
11159
10994
|
let cmp = 0;
|
|
11160
10995
|
switch (sortCol) {
|
|
11161
|
-
case 'id':
|
|
11162
|
-
case 'name':
|
|
11163
|
-
case 'status':
|
|
11164
|
-
case 'tokens':
|
|
11165
|
-
case 'cost':
|
|
11166
|
-
case 'records':
|
|
10996
|
+
case 'id': cmp = Number(a.id) - Number(b.id); break;
|
|
10997
|
+
case 'name': cmp = a.name.localeCompare(b.name); break;
|
|
10998
|
+
case 'status': cmp = a.status.localeCompare(b.status); break;
|
|
10999
|
+
case 'tokens': cmp = a.totalTokens - b.totalTokens; break;
|
|
11000
|
+
case 'cost': cmp = a.combinedCost - b.combinedCost; break;
|
|
11001
|
+
case 'records': cmp = a.records - b.records; break;
|
|
11002
|
+
case 'category': cmp = (a.dominantCategory || '').localeCompare(b.dominantCategory || ''); break;
|
|
11167
11003
|
}
|
|
11168
11004
|
return sortDir === 'asc' ? cmp : -cmp;
|
|
11169
11005
|
});
|
|
@@ -11200,6 +11036,19 @@ function CostsPage({ selectedProject }) {
|
|
|
11200
11036
|
</div>
|
|
11201
11037
|
</div>
|
|
11202
11038
|
|
|
11039
|
+
${categoryPills.length > 0 && html`
|
|
11040
|
+
<div class="cost-category-row" data-testid="cost-category-pills" style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:24px;">
|
|
11041
|
+
${categoryPills.map(({ cat, amount }) => html`
|
|
11042
|
+
<div key=${cat}
|
|
11043
|
+
data-testid=${'category-pill-' + cat}
|
|
11044
|
+
style=${'display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border-radius:999px;background:' + (categoryColors[cat] || '#64748b') + '22;border:1px solid ' + (categoryColors[cat] || '#64748b') + '55;color:' + (categoryColors[cat] || '#64748b') + ';font-size:12px;font-weight:600;'}>
|
|
11045
|
+
<span style="text-transform:lowercase;">${cat}</span>
|
|
11046
|
+
<span style="font-family:var(--font-mono);">$${amount.toFixed(2)}</span>
|
|
11047
|
+
</div>
|
|
11048
|
+
`)}
|
|
11049
|
+
</div>
|
|
11050
|
+
`}
|
|
11051
|
+
|
|
11203
11052
|
${inFlightItems.length > 0 && html`
|
|
11204
11053
|
<div class="card" data-testid="currently-running-card" style="margin-bottom:24px;border-left:4px solid #f59e0b;">
|
|
11205
11054
|
<div class="card-header">
|
|
@@ -11249,7 +11098,8 @@ function CostsPage({ selectedProject }) {
|
|
|
11249
11098
|
<th class="costs-th costs-th-name" onClick=${() => handleSort('name')}>Task${sortIndicator('name')}</th>
|
|
11250
11099
|
<th class="costs-th costs-th-status" onClick=${() => handleSort('status')}>Status${sortIndicator('status')}</th>
|
|
11251
11100
|
<th class="costs-th costs-th-tokens" onClick=${() => handleSort('tokens')}>Tokens${sortIndicator('tokens')}</th>
|
|
11252
|
-
<th class="costs-th costs-th-cost" onClick=${() => handleSort('cost')}>Cost (
|
|
11101
|
+
<th class="costs-th costs-th-cost" onClick=${() => handleSort('cost')}>Total Cost (incl. children)${sortIndicator('cost')}</th>
|
|
11102
|
+
<th class="costs-th costs-th-category" onClick=${() => handleSort('category')}>Category${sortIndicator('category')}</th>
|
|
11253
11103
|
<th class="costs-th costs-th-records" onClick=${() => handleSort('records')}>Records${sortIndicator('records')}</th>
|
|
11254
11104
|
</tr>
|
|
11255
11105
|
</thead>
|
|
@@ -11261,16 +11111,28 @@ function CostsPage({ selectedProject }) {
|
|
|
11261
11111
|
e.stopPropagation();
|
|
11262
11112
|
setExpandedRows(prev => ({ ...prev, [row.id]: !prev[row.id] }));
|
|
11263
11113
|
};
|
|
11114
|
+
const hasChildCost = row.descendantCost > 0;
|
|
11115
|
+
const isChild = !!row.parentTask;
|
|
11116
|
+
const catColor = categoryColors[row.dominantCategory] || 'var(--text-muted)';
|
|
11264
11117
|
return html`
|
|
11265
11118
|
<${Fragment} key=${row.id}>
|
|
11266
11119
|
<tr class="costs-tr" onClick=${() => navigate('#/board?item=' + row.id)}>
|
|
11267
11120
|
<td class="costs-td costs-td-mono">
|
|
11268
11121
|
${hasMultiIter ? html`<span class="costs-expand-toggle" onClick=${toggle} style="cursor:pointer;display:inline-block;width:14px;">${isExpanded ? '▼' : '▶'}</span> ` : ''}#${row.id}
|
|
11122
|
+
${isChild ? html`<span data-testid=${'parent-link-' + row.id} title=${'Child of #' + row.parentTask} style="margin-left:4px;color:var(--text-muted);font-size:11px;">↑#${row.parentTask}</span>` : ''}
|
|
11269
11123
|
</td>
|
|
11270
11124
|
<td class="costs-td costs-td-name" title=${row.name}>${row.name}</td>
|
|
11271
11125
|
<td class="costs-td"><span class=${'badge ' + statusBadgeClass(row.status)}>${row.status}</span></td>
|
|
11272
11126
|
<td class="costs-td costs-td-mono">${formatTokens(row.totalTokens)}</td>
|
|
11273
|
-
<td class="costs-td costs-td-mono"
|
|
11127
|
+
<td class="costs-td costs-td-mono" data-testid=${'combined-cost-' + row.id}>
|
|
11128
|
+
$${row.combinedCost.toFixed(2)}
|
|
11129
|
+
${hasChildCost ? html`<div data-testid=${'descendant-indicator-' + row.id} style="font-size:10px;color:var(--text-muted);font-weight:400;">+$${row.descendantCost.toFixed(2)} from children</div>` : ''}
|
|
11130
|
+
</td>
|
|
11131
|
+
<td class="costs-td">
|
|
11132
|
+
${row.dominantCategory
|
|
11133
|
+
? html`<span data-testid=${'category-cell-' + row.id} style=${'display:inline-block;padding:2px 8px;border-radius:4px;background:' + catColor + '22;color:' + catColor + ';font-size:11px;font-weight:600;text-transform:lowercase;'}>${row.dominantCategory}</span>`
|
|
11134
|
+
: html`<span style="color:var(--text-muted);font-size:11px;">—</span>`}
|
|
11135
|
+
</td>
|
|
11274
11136
|
<td class="costs-td costs-td-mono">${row.records}</td>
|
|
11275
11137
|
</tr>
|
|
11276
11138
|
${hasMultiIter && isExpanded ? row.byIteration.map(it => html`
|
|
@@ -11280,6 +11142,7 @@ function CostsPage({ selectedProject }) {
|
|
|
11280
11142
|
<td class="costs-td"></td>
|
|
11281
11143
|
<td class="costs-td costs-td-mono">${formatTokens((it.input_tokens || 0) + (it.output_tokens || 0))}</td>
|
|
11282
11144
|
<td class="costs-td costs-td-mono">${it.cost_usd != null ? '$' + it.cost_usd.toFixed(2) : '—'}</td>
|
|
11145
|
+
<td class="costs-td"></td>
|
|
11283
11146
|
<td class="costs-td costs-td-mono">${it.record_count || 0}</td>
|
|
11284
11147
|
</tr>
|
|
11285
11148
|
`) : ''}
|
|
@@ -11753,24 +11616,6 @@ function App() {
|
|
|
11753
11616
|
setSelectedProjectRaw(value);
|
|
11754
11617
|
}, []);
|
|
11755
11618
|
|
|
11756
|
-
// Auto-select the first registered project when localStorage has no prior
|
|
11757
|
-
// choice. Without this, items and costs both route to the default board
|
|
11758
|
-
// (board.db) which doesn't match project board data — no badges render in
|
|
11759
|
-
// a fresh Playwright or incognito session. Multi-project factories default
|
|
11760
|
-
// to the first registered project; users can override via the sidebar.
|
|
11761
|
-
useEffect(() => {
|
|
11762
|
-
if (localStorage.getItem('beastmode-selected-project')) return;
|
|
11763
|
-
fetch('/api/projects')
|
|
11764
|
-
.then(r => r.ok ? r.json() : null)
|
|
11765
|
-
.then(data => {
|
|
11766
|
-
const projects = data && data.projects;
|
|
11767
|
-
if (Array.isArray(projects) && projects.length > 0) {
|
|
11768
|
-
setSelectedProject(projects[0].name);
|
|
11769
|
-
}
|
|
11770
|
-
})
|
|
11771
|
-
.catch(() => {});
|
|
11772
|
-
}, []);
|
|
11773
|
-
|
|
11774
11619
|
// If ?view=pipeline or ?view=board is in the URL but no hash route is
|
|
11775
11620
|
// set, the router would default to Dashboard. Redirect to #/board so
|
|
11776
11621
|
// the board page renders (and BoardPage can read the ?view= param).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
24408242cd5d9e462deb310bcd8725e1f24cd414
|
package/dist/web/build-stamp.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
20260516-
|
|
1
|
+
20260516-174800-2440824
|