@beastmode-develeap/beastmode 0.1.203 → 0.1.204

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__ = "20260509-101824-acec518";</script>
18
+ <script>window.__BUILD_STAMP__ = "20260509-102609-7aa0839";</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">
@@ -1522,6 +1522,37 @@ input[type="range"]::-webkit-slider-thumb {
1522
1522
  margin: 0 -8px;
1523
1523
  padding: 8px;
1524
1524
  }
1525
+ /* Audit Trail — Superseded transition history (Story 6) */
1526
+ .audit-timeline { display: flex; flex-direction: column; gap: 8px; }
1527
+ .audit-entry {
1528
+ background: var(--bg-input);
1529
+ border-radius: 8px;
1530
+ padding: 10px 12px;
1531
+ border-left: 3px solid var(--accent);
1532
+ }
1533
+ .audit-entry-header {
1534
+ display: flex;
1535
+ justify-content: space-between;
1536
+ align-items: center;
1537
+ margin-bottom: 4px;
1538
+ }
1539
+ .audit-actor { font-weight: 600; font-size: 13px; }
1540
+ .audit-time { color: var(--text-muted); font-size: 12px; }
1541
+ .audit-entry-body { font-size: 13px; display: flex; flex-direction: column; gap: 4px; }
1542
+ .audit-from { color: var(--text-muted); }
1543
+ .audit-successor a, .audit-cascade a {
1544
+ color: var(--accent);
1545
+ text-decoration: none;
1546
+ }
1547
+ .audit-successor a:hover, .audit-cascade a:hover { text-decoration: underline; }
1548
+ .audit-reason {
1549
+ margin-top: 4px;
1550
+ padding: 6px 8px;
1551
+ background: var(--bg-secondary);
1552
+ border-radius: 4px;
1553
+ font-style: italic;
1554
+ color: var(--text-secondary);
1555
+ }
1525
1556
  .btn-env-deploy {
1526
1557
  display: inline-flex;
1527
1558
  align-items: center;
@@ -4260,7 +4291,7 @@ const STATUSES = [
4260
4291
  'Waiting for Infra Approval', 'Provisioning Infra', 'Infra Ready',
4261
4292
  'Approved & Merge to Main', 'Ready For Review',
4262
4293
  'Verifying Prod with Tests', 'Verifying Epic', 'Waiting for Epic Bugs', 'Awaiting Input',
4263
- 'Epic Breakdown Posted', 'Stuck', 'Done',
4294
+ 'Epic Breakdown Posted', 'Stuck', 'Done', 'Superseded',
4264
4295
  ];
4265
4296
 
4266
4297
  const KANBAN_COLUMNS = [
@@ -5095,12 +5126,88 @@ function DeployModal({ envName, refs, loading, item, selectedProject, onClose })
5095
5126
  `;
5096
5127
  }
5097
5128
 
5129
+ // ── Superseded Modal ──
5130
+
5131
+ function SupersededModal({ item, onConfirm, onCancel }) {
5132
+ const [supersededBy, setSupersededBy] = useState('');
5133
+ const [reason, setReason] = useState('');
5134
+ const [error, setError] = useState('');
5135
+ const [loading, setLoading] = useState(false);
5136
+
5137
+ const isEpic = (item.task_type || '').toLowerCase() === 'epic';
5138
+
5139
+ useEffect(() => {
5140
+ const handleEsc = (e) => { if (e.key === 'Escape') onCancel(); };
5141
+ document.addEventListener('keydown', handleEsc);
5142
+ return () => document.removeEventListener('keydown', handleEsc);
5143
+ }, []);
5144
+
5145
+ const handleSubmit = async () => {
5146
+ if (isEpic && !supersededBy.trim()) {
5147
+ setError('Epics require a successor item ID (superseded_by).');
5148
+ return;
5149
+ }
5150
+ if (isEpic && !reason.trim()) {
5151
+ setError('Epics require a supersede reason.');
5152
+ return;
5153
+ }
5154
+ setLoading(true);
5155
+ setError('');
5156
+ try {
5157
+ const payload = { status: 'Superseded' };
5158
+ const extra = {};
5159
+ if (supersededBy.trim()) extra.superseded_by = supersededBy.trim();
5160
+ if (reason.trim()) extra.superseded_reason = reason.trim();
5161
+ if (Object.keys(extra).length > 0) payload.extra_columns = extra;
5162
+ await api('PATCH', '/api/board/items/' + item.id, payload);
5163
+ onConfirm();
5164
+ } catch (err) {
5165
+ setError(err.message || 'Failed to update status');
5166
+ } finally {
5167
+ setLoading(false);
5168
+ }
5169
+ };
5170
+
5171
+ return html`
5172
+ <div class="deploy-modal-overlay" data-testid="superseded-modal-overlay"
5173
+ onClick=${(e) => { if (e.target === e.currentTarget) onCancel(); }}>
5174
+ <div class="deploy-modal" data-testid="superseded-modal">
5175
+ <h3>Mark as Superseded</h3>
5176
+ <p style="font-size:13px;color:var(--text-muted);margin:0 0 12px;">
5177
+ This action is permanent. Superseded items cannot be reactivated.
5178
+ </p>
5179
+ ${error && html`<div class="error-banner" style="margin-bottom:12px;">${error}</div>`}
5180
+ <label>Successor item ID ${isEpic ? html`<span style="color:var(--danger);">*</span>` : '(optional)'}</label>
5181
+ <input type="text" value=${supersededBy}
5182
+ data-testid="superseded-by-input"
5183
+ placeholder="e.g. 42"
5184
+ onInput=${(e) => setSupersededBy(e.target.value)}
5185
+ style="width:100%;padding:8px;border:1px solid var(--border);border-radius:6px;background:var(--bg);color:var(--text);font-size:13px;box-sizing:border-box;" />
5186
+ <label>Reason ${isEpic ? html`<span style="color:var(--danger);">*</span>` : '(optional)'}</label>
5187
+ <textarea value=${reason}
5188
+ data-testid="superseded-reason-input"
5189
+ placeholder="Why is this item being superseded?"
5190
+ onInput=${(e) => setReason(e.target.value)}
5191
+ style="width:100%;padding:8px;border:1px solid var(--border);border-radius:6px;background:var(--bg);color:var(--text);font-size:13px;box-sizing:border-box;resize:vertical;min-height:60px;" />
5192
+ <div class="deploy-modal-actions">
5193
+ <button onClick=${onCancel} disabled=${loading}>Cancel</button>
5194
+ <button class="btn-primary" onClick=${handleSubmit} disabled=${loading}
5195
+ data-testid="superseded-confirm-btn">
5196
+ ${loading ? 'Updating...' : 'Mark Superseded'}
5197
+ </button>
5198
+ </div>
5199
+ </div>
5200
+ </div>
5201
+ `;
5202
+ }
5203
+
5098
5204
  // ── Item Detail Sidebar ──
5099
5205
 
5100
- function ItemDetailSidebar({ item, onClose, onStatusChange, selectedProject }) {
5206
+ function ItemDetailSidebar({ item, onClose, onStatusChange, selectedProject, allItems, onSelectItem }) {
5101
5207
  const [updates, setUpdates] = useState([]);
5102
5208
  const [loadingUpdates, setLoadingUpdates] = useState(true);
5103
5209
  const [sortNewest, setSortNewest] = useState(true);
5210
+ const [showSupersededModal, setShowSupersededModal] = useState(false);
5104
5211
  // Attachments (Gap 8a — 2026-04-15): list every attachment the
5105
5212
  // board service has for this item, render image types as thumbnails
5106
5213
  // that open in a new tab when clicked.
@@ -5111,6 +5218,7 @@ function ItemDetailSidebar({ item, onClose, onStatusChange, selectedProject }) {
5111
5218
  const [phaseData, setPhaseData] = useState([]);
5112
5219
  const [envTimeline, setEnvTimeline] = useState(null);
5113
5220
  const [deployModal, setDeployModal] = useState(null);
5221
+ const [auditEvents, setAuditEvents] = useState([]);
5114
5222
  const sidebarRef = useRef(null);
5115
5223
  const topCommentRef = useRef(null);
5116
5224
 
@@ -5132,6 +5240,25 @@ function ItemDetailSidebar({ item, onClose, onStatusChange, selectedProject }) {
5132
5240
  .catch(() => setEnvTimeline(null));
5133
5241
  }, [item && item.id, item && item.extra && item.extra.current_env]);
5134
5242
 
5243
+ // FR-4: Fetch SupersededTransition audit events for the selected item.
5244
+ // Only fires when the item is in Superseded status — non-Superseded items
5245
+ // never have audit entries and the section is hidden anyway (NFR-4).
5246
+ useEffect(() => {
5247
+ if (!item || item.status !== 'Superseded') {
5248
+ setAuditEvents([]);
5249
+ return;
5250
+ }
5251
+ const proj = selectedProject && selectedProject !== 'all' ? selectedProject : null;
5252
+ const qs = proj ? '?board=' + encodeURIComponent(proj) : '';
5253
+ const sep = qs ? '&' : '?';
5254
+ const url = '/api/events' + qs + sep + 'item_id=' + encodeURIComponent(item.id) +
5255
+ '&type=SupersededTransition';
5256
+ fetch(url)
5257
+ .then(r => r.ok ? r.json() : [])
5258
+ .then(events => setAuditEvents(Array.isArray(events) ? events : []))
5259
+ .catch(() => setAuditEvents([]));
5260
+ }, [item && item.id, item && item.status]);
5261
+
5135
5262
  // Cost summary fetch — keyed on item.id, refreshed alongside the
5136
5263
  // 10-second updates/attachments poll below. The api() helper only
5137
5264
  // auto-scopes /api/board/*, so append ?board=<proj> manually.
@@ -5300,9 +5427,17 @@ function ItemDetailSidebar({ item, onClose, onStatusChange, selectedProject }) {
5300
5427
  </div>
5301
5428
  <div class="detail-fields">
5302
5429
  <label>Status</label>
5303
- <select value=${item.status || 'New'} onChange=${async (e) => {
5430
+ <select value=${item.status || 'New'}
5431
+ disabled=${item.status === 'Superseded'}
5432
+ onChange=${async (e) => {
5433
+ const newStatus = e.target.value;
5434
+ if (newStatus === 'Superseded') {
5435
+ e.target.value = item.status || 'New';
5436
+ setShowSupersededModal(true);
5437
+ return;
5438
+ }
5304
5439
  try {
5305
- await api('PATCH', '/api/board/items/' + item.id, { status: e.target.value });
5440
+ await api('PATCH', '/api/board/items/' + item.id, { status: newStatus });
5306
5441
  onStatusChange();
5307
5442
  } catch (err) { /* ignore */ }
5308
5443
  }}>
@@ -5451,6 +5586,49 @@ function ItemDetailSidebar({ item, onClose, onStatusChange, selectedProject }) {
5451
5586
  </div>
5452
5587
  </div>
5453
5588
  `}
5589
+ ${item && item.status === 'Superseded' && auditEvents.length > 0 && html`
5590
+ <div class="detail-section" style="padding:12px 24px 0;" data-testid="audit-trail-section">
5591
+ <h4 class="detail-section-title" style="margin:0 0 8px 0;font-size:13px;font-weight:600;">Audit Trail</h4>
5592
+ <div class="audit-timeline">
5593
+ ${auditEvents.map(ev => {
5594
+ const payload = (ev && ev.payload) || {};
5595
+ const isCascade = ev.actor === 'cascade';
5596
+ return html`
5597
+ <div class="audit-entry" key=${ev.id} data-testid="audit-entry">
5598
+ <div class="audit-entry-header">
5599
+ <span class="audit-actor" data-testid="audit-actor">${isCascade ? 'Auto-cascaded' : 'Status changed'}</span>
5600
+ <span class="audit-time" data-testid="audit-time">${timeAgo(ev.timestamp)}</span>
5601
+ </div>
5602
+ <div class="audit-entry-body">
5603
+ <span class="audit-from" data-testid="audit-from-status">From: <strong>${payload.from_status || ''}</strong></span>
5604
+ ${payload.superseded_by && html`
5605
+ <span class="audit-successor">
5606
+ Successor: <a href="#" data-testid="audit-successor-link" onClick=${(e) => {
5607
+ e.preventDefault();
5608
+ const target = allItems && allItems.find ? allItems.find(i => String(i.id) === String(payload.superseded_by)) : null;
5609
+ if (target && onSelectItem) onSelectItem(target);
5610
+ }}>#${payload.superseded_by}</a>
5611
+ </span>
5612
+ `}
5613
+ ${payload.cascade_source && html`
5614
+ <span class="audit-cascade">
5615
+ Cascaded from parent: <a href="#" data-testid="audit-cascade-source" onClick=${(e) => {
5616
+ e.preventDefault();
5617
+ const target = allItems && allItems.find ? allItems.find(i => String(i.id) === String(payload.cascade_source)) : null;
5618
+ if (target && onSelectItem) onSelectItem(target);
5619
+ }}>#${payload.cascade_source}</a>
5620
+ </span>
5621
+ `}
5622
+ ${payload.superseded_reason && html`
5623
+ <div class="audit-reason" data-testid="audit-reason">${payload.superseded_reason}</div>
5624
+ `}
5625
+ </div>
5626
+ </div>
5627
+ `;
5628
+ })}
5629
+ </div>
5630
+ </div>
5631
+ `}
5454
5632
  ${(loadingAttachments || attachments.length > 0) && html`
5455
5633
  <div style="padding:12px 24px 0;">
5456
5634
  <h4 style="margin:0 0 8px 0;font-size:13px;font-weight:600;">
@@ -5507,6 +5685,11 @@ function ItemDetailSidebar({ item, onClose, onStatusChange, selectedProject }) {
5507
5685
  selectedProject=${selectedProject}
5508
5686
  onClose=${() => setDeployModal(null)}
5509
5687
  />`}
5688
+ ${showSupersededModal && html`<${SupersededModal}
5689
+ item=${item}
5690
+ onConfirm=${() => { setShowSupersededModal(false); onStatusChange(); }}
5691
+ onCancel=${() => setShowSupersededModal(false)}
5692
+ />`}
5510
5693
  `;
5511
5694
  }
5512
5695
 
@@ -6359,6 +6542,7 @@ function BoardPage({ selectedProject }) {
6359
6542
  e.preventDefault();
6360
6543
  e.currentTarget.classList.remove('drag-over', 'drag-over-invalid');
6361
6544
  e.currentTarget.removeAttribute('title');
6545
+ if (status === 'Superseded') return;
6362
6546
  const di = dragInfoRef.current;
6363
6547
  if (!di) return;
6364
6548
  const validStages = (typeof getStagesForType === 'function')
@@ -6841,7 +7025,7 @@ function BoardPage({ selectedProject }) {
6841
7025
  `}
6842
7026
 
6843
7027
  ${showCreateDialog && html`<${CreateTaskDialog} onClose=${() => setShowCreateDialog(false)} onCreated=${fetchItems} />`}
6844
- ${selectedItem && html`<${ItemDetailSidebar} item=${selectedItem} selectedProject=${selectedProject} onClose=${() => setSelectedItem(null)} onStatusChange=${() => { fetchItems(); setSelectedItem(null); }} />`}
7028
+ ${selectedItem && html`<${ItemDetailSidebar} item=${selectedItem} selectedProject=${selectedProject} allItems=${items} onSelectItem=${setSelectedItem} onClose=${() => setSelectedItem(null)} onStatusChange=${() => { fetchItems(); setSelectedItem(null); }} />`}
6845
7029
  </div>
6846
7030
  `;
6847
7031
  }
@@ -1 +1 @@
1
- acec5181b8ac3a2dea78f33581a722e5cbe445b9
1
+ 7aa08391228bd7979262a65efcc7e0fa2df66453
@@ -1 +1 @@
1
- 20260509-101824-acec518
1
+ 20260509-102609-7aa0839
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beastmode-develeap/beastmode",
3
- "version": "0.1.203",
3
+ "version": "0.1.204",
4
4
  "description": "BeastMode Dark Factory — turn intent into verified software",
5
5
  "type": "module",
6
6
  "bin": {