@beastmode-develeap/beastmode 0.1.119 → 0.1.121
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 +13 -3
- package/dist/index.js.map +1 -1
- package/dist/web/board.html +296 -2
- 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-190841-6b44897";</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">
|
|
@@ -1448,6 +1448,117 @@ input[type="range"]::-webkit-slider-thumb {
|
|
|
1448
1448
|
}
|
|
1449
1449
|
}
|
|
1450
1450
|
|
|
1451
|
+
/* Pipeline health panel */
|
|
1452
|
+
.pipeline-health {
|
|
1453
|
+
background: var(--surface-elevated);
|
|
1454
|
+
border: 1px solid var(--border);
|
|
1455
|
+
border-radius: var(--radius-sm);
|
|
1456
|
+
padding: 12px 16px;
|
|
1457
|
+
margin-bottom: 16px;
|
|
1458
|
+
}
|
|
1459
|
+
.pipeline-health-header {
|
|
1460
|
+
display: flex;
|
|
1461
|
+
justify-content: space-between;
|
|
1462
|
+
align-items: center;
|
|
1463
|
+
margin-bottom: 8px;
|
|
1464
|
+
cursor: pointer;
|
|
1465
|
+
user-select: none;
|
|
1466
|
+
}
|
|
1467
|
+
.pipeline-health-title {
|
|
1468
|
+
display: flex;
|
|
1469
|
+
align-items: center;
|
|
1470
|
+
gap: 6px;
|
|
1471
|
+
font-size: 13px;
|
|
1472
|
+
font-weight: 600;
|
|
1473
|
+
color: var(--text);
|
|
1474
|
+
}
|
|
1475
|
+
.pipeline-health-chevron {
|
|
1476
|
+
font-size: 10px;
|
|
1477
|
+
color: var(--text-muted);
|
|
1478
|
+
transition: transform 0.15s ease;
|
|
1479
|
+
}
|
|
1480
|
+
.pipeline-health-global-text {
|
|
1481
|
+
font-size: 12px;
|
|
1482
|
+
color: var(--text-secondary);
|
|
1483
|
+
font-family: var(--font-mono);
|
|
1484
|
+
}
|
|
1485
|
+
.pipeline-health-bar-global {
|
|
1486
|
+
height: 6px;
|
|
1487
|
+
background: var(--bg-input);
|
|
1488
|
+
border-radius: 3px;
|
|
1489
|
+
overflow: hidden;
|
|
1490
|
+
margin-bottom: 12px;
|
|
1491
|
+
}
|
|
1492
|
+
.pipeline-health-bar-fill {
|
|
1493
|
+
height: 100%;
|
|
1494
|
+
border-radius: 3px;
|
|
1495
|
+
transition: width 0.3s ease;
|
|
1496
|
+
background: var(--success);
|
|
1497
|
+
}
|
|
1498
|
+
.pipeline-health-swimlanes {
|
|
1499
|
+
display: grid;
|
|
1500
|
+
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
1501
|
+
gap: 8px;
|
|
1502
|
+
margin-bottom: 8px;
|
|
1503
|
+
}
|
|
1504
|
+
.swimlane-mini {
|
|
1505
|
+
display: flex;
|
|
1506
|
+
flex-direction: column;
|
|
1507
|
+
gap: 4px;
|
|
1508
|
+
}
|
|
1509
|
+
.swimlane-mini-label {
|
|
1510
|
+
display: flex;
|
|
1511
|
+
justify-content: space-between;
|
|
1512
|
+
align-items: center;
|
|
1513
|
+
font-size: 11px;
|
|
1514
|
+
color: var(--text-secondary);
|
|
1515
|
+
}
|
|
1516
|
+
.swimlane-mini-count {
|
|
1517
|
+
font-family: var(--font-mono);
|
|
1518
|
+
font-size: 11px;
|
|
1519
|
+
font-weight: 500;
|
|
1520
|
+
}
|
|
1521
|
+
.swimlane-mini-bar {
|
|
1522
|
+
height: 4px;
|
|
1523
|
+
background: var(--bg-input);
|
|
1524
|
+
border-radius: 2px;
|
|
1525
|
+
overflow: hidden;
|
|
1526
|
+
}
|
|
1527
|
+
.swimlane-mini-fill {
|
|
1528
|
+
height: 100%;
|
|
1529
|
+
border-radius: 2px;
|
|
1530
|
+
transition: width 0.3s ease;
|
|
1531
|
+
}
|
|
1532
|
+
.pipeline-health-warnings {
|
|
1533
|
+
display: flex;
|
|
1534
|
+
flex-wrap: wrap;
|
|
1535
|
+
gap: 6px;
|
|
1536
|
+
margin-top: 4px;
|
|
1537
|
+
}
|
|
1538
|
+
.bottleneck-badge {
|
|
1539
|
+
display: inline-flex;
|
|
1540
|
+
align-items: center;
|
|
1541
|
+
gap: 4px;
|
|
1542
|
+
padding: 2px 8px;
|
|
1543
|
+
border-radius: var(--radius-xs);
|
|
1544
|
+
font-size: 11px;
|
|
1545
|
+
font-weight: 500;
|
|
1546
|
+
background: var(--warning-subtle);
|
|
1547
|
+
color: var(--warning);
|
|
1548
|
+
border: 1px solid rgba(251, 191, 36, 0.2);
|
|
1549
|
+
}
|
|
1550
|
+
.pipeline-health.collapsed .pipeline-health-bar-global,
|
|
1551
|
+
.pipeline-health.collapsed .pipeline-health-swimlanes,
|
|
1552
|
+
.pipeline-health.collapsed .pipeline-health-warnings {
|
|
1553
|
+
display: none;
|
|
1554
|
+
}
|
|
1555
|
+
@media (max-width: 768px) {
|
|
1556
|
+
.pipeline-health-swimlanes { grid-template-columns: 1fr 1fr; }
|
|
1557
|
+
}
|
|
1558
|
+
@media (max-width: 480px) {
|
|
1559
|
+
.pipeline-health-swimlanes { grid-template-columns: 1fr; }
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1451
1562
|
/* Board stats bar */
|
|
1452
1563
|
.board-stats-bar {
|
|
1453
1564
|
display: flex;
|
|
@@ -2615,7 +2726,7 @@ fetch('/api/pipeline-config')
|
|
|
2615
2726
|
// CDN Imports
|
|
2616
2727
|
// ================================================================
|
|
2617
2728
|
import { h, render } from 'https://unpkg.com/preact@10.25.4/dist/preact.module.js';
|
|
2618
|
-
import { useState, useEffect, useCallback, useRef } from 'https://unpkg.com/preact@10.25.4/hooks/dist/hooks.module.js';
|
|
2729
|
+
import { useState, useEffect, useCallback, useRef, useMemo } from 'https://unpkg.com/preact@10.25.4/hooks/dist/hooks.module.js';
|
|
2619
2730
|
import htm from 'https://unpkg.com/htm@3.1.1/dist/htm.module.js';
|
|
2620
2731
|
|
|
2621
2732
|
const html = htm.bind(h);
|
|
@@ -3288,6 +3399,91 @@ window.overlayKey = overlayKey;
|
|
|
3288
3399
|
window.overlayTooltip = overlayTooltip;
|
|
3289
3400
|
window.getEffectivePipelineColumn = getEffectivePipelineColumn;
|
|
3290
3401
|
|
|
3402
|
+
function swimlaneColor(colorKey) {
|
|
3403
|
+
const map = {
|
|
3404
|
+
'accent': 'var(--accent)', 'var(--accent)': 'var(--accent)',
|
|
3405
|
+
'purple': '#a78bfa', 'var(--purple)': '#a78bfa',
|
|
3406
|
+
'orange': '#f97316', 'var(--orange)': '#f97316',
|
|
3407
|
+
'muted': 'var(--text-muted)', 'var(--text-muted)': 'var(--text-muted)',
|
|
3408
|
+
};
|
|
3409
|
+
return map[colorKey] || 'var(--accent)';
|
|
3410
|
+
}
|
|
3411
|
+
window.swimlaneColor = swimlaneColor;
|
|
3412
|
+
|
|
3413
|
+
function computePipelineStats(items) {
|
|
3414
|
+
const config = window.PIPELINE_CONFIG || {};
|
|
3415
|
+
const swimlanes = config.swimlanes || [];
|
|
3416
|
+
const typeStages = config.type_stages || {};
|
|
3417
|
+
|
|
3418
|
+
const totalItems = items.length;
|
|
3419
|
+
const doneItems = items.filter(i => i.status === 'Done').length;
|
|
3420
|
+
const stuckItems = items.filter(i => i.status === 'Stuck').length;
|
|
3421
|
+
const activeItems = items.filter(i =>
|
|
3422
|
+
i.status && i.status !== 'New' && i.status !== 'Done' && i.status !== 'Stuck' && i.status !== ''
|
|
3423
|
+
).length;
|
|
3424
|
+
const globalProgress = totalItems > 0 ? doneItems / totalItems : 0;
|
|
3425
|
+
|
|
3426
|
+
const swimlaneStats = swimlanes.map(lane => {
|
|
3427
|
+
const taskTypes = lane.taskTypes || lane.task_types || [];
|
|
3428
|
+
const laneItems = items.filter(i => taskTypes.includes(i.task_type || 'code'));
|
|
3429
|
+
const total = laneItems.length;
|
|
3430
|
+
const done = laneItems.filter(i => i.status === 'Done').length;
|
|
3431
|
+
const stuck = laneItems.filter(i => i.status === 'Stuck').length;
|
|
3432
|
+
const active = laneItems.filter(i =>
|
|
3433
|
+
i.status && i.status !== 'New' && i.status !== 'Done' && i.status !== 'Stuck' && i.status !== ''
|
|
3434
|
+
).length;
|
|
3435
|
+
|
|
3436
|
+
const repType = taskTypes[0] || 'code';
|
|
3437
|
+
const stages = typeStages[repType] || typeStages['code'] || [];
|
|
3438
|
+
const stageCounts = {};
|
|
3439
|
+
stages.forEach(s => { stageCounts[s] = 0; });
|
|
3440
|
+
laneItems.forEach(item => {
|
|
3441
|
+
const col = getEffectivePipelineColumn(item);
|
|
3442
|
+
if (col in stageCounts) stageCounts[col]++;
|
|
3443
|
+
else stageCounts[col] = (stageCounts[col] || 0) + 1;
|
|
3444
|
+
});
|
|
3445
|
+
|
|
3446
|
+
let bottleneck = null, maxCount = 0;
|
|
3447
|
+
for (const [stage, count] of Object.entries(stageCounts)) {
|
|
3448
|
+
if (stage === 'New' || stage === 'Done') continue;
|
|
3449
|
+
if (count > maxCount) { maxCount = count; bottleneck = stage; }
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
return {
|
|
3453
|
+
key: lane.key, label: lane.label, color: lane.color,
|
|
3454
|
+
total, done, active, stuck,
|
|
3455
|
+
progress: total > 0 ? done / total : 0,
|
|
3456
|
+
stageCounts, bottleneck, bottleneckCount: maxCount,
|
|
3457
|
+
};
|
|
3458
|
+
});
|
|
3459
|
+
|
|
3460
|
+
const allActiveCounts = [];
|
|
3461
|
+
swimlaneStats.forEach(lane => {
|
|
3462
|
+
Object.entries(lane.stageCounts).forEach(([stage, count]) => {
|
|
3463
|
+
if (stage !== 'New' && stage !== 'Done' && count > 0) allActiveCounts.push(count);
|
|
3464
|
+
});
|
|
3465
|
+
});
|
|
3466
|
+
const avgCount = allActiveCounts.length > 0
|
|
3467
|
+
? allActiveCounts.reduce((a, b) => a + b, 0) / allActiveCounts.length : 0;
|
|
3468
|
+
|
|
3469
|
+
const bottlenecks = [];
|
|
3470
|
+
swimlaneStats.forEach(lane => {
|
|
3471
|
+
Object.entries(lane.stageCounts).forEach(([stage, count]) => {
|
|
3472
|
+
if (stage === 'New' || stage === 'Done') return;
|
|
3473
|
+
if (count > 0 && count >= avgCount * 2) {
|
|
3474
|
+
bottlenecks.push({
|
|
3475
|
+
swimlane: lane.key, swimlaneLabel: lane.label,
|
|
3476
|
+
stage, count, thresholdExceeded: true,
|
|
3477
|
+
});
|
|
3478
|
+
}
|
|
3479
|
+
});
|
|
3480
|
+
});
|
|
3481
|
+
|
|
3482
|
+
return { totalItems, doneItems, activeItems, stuckItems, globalProgress, swimlaneStats, bottlenecks };
|
|
3483
|
+
}
|
|
3484
|
+
window.computePipelineStats = computePipelineStats;
|
|
3485
|
+
window.__BEASTMODE_PIPELINE_STATS__ = null;
|
|
3486
|
+
|
|
3291
3487
|
function isEpicGroupCollapsed(epicId) {
|
|
3292
3488
|
try {
|
|
3293
3489
|
const state = JSON.parse(localStorage.getItem('beastmode-epic-collapse') || '{}');
|
|
@@ -4242,6 +4438,96 @@ function PipelineView({
|
|
|
4242
4438
|
`;
|
|
4243
4439
|
}
|
|
4244
4440
|
|
|
4441
|
+
// ── Pipeline Health ──
|
|
4442
|
+
|
|
4443
|
+
function SwimlaneMiniBar({ stat }) {
|
|
4444
|
+
const pct = Math.round((stat.progress || 0) * 100);
|
|
4445
|
+
const color = swimlaneColor(stat.color);
|
|
4446
|
+
return html`
|
|
4447
|
+
<div class="swimlane-mini">
|
|
4448
|
+
<div class="swimlane-mini-label">
|
|
4449
|
+
<span>${stat.label}</span>
|
|
4450
|
+
<span class="swimlane-mini-count">${stat.done}/${stat.total}</span>
|
|
4451
|
+
</div>
|
|
4452
|
+
<div class="swimlane-mini-bar"
|
|
4453
|
+
role="progressbar"
|
|
4454
|
+
aria-valuenow=${pct}
|
|
4455
|
+
aria-valuemin="0"
|
|
4456
|
+
aria-valuemax="100"
|
|
4457
|
+
aria-label=${`${stat.label}: ${pct}% complete`}>
|
|
4458
|
+
<div class="swimlane-mini-fill"
|
|
4459
|
+
style=${`width: ${pct}%; background: ${color};`}></div>
|
|
4460
|
+
</div>
|
|
4461
|
+
</div>
|
|
4462
|
+
`;
|
|
4463
|
+
}
|
|
4464
|
+
|
|
4465
|
+
function BottleneckBadge({ warning }) {
|
|
4466
|
+
const title = `${warning.swimlaneLabel}: ${warning.count} items stuck in ${warning.stage}`;
|
|
4467
|
+
return html`
|
|
4468
|
+
<span class="bottleneck-badge" title=${title}>
|
|
4469
|
+
\u26A0 ${warning.swimlaneLabel}: ${warning.count} in ${warning.stage}
|
|
4470
|
+
</span>
|
|
4471
|
+
`;
|
|
4472
|
+
}
|
|
4473
|
+
|
|
4474
|
+
function PipelineHealth({ items }) {
|
|
4475
|
+
const stats = useMemo(() => computePipelineStats(items || []), [items]);
|
|
4476
|
+
const [collapsed, setCollapsed] = useState(() => {
|
|
4477
|
+
try { return localStorage.getItem('beastmode-pipeline-health-collapsed') === 'true'; }
|
|
4478
|
+
catch { return false; }
|
|
4479
|
+
});
|
|
4480
|
+
|
|
4481
|
+
useEffect(() => {
|
|
4482
|
+
window.__BEASTMODE_PIPELINE_STATS__ = stats;
|
|
4483
|
+
}, [stats]);
|
|
4484
|
+
|
|
4485
|
+
const toggle = () => {
|
|
4486
|
+
setCollapsed(v => {
|
|
4487
|
+
const next = !v;
|
|
4488
|
+
try { localStorage.setItem('beastmode-pipeline-health-collapsed', String(next)); } catch {}
|
|
4489
|
+
return next;
|
|
4490
|
+
});
|
|
4491
|
+
};
|
|
4492
|
+
|
|
4493
|
+
const globalPct = Math.round((stats.globalProgress || 0) * 100);
|
|
4494
|
+
const globalText = `${stats.doneItems}/${stats.totalItems} done (${globalPct}%)`;
|
|
4495
|
+
|
|
4496
|
+
return html`
|
|
4497
|
+
<div class=${'pipeline-health' + (collapsed ? ' collapsed' : '')}
|
|
4498
|
+
data-testid="pipeline-health">
|
|
4499
|
+
<div class="pipeline-health-header" onClick=${toggle}>
|
|
4500
|
+
<div class="pipeline-health-title">
|
|
4501
|
+
<span class="pipeline-health-chevron">${collapsed ? '\u25B8' : '\u25BE'}</span>
|
|
4502
|
+
<span>Pipeline Health</span>
|
|
4503
|
+
</div>
|
|
4504
|
+
<div class="pipeline-health-global-text">${globalText}</div>
|
|
4505
|
+
</div>
|
|
4506
|
+
<div class="pipeline-health-bar-global"
|
|
4507
|
+
role="progressbar"
|
|
4508
|
+
aria-valuenow=${globalPct}
|
|
4509
|
+
aria-valuemin="0"
|
|
4510
|
+
aria-valuemax="100"
|
|
4511
|
+
aria-label=${`Pipeline progress: ${globalPct}%`}>
|
|
4512
|
+
<div class="pipeline-health-bar-fill"
|
|
4513
|
+
style=${`width: ${globalPct}%`}></div>
|
|
4514
|
+
</div>
|
|
4515
|
+
<div class="pipeline-health-swimlanes">
|
|
4516
|
+
${stats.swimlaneStats.map(stat => html`
|
|
4517
|
+
<${SwimlaneMiniBar} key=${stat.key} stat=${stat} />
|
|
4518
|
+
`)}
|
|
4519
|
+
</div>
|
|
4520
|
+
${stats.bottlenecks.length > 0 && html`
|
|
4521
|
+
<div class="pipeline-health-warnings">
|
|
4522
|
+
${stats.bottlenecks.map((w, i) => html`
|
|
4523
|
+
<${BottleneckBadge} key=${i} warning=${w} />
|
|
4524
|
+
`)}
|
|
4525
|
+
</div>
|
|
4526
|
+
`}
|
|
4527
|
+
</div>
|
|
4528
|
+
`;
|
|
4529
|
+
}
|
|
4530
|
+
|
|
4245
4531
|
// ── Board Page ──
|
|
4246
4532
|
|
|
4247
4533
|
function BoardPage({ selectedProject }) {
|
|
@@ -4564,6 +4850,9 @@ function BoardPage({ selectedProject }) {
|
|
|
4564
4850
|
|
|
4565
4851
|
const totalItems = filteredItems.length;
|
|
4566
4852
|
const activeItems = filteredItems.filter(i => i.status !== 'Done').length;
|
|
4853
|
+
const doneItems = filteredItems.filter(i => i.status === 'Done').length;
|
|
4854
|
+
const stuckItems = filteredItems.filter(i => i.status === 'Stuck').length;
|
|
4855
|
+
const progressPct = totalItems > 0 ? Math.round((doneItems / totalItems) * 100) : 0;
|
|
4567
4856
|
const isFiltered = searchTerm.length >= 2 || activeFilterCount > 0;
|
|
4568
4857
|
|
|
4569
4858
|
return html`
|
|
@@ -4580,6 +4869,9 @@ function BoardPage({ selectedProject }) {
|
|
|
4580
4869
|
<span class="stat-label">Board</span>
|
|
4581
4870
|
<span class="stat-item"><strong>${totalItems}</strong>${isFiltered ? '/' + items.length : ''} total</span>
|
|
4582
4871
|
<span class="stat-item"><strong>${activeItems}</strong> active</span>
|
|
4872
|
+
<span class="stat-item" style="color: var(--success);"><strong style="color: var(--success);">${doneItems}</strong> done</span>
|
|
4873
|
+
${stuckItems > 0 && html`<span class="stat-item" style="color: var(--danger);"><strong style="color: var(--danger);">${stuckItems}</strong> stuck</span>`}
|
|
4874
|
+
<span class="stat-item" style="color: var(--accent);"><strong style="color: var(--accent); font-family: var(--font-mono);">${progressPct}%</strong> complete</span>
|
|
4583
4875
|
</div>
|
|
4584
4876
|
|
|
4585
4877
|
<div class="board-search-wrap">
|
|
@@ -4655,6 +4947,8 @@ function BoardPage({ selectedProject }) {
|
|
|
4655
4947
|
</div>
|
|
4656
4948
|
`}
|
|
4657
4949
|
|
|
4950
|
+
${items.length > 0 && html`<${PipelineHealth} items=${filteredItems} />`}
|
|
4951
|
+
|
|
4658
4952
|
${viewMode === 'pipeline'
|
|
4659
4953
|
? html`<${PipelineView}
|
|
4660
4954
|
filteredItems=${filteredItems}
|
package/dist/web/build-stamp.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
20260418-
|
|
1
|
+
20260418-190841-6b44897
|