@beastmode-develeap/beastmode 0.1.180 → 0.1.182

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-074735-3f990f4";</script>
18
+ <script>window.__BUILD_STAMP__ = "20260507-093155-c3fba6c";</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; }
@@ -4726,6 +4809,147 @@ function CostPhaseTable({ itemId, boardParam }) {
4726
4809
  `;
4727
4810
  }
4728
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
+
4729
4953
  // ── Item Detail Sidebar ──
4730
4954
 
4731
4955
  function ItemDetailSidebar({ item, onClose, onStatusChange, selectedProject }) {
@@ -4741,6 +4965,7 @@ function ItemDetailSidebar({ item, onClose, onStatusChange, selectedProject }) {
4741
4965
  const [loadingCost, setLoadingCost] = useState(true);
4742
4966
  const [phaseData, setPhaseData] = useState([]);
4743
4967
  const [envTimeline, setEnvTimeline] = useState(null);
4968
+ const [deployModal, setDeployModal] = useState(null);
4744
4969
  const sidebarRef = useRef(null);
4745
4970
  const topCommentRef = useRef(null);
4746
4971
 
@@ -5065,36 +5290,14 @@ function ItemDetailSidebar({ item, onClose, onStatusChange, selectedProject }) {
5065
5290
  onClick=${(e) => {
5066
5291
  e.stopPropagation();
5067
5292
  if (isDeploying) return;
5068
- if (!confirm('Deploy current code to ' + envName + '?')) return;
5069
5293
  const proj = (selectedProject && selectedProject !== 'all')
5070
5294
  ? selectedProject
5071
5295
  : (item && item.project_id) || 'beastmode';
5072
- const boardParam = proj ? '?board=' + encodeURIComponent(proj) : '';
5073
- const body = {
5074
- environment: envName,
5075
- project: proj,
5076
- };
5077
- if (item && item.id) body.source_item_id = String(item.id);
5078
- fetch('/api/deploy/trigger' + boardParam, {
5079
- method: 'POST',
5080
- headers: { 'Content-Type': 'application/json' },
5081
- body: JSON.stringify(body),
5082
- })
5083
- .then(r => r.json().then(data => ({ ok: r.ok, status: r.status, data })))
5084
- .then(({ ok, status, data }) => {
5085
- if (status === 409) {
5086
- alert(data.detail || 'A deploy to ' + envName + ' is already in progress.');
5087
- return;
5088
- }
5089
- if (!ok) {
5090
- alert('Deploy failed: ' + (data.detail || ('HTTP ' + status)));
5091
- return;
5092
- }
5093
- if (data && data.id) {
5094
- alert('Deploy task created: #' + data.id);
5095
- }
5096
- })
5097
- .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));
5098
5301
  }}
5099
5302
  >🚀</button>
5100
5303
  </div>
@@ -5151,6 +5354,14 @@ function ItemDetailSidebar({ item, onClose, onStatusChange, selectedProject }) {
5151
5354
  <${CommentBox} itemId=${item.id} onPosted=${refreshUpdates} />
5152
5355
  </div>
5153
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
+ />`}
5154
5365
  `;
5155
5366
  }
5156
5367
 
@@ -8190,15 +8401,16 @@ function HelpPage() {
8190
8401
  // Settings Page
8191
8402
  // ================================================================
8192
8403
 
8193
- // Gap 15: opt-in anonymous telemetry status surface. Reads
8194
- // /api/telemetry/status (proxied to the board, which reads the daemon's
8195
- // last heartbeat). Always renders — shows "Disabled" with a red dot
8196
- // when telemetry is off, full metrics when on. See docs/telemetry.md.
8404
+ // Badge component for restart-required fields (shown next to section headers)
8197
8405
  function RestartBadge({field, restartFields}) {
8198
8406
  if (!restartFields || !restartFields.has(field)) return null;
8199
8407
  return html`<span class="badge-restart" title="Changing this field requires a daemon restart to take effect">restart required</span>`;
8200
8408
  }
8201
8409
 
8410
+ // Gap 15: opt-in anonymous telemetry status surface. Reads
8411
+ // /api/telemetry/status (proxied to the board, which reads the daemon's
8412
+ // last heartbeat). Always renders — shows "Disabled" with a red dot
8413
+ // when telemetry is off, full metrics when on. See docs/telemetry.md.
8202
8414
  function TelemetrySettings({restartFields}) {
8203
8415
  const [status, setStatus] = useState(null);
8204
8416
  const [loading, setLoading] = useState(true);
@@ -8274,6 +8486,7 @@ function SettingsPage() {
8274
8486
  const [error, setError] = useState(null);
8275
8487
  const [success, setSuccess] = useState(null);
8276
8488
  const [saving, setSaving] = useState(false);
8489
+
8277
8490
  const [restartFields, setRestartFields] = useState(new Set());
8278
8491
 
8279
8492
  useEffect(() => {
@@ -8286,7 +8499,7 @@ function SettingsPage() {
8286
8499
  useEffect(() => {
8287
8500
  api('GET', '/api/daemon/reload-categories')
8288
8501
  .then(data => {
8289
- const cats = (data && data.categories) || {};
8502
+ const cats = data.categories || {};
8290
8503
  const fields = new Set(
8291
8504
  Object.entries(cats)
8292
8505
  .filter(([_, v]) => v === 'restart')
@@ -1 +1 @@
1
- 3f990f4d8de08cb374c9645579bf78e12e82bcb1
1
+ c3fba6c603873fcba435d66a137c45c9e2a5d172
@@ -1 +1 @@
1
- 20260507-074735-3f990f4
1
+ 20260507-093155-c3fba6c
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beastmode-develeap/beastmode",
3
- "version": "0.1.180",
3
+ "version": "0.1.182",
4
4
  "description": "BeastMode Dark Factory — turn intent into verified software",
5
5
  "type": "module",
6
6
  "bin": {