@beastmode-develeap/beastmode 0.1.179 → 0.1.181

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__ = "20260507-062410-80e7997";</script>
18
+ <script>window.__BUILD_STAMP__ = "20260507-091132-6af0b67";</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">
@@ -1540,6 +1540,89 @@ input[type="range"]::-webkit-slider-thumb {
1540
1540
  opacity: 0.4;
1541
1541
  cursor: not-allowed;
1542
1542
  }
1543
+
1544
+ /* Deploy modal styles */
1545
+ .deploy-modal-overlay {
1546
+ position: fixed;
1547
+ inset: 0;
1548
+ background: rgba(0, 0, 0, 0.5);
1549
+ display: flex;
1550
+ align-items: center;
1551
+ justify-content: center;
1552
+ z-index: 1000;
1553
+ }
1554
+ .deploy-modal {
1555
+ background: var(--surface);
1556
+ border: 1px solid var(--border);
1557
+ border-radius: 12px;
1558
+ padding: 24px;
1559
+ min-width: 360px;
1560
+ max-width: 480px;
1561
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
1562
+ }
1563
+ .deploy-modal h3 {
1564
+ margin: 0 0 16px;
1565
+ }
1566
+ .deploy-modal label {
1567
+ display: block;
1568
+ font-size: 13px;
1569
+ color: var(--text-muted);
1570
+ margin: 12px 0 4px;
1571
+ }
1572
+ .deploy-modal select,
1573
+ .deploy-modal textarea {
1574
+ width: 100%;
1575
+ padding: 8px;
1576
+ border: 1px solid var(--border);
1577
+ border-radius: 6px;
1578
+ background: var(--bg);
1579
+ color: var(--text);
1580
+ font-size: 13px;
1581
+ box-sizing: border-box;
1582
+ }
1583
+ .deploy-modal textarea {
1584
+ resize: vertical;
1585
+ min-height: 60px;
1586
+ font-family: monospace;
1587
+ }
1588
+ .deploy-modal-actions {
1589
+ display: flex;
1590
+ justify-content: flex-end;
1591
+ gap: 8px;
1592
+ margin-top: 16px;
1593
+ }
1594
+ .deploy-modal-actions button {
1595
+ padding: 8px 16px;
1596
+ border: 1px solid var(--border);
1597
+ border-radius: 6px;
1598
+ background: var(--bg);
1599
+ color: var(--text);
1600
+ font-size: 13px;
1601
+ cursor: pointer;
1602
+ transition: background 0.15s;
1603
+ }
1604
+ .deploy-modal-actions button:hover {
1605
+ background: var(--bg-hover);
1606
+ }
1607
+ .deploy-modal-actions .btn-primary {
1608
+ background: var(--primary);
1609
+ color: white;
1610
+ border-color: var(--primary);
1611
+ }
1612
+ .deploy-modal-actions .btn-primary:hover {
1613
+ opacity: 0.9;
1614
+ }
1615
+ .deploy-modal-check {
1616
+ display: flex;
1617
+ align-items: center;
1618
+ gap: 6px;
1619
+ margin: 12px 0 0;
1620
+ font-size: 13px;
1621
+ }
1622
+ .deploy-modal-check input {
1623
+ cursor: pointer;
1624
+ }
1625
+
1543
1626
  @keyframes pulse-dot {
1544
1627
  0%, 100% { opacity: 1; }
1545
1628
  50% { opacity: 0.5; }
@@ -2509,6 +2592,21 @@ input[type="range"]::-webkit-slider-thumb {
2509
2592
  .badge-info { background: var(--info-subtle); color: var(--info); }
2510
2593
  .badge-muted { background: var(--surface-elevated); color: var(--text-muted); }
2511
2594
  .badge-accent { background: var(--accent-subtle); color: var(--accent); }
2595
+ .badge-restart {
2596
+ display: inline-block;
2597
+ padding: 2px 6px;
2598
+ border-radius: 3px;
2599
+ font-size: 10px;
2600
+ font-weight: 600;
2601
+ background: rgba(249, 115, 22, 0.15);
2602
+ color: #fb923c;
2603
+ border: 1px solid rgba(249, 115, 22, 0.3);
2604
+ margin-left: 6px;
2605
+ cursor: help;
2606
+ vertical-align: middle;
2607
+ text-transform: uppercase;
2608
+ letter-spacing: 0.5px;
2609
+ }
2512
2610
 
2513
2611
  /* ================================================================
2514
2612
  SKELETON LOADING
@@ -4711,6 +4809,147 @@ function CostPhaseTable({ itemId, boardParam }) {
4711
4809
  `;
4712
4810
  }
4713
4811
 
4812
+ // ── Deploy Modal ──
4813
+
4814
+ function DeployModal({ envName, refs, loading, item, selectedProject, onClose }) {
4815
+ const [selectedRef, setSelectedRef] = useState('main');
4816
+ const [dryRun, setDryRun] = useState(false);
4817
+ const [description, setDescription] = useState('');
4818
+ const [submitting, setSubmitting] = useState(false);
4819
+
4820
+ // Default-select main if available, else the first branch.
4821
+ useEffect(() => {
4822
+ const branches = (refs && refs.branches) || [];
4823
+ if (!branches.length) return;
4824
+ if (branches.includes('main')) {
4825
+ setSelectedRef('main');
4826
+ } else {
4827
+ setSelectedRef(branches[0]);
4828
+ }
4829
+ }, [refs && refs.branches && refs.branches.join(',')]);
4830
+
4831
+ // Escape key closes the modal — symmetric with overlay click.
4832
+ useEffect(() => {
4833
+ const onKey = (e) => { if (e.key === 'Escape') onClose(); };
4834
+ window.addEventListener('keydown', onKey);
4835
+ return () => window.removeEventListener('keydown', onKey);
4836
+ }, []);
4837
+
4838
+ const handleDeploy = async () => {
4839
+ if (!envName || submitting) return;
4840
+ setSubmitting(true);
4841
+ try {
4842
+ const proj = (selectedProject && selectedProject !== 'all')
4843
+ ? selectedProject
4844
+ : (item && item.project_id) || 'beastmode';
4845
+ const boardParam = proj ? '?board=' + encodeURIComponent(proj) : '';
4846
+ const body = {
4847
+ environment: envName,
4848
+ project: proj,
4849
+ source_ref: selectedRef,
4850
+ dry_run: dryRun,
4851
+ description: description || undefined,
4852
+ };
4853
+ if (item && item.id) body.source_item_id = String(item.id);
4854
+
4855
+ const response = await fetch('/api/deploy/trigger' + boardParam, {
4856
+ method: 'POST',
4857
+ headers: { 'Content-Type': 'application/json' },
4858
+ body: JSON.stringify(body),
4859
+ });
4860
+ const data = await response.json();
4861
+
4862
+ if (response.status === 409) {
4863
+ alert(data.detail || 'A deploy to ' + envName + ' is already in progress.');
4864
+ } else if (!response.ok) {
4865
+ alert('Deploy failed: ' + (data.detail || ('HTTP ' + response.status)));
4866
+ } else if (data && data.id) {
4867
+ alert('Deploy task created: #' + data.id);
4868
+ onClose();
4869
+ }
4870
+ } catch (err) {
4871
+ alert('Deploy error: ' + err.message);
4872
+ } finally {
4873
+ setSubmitting(false);
4874
+ }
4875
+ };
4876
+
4877
+ const branches = (refs && refs.branches) || [];
4878
+ const tags = (refs && refs.tags) || [];
4879
+
4880
+ return html`
4881
+ <div
4882
+ class="deploy-modal-overlay"
4883
+ data-testid="deploy-modal"
4884
+ onClick=${(e) => e.target === e.currentTarget && onClose()}
4885
+ >
4886
+ <div class="deploy-modal">
4887
+ <h3>Deploy to ${envName}</h3>
4888
+
4889
+ <label>Git Ref</label>
4890
+ <select
4891
+ data-testid="deploy-ref-select"
4892
+ value=${selectedRef}
4893
+ onChange=${(e) => setSelectedRef(e.target.value)}
4894
+ disabled=${loading || submitting}
4895
+ >
4896
+ ${loading ? html`<option>Loading…</option>` : null}
4897
+ ${!loading && branches.length === 0 && tags.length === 0
4898
+ ? html`<option>No refs available</option>`
4899
+ : null}
4900
+ ${branches.length > 0 ? html`
4901
+ <optgroup label="Branches">
4902
+ ${branches.map(b => html`<option value=${b}>${b}</option>`)}
4903
+ </optgroup>
4904
+ ` : null}
4905
+ ${tags.length > 0 ? html`
4906
+ <optgroup label="Tags">
4907
+ ${tags.map(t => html`<option value=${t}>${t}</option>`)}
4908
+ </optgroup>
4909
+ ` : null}
4910
+ </select>
4911
+
4912
+ <label>Description (optional)</label>
4913
+ <textarea
4914
+ data-testid="deploy-description"
4915
+ placeholder="Why are you deploying?"
4916
+ value=${description}
4917
+ onChange=${(e) => setDescription(e.target.value)}
4918
+ disabled=${submitting}
4919
+ ></textarea>
4920
+
4921
+ <div class="deploy-modal-check">
4922
+ <input
4923
+ type="checkbox"
4924
+ id="dry-run-check"
4925
+ data-testid="deploy-dry-run"
4926
+ checked=${dryRun}
4927
+ onChange=${(e) => setDryRun(e.target.checked)}
4928
+ disabled=${submitting}
4929
+ />
4930
+ <label for="dry-run-check" style="margin:0;cursor:pointer;">Dry run (no changes)</label>
4931
+ </div>
4932
+
4933
+ <div class="deploy-modal-actions">
4934
+ <button
4935
+ data-testid="deploy-cancel"
4936
+ onClick=${onClose}
4937
+ disabled=${submitting}
4938
+ >Cancel</button>
4939
+ <button
4940
+ class="btn-primary"
4941
+ data-testid="deploy-confirm"
4942
+ onClick=${handleDeploy}
4943
+ disabled=${submitting || loading}
4944
+ >
4945
+ ${submitting ? 'Deploying...' : dryRun ? 'Test' : 'Deploy'}
4946
+ </button>
4947
+ </div>
4948
+ </div>
4949
+ </div>
4950
+ `;
4951
+ }
4952
+
4714
4953
  // ── Item Detail Sidebar ──
4715
4954
 
4716
4955
  function ItemDetailSidebar({ item, onClose, onStatusChange, selectedProject }) {
@@ -4726,6 +4965,7 @@ function ItemDetailSidebar({ item, onClose, onStatusChange, selectedProject }) {
4726
4965
  const [loadingCost, setLoadingCost] = useState(true);
4727
4966
  const [phaseData, setPhaseData] = useState([]);
4728
4967
  const [envTimeline, setEnvTimeline] = useState(null);
4968
+ const [deployModal, setDeployModal] = useState(null);
4729
4969
  const sidebarRef = useRef(null);
4730
4970
  const topCommentRef = useRef(null);
4731
4971
 
@@ -5050,36 +5290,14 @@ function ItemDetailSidebar({ item, onClose, onStatusChange, selectedProject }) {
5050
5290
  onClick=${(e) => {
5051
5291
  e.stopPropagation();
5052
5292
  if (isDeploying) return;
5053
- if (!confirm('Deploy current code to ' + envName + '?')) return;
5054
5293
  const proj = (selectedProject && selectedProject !== 'all')
5055
5294
  ? selectedProject
5056
5295
  : (item && item.project_id) || 'beastmode';
5057
- const boardParam = proj ? '?board=' + encodeURIComponent(proj) : '';
5058
- const body = {
5059
- environment: envName,
5060
- project: proj,
5061
- };
5062
- if (item && item.id) body.source_item_id = String(item.id);
5063
- fetch('/api/deploy/trigger' + boardParam, {
5064
- method: 'POST',
5065
- headers: { 'Content-Type': 'application/json' },
5066
- body: JSON.stringify(body),
5067
- })
5068
- .then(r => r.json().then(data => ({ ok: r.ok, status: r.status, data })))
5069
- .then(({ ok, status, data }) => {
5070
- if (status === 409) {
5071
- alert(data.detail || 'A deploy to ' + envName + ' is already in progress.');
5072
- return;
5073
- }
5074
- if (!ok) {
5075
- alert('Deploy failed: ' + (data.detail || ('HTTP ' + status)));
5076
- return;
5077
- }
5078
- if (data && data.id) {
5079
- alert('Deploy task created: #' + data.id);
5080
- }
5081
- })
5082
- .catch(err => alert('Deploy error: ' + err.message));
5296
+ setDeployModal({ envName, proj, refs: null, loading: true });
5297
+ fetch('/api/deploy/refs?project=' + encodeURIComponent(proj))
5298
+ .then(r => r.ok ? r.json() : { branches: [], tags: [] })
5299
+ .then(refs => setDeployModal(prev => prev ? { ...prev, refs, loading: false } : null))
5300
+ .catch(() => setDeployModal(prev => prev ? { ...prev, refs: { branches: [], tags: [] }, loading: false } : null));
5083
5301
  }}
5084
5302
  >🚀</button>
5085
5303
  </div>
@@ -5136,6 +5354,14 @@ function ItemDetailSidebar({ item, onClose, onStatusChange, selectedProject }) {
5136
5354
  <${CommentBox} itemId=${item.id} onPosted=${refreshUpdates} />
5137
5355
  </div>
5138
5356
  </aside>
5357
+ ${deployModal && html`<${DeployModal}
5358
+ envName=${deployModal.envName}
5359
+ refs=${deployModal.refs}
5360
+ loading=${deployModal.loading}
5361
+ item=${item}
5362
+ selectedProject=${selectedProject}
5363
+ onClose=${() => setDeployModal(null)}
5364
+ />`}
5139
5365
  `;
5140
5366
  }
5141
5367
 
@@ -8175,11 +8401,17 @@ function HelpPage() {
8175
8401
  // Settings Page
8176
8402
  // ================================================================
8177
8403
 
8404
+ // Badge component for restart-required fields (shown next to section headers)
8405
+ function RestartBadge({field, restartFields}) {
8406
+ if (!restartFields || !restartFields.has(field)) return null;
8407
+ return html`<span class="badge-restart" title="Changing this field requires a daemon restart to take effect">restart required</span>`;
8408
+ }
8409
+
8178
8410
  // Gap 15: opt-in anonymous telemetry status surface. Reads
8179
8411
  // /api/telemetry/status (proxied to the board, which reads the daemon's
8180
8412
  // last heartbeat). Always renders — shows "Disabled" with a red dot
8181
8413
  // when telemetry is off, full metrics when on. See docs/telemetry.md.
8182
- function TelemetrySettings() {
8414
+ function TelemetrySettings({restartFields}) {
8183
8415
  const [status, setStatus] = useState(null);
8184
8416
  const [loading, setLoading] = useState(true);
8185
8417
  const [loadError, setLoadError] = useState(null);
@@ -8196,7 +8428,7 @@ function TelemetrySettings() {
8196
8428
 
8197
8429
  return html`
8198
8430
  <div class="settings-section">
8199
- <h3>Telemetry</h3>
8431
+ <h3 style="display:flex;align-items:center;gap:8px">Telemetry <${RestartBadge} field="telemetry" restartFields=${restartFields} /></h3>
8200
8432
  ${loading ? html`<p style="font-size:13px;color:var(--text-muted);">Loading telemetry status...</p>` : null}
8201
8433
  ${loadError ? html`<p style="font-size:13px;color:var(--danger);">Telemetry status unavailable: ${loadError}</p>` : null}
8202
8434
  ${!loading && !loadError ? html`
@@ -8255,6 +8487,8 @@ function SettingsPage() {
8255
8487
  const [success, setSuccess] = useState(null);
8256
8488
  const [saving, setSaving] = useState(false);
8257
8489
 
8490
+ const [restartFields, setRestartFields] = useState(new Set());
8491
+
8258
8492
  useEffect(() => {
8259
8493
  api('GET', '/api/config')
8260
8494
  .then(setConfig)
@@ -8262,6 +8496,20 @@ function SettingsPage() {
8262
8496
  .finally(() => setLoading(false));
8263
8497
  }, []);
8264
8498
 
8499
+ useEffect(() => {
8500
+ api('GET', '/api/daemon/reload-categories')
8501
+ .then(data => {
8502
+ const cats = data.categories || {};
8503
+ const fields = new Set(
8504
+ Object.entries(cats)
8505
+ .filter(([_, v]) => v === 'restart')
8506
+ .map(([k]) => k)
8507
+ );
8508
+ setRestartFields(fields);
8509
+ })
8510
+ .catch(() => {});
8511
+ }, []);
8512
+
8265
8513
  const saveConfig = async (forceWrite = false) => {
8266
8514
  try {
8267
8515
  setSaving(true);
@@ -8696,7 +8944,7 @@ function SettingsPage() {
8696
8944
  </div>
8697
8945
 
8698
8946
  <!-- Telemetry Section (Gap 15) -->
8699
- <${TelemetrySettings} />
8947
+ <${TelemetrySettings} restartFields=${restartFields} />
8700
8948
  </div>
8701
8949
  </div>
8702
8950
  `;
@@ -1 +1 @@
1
- 80e79979abd745cd7807b4b463ed0023c13ac768
1
+ 6af0b67cf1cb45e506a8f816517181c9ac1e8606
@@ -1 +1 @@
1
- 20260507-062410-80e7997
1
+ 20260507-091132-6af0b67
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beastmode-develeap/beastmode",
3
- "version": "0.1.179",
3
+ "version": "0.1.181",
4
4
  "description": "BeastMode Dark Factory — turn intent into verified software",
5
5
  "type": "module",
6
6
  "bin": {