@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.
@@ -15,7 +15,7 @@
15
15
  }
16
16
  </script>
17
17
  <!--BOARD_DATA-->
18
- <script>window.__BUILD_STAMP__ = "20260418-141101-0c947a4";</script>
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}
@@ -1 +1 @@
1
- 20260418-141101-0c947a4
1
+ 20260418-190841-6b44897
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beastmode-develeap/beastmode",
3
- "version": "0.1.119",
3
+ "version": "0.1.121",
4
4
  "description": "BeastMode Dark Factory — turn intent into verified software",
5
5
  "type": "module",
6
6
  "bin": {