synabun 2026.4.26-1 → 2026.4.26-2

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.
@@ -399,8 +399,8 @@ let _cliInstallFailureForced = false;
399
399
  function ensureCliBannerEl() {
400
400
  if (!_panel) return null;
401
401
  const container = panelEl('#cxp-messages-container');
402
- if (!container) return null;
403
- let banner = container.querySelector(':scope > .cxp-cli-banner');
402
+ if (!container || !container.parentNode) return null;
403
+ let banner = container.parentNode.querySelector(':scope > .cxp-cli-banner');
404
404
  if (banner) return banner;
405
405
  const label = getCliLabel('codex');
406
406
  const cmd = getCliInstallCommand('codex');
@@ -418,7 +418,7 @@ function ensureCliBannerEl() {
418
418
  <button class="cxp-cli-banner-recheck" type="button">Re-check</button>
419
419
  </div>
420
420
  `;
421
- container.insertBefore(banner, container.firstChild);
421
+ container.parentNode.insertBefore(banner, container);
422
422
  const btn = banner.querySelector('.cxp-cli-banner-recheck');
423
423
  btn?.addEventListener('click', async () => {
424
424
  btn.disabled = true;
@@ -437,7 +437,7 @@ function ensureCliBannerEl() {
437
437
  function removeCliBannerEl() {
438
438
  if (!_panel) return;
439
439
  const container = panelEl('#cxp-messages-container');
440
- const banner = container?.querySelector(':scope > .cxp-cli-banner');
440
+ const banner = container?.parentNode?.querySelector(':scope > .cxp-cli-banner');
441
441
  banner?.remove();
442
442
  }
443
443
 
@@ -990,7 +990,7 @@ export function injectStyles() {
990
990
  grid-template-columns: auto 1fr auto;
991
991
  gap: 10px;
992
992
  align-items: center;
993
- margin: 8px 12px 0;
993
+ margin: 6px 8px 0;
994
994
  padding: 10px 12px;
995
995
  border: 1px solid rgba(255,180,80,0.45);
996
996
  border-radius: 8px;
@@ -1526,8 +1526,8 @@ let _cliInstallFailureForced = false;
1526
1526
  function ensureCliBannerEl() {
1527
1527
  if (!_panel) return null;
1528
1528
  const container = _panel.querySelector('.ocp-messages-container');
1529
- if (!container) return null;
1530
- let banner = container.querySelector(':scope > .ocp-cli-banner');
1529
+ if (!container || !container.parentNode) return null;
1530
+ let banner = container.parentNode.querySelector(':scope > .ocp-cli-banner');
1531
1531
  if (banner) return banner;
1532
1532
  const label = getCliLabel('opencode');
1533
1533
  const cmd = getCliInstallCommand('opencode');
@@ -1545,7 +1545,7 @@ function ensureCliBannerEl() {
1545
1545
  <button class="ocp-cli-banner-recheck" type="button">Re-check</button>
1546
1546
  </div>
1547
1547
  `;
1548
- container.insertBefore(banner, container.firstChild);
1548
+ container.parentNode.insertBefore(banner, container);
1549
1549
  const btn = banner.querySelector('.ocp-cli-banner-recheck');
1550
1550
  btn?.addEventListener('click', async () => {
1551
1551
  btn.disabled = true;
@@ -1563,7 +1563,8 @@ function ensureCliBannerEl() {
1563
1563
 
1564
1564
  function removeCliBannerEl() {
1565
1565
  if (!_panel) return;
1566
- const banner = _panel.querySelector('.ocp-messages-container > .ocp-cli-banner');
1566
+ const container = _panel.querySelector('.ocp-messages-container');
1567
+ const banner = container?.parentNode?.querySelector(':scope > .ocp-cli-banner');
1567
1568
  banner?.remove();
1568
1569
  }
1569
1570
 
@@ -1527,7 +1527,7 @@ export function injectStyles() {
1527
1527
  grid-template-columns: auto 1fr auto;
1528
1528
  gap: 10px;
1529
1529
  align-items: center;
1530
- margin: 8px 12px 0;
1530
+ margin: 6px 8px 0;
1531
1531
  padding: 10px 12px;
1532
1532
  border: 1px solid rgba(255,180,80,0.45);
1533
1533
  border-radius: 8px;
@@ -5094,15 +5094,51 @@
5094
5094
  }
5095
5095
  .update-backup-btn:hover { background: rgba(52,199,89,0.16); }
5096
5096
  .update-backup-btn:disabled { opacity: 0.5; pointer-events: none; }
5097
- .update-backup-status {
5098
- display: flex;
5099
- align-items: center;
5100
- gap: 8px;
5097
+ .update-backup-status {
5098
+ display: flex;
5099
+ align-items: center;
5100
+ gap: 8px;
5101
5101
  font-size: 11px;
5102
5102
  color: var(--t-muted);
5103
- padding: 4px 0;
5104
- }
5105
- .update-backup-icon { font-size: 14px; font-weight: 700; }
5103
+ padding: 4px 0;
5104
+ }
5105
+ .update-run-status {
5106
+ align-items: flex-start;
5107
+ gap: 10px;
5108
+ padding: 10px 12px;
5109
+ border-radius: var(--r-card);
5110
+ border: 1px solid rgba(255,255,255,0.08);
5111
+ background: rgba(255,255,255,0.045);
5112
+ color: var(--t-secondary);
5113
+ font-size: 12px;
5114
+ line-height: 1.4;
5115
+ }
5116
+ .update-run-status .update-backup-icon {
5117
+ flex: 0 0 auto;
5118
+ margin-top: 1px;
5119
+ }
5120
+ .update-run-status strong {
5121
+ display: block;
5122
+ color: var(--t-primary);
5123
+ font-size: 12px;
5124
+ font-weight: 600;
5125
+ margin-bottom: 2px;
5126
+ }
5127
+ .update-run-detail,
5128
+ .update-run-hint {
5129
+ display: block;
5130
+ color: var(--t-muted);
5131
+ }
5132
+ .update-run-hint {
5133
+ margin-top: 5px;
5134
+ opacity: .82;
5135
+ }
5136
+ .update-run-status code {
5137
+ font-family: var(--ff-mono);
5138
+ font-size: 11px;
5139
+ color: var(--t-secondary);
5140
+ }
5141
+ .update-backup-icon { font-size: 14px; font-weight: 700; }
5106
5142
  .update-backup-icon.spin { animation: updateSpin 1s linear infinite; }
5107
5143
  @keyframes updateSpin {
5108
5144
  from { transform: rotate(0deg); }
@@ -439,7 +439,7 @@ function injectStyles() {
439
439
  grid-template-columns: auto 1fr auto;
440
440
  gap: 10px;
441
441
  align-items: center;
442
- margin: 8px 12px 0;
442
+ margin: 6px 8px 0;
443
443
  padding: 10px 12px;
444
444
  border: 1px solid rgba(255,180,80,0.45);
445
445
  border-radius: 8px;
@@ -7331,8 +7331,8 @@ let _cliInstallFailureForced = false;
7331
7331
  function _ensureCliBannerEl() {
7332
7332
  if (!_panel) return null;
7333
7333
  const container = _panel.querySelector('#cp-messages-container');
7334
- if (!container) return null;
7335
- let banner = container.querySelector(':scope > .cp-cli-banner');
7334
+ if (!container || !container.parentNode) return null;
7335
+ let banner = container.parentNode.querySelector(':scope > .cp-cli-banner');
7336
7336
  if (banner) return banner;
7337
7337
  const label = getCliLabel('claude-code');
7338
7338
  const cmd = getCliInstallCommand('claude-code');
@@ -7350,7 +7350,7 @@ function _ensureCliBannerEl() {
7350
7350
  <button class="cp-cli-banner-recheck" type="button">Re-check</button>
7351
7351
  </div>
7352
7352
  `;
7353
- container.insertBefore(banner, container.firstChild);
7353
+ container.parentNode.insertBefore(banner, container);
7354
7354
  const btn = banner.querySelector('.cp-cli-banner-recheck');
7355
7355
  btn?.addEventListener('click', async () => {
7356
7356
  btn.disabled = true;
@@ -7368,7 +7368,8 @@ function _ensureCliBannerEl() {
7368
7368
 
7369
7369
  function _removeCliBannerEl() {
7370
7370
  if (!_panel) return;
7371
- const banner = _panel.querySelector('#cp-messages-container > .cp-cli-banner');
7371
+ const container = _panel.querySelector('#cp-messages-container');
7372
+ const banner = container?.parentNode?.querySelector(':scope > .cp-cli-banner');
7372
7373
  banner?.remove();
7373
7374
  }
7374
7375
 
@@ -128,12 +128,21 @@ export function initMenubar() {
128
128
  'menu-open-skills-studio': 'skills',
129
129
  'menu-skills-new': 'skills',
130
130
  'menu-skills-import': 'skills',
131
+ <<<<<<< HEAD
131
132
  'menu-open-automation-studio': 'automations',
132
133
  'menu-automations-schedule-new': 'automations',
133
134
  'menu-automations-quick-timer': 'automations',
134
135
  'menu-automations-schedule-groups': 'automations',
135
136
  'menu-automations-new': 'automations',
136
137
  'menu-automations-import': 'automations',
138
+ =======
139
+ 'menu-open-automation-studio': 'automations',
140
+ 'menu-automations-schedule-new': 'automations',
141
+ 'menu-automations-quick-timer': 'automations',
142
+ 'menu-automations-schedule-groups': 'automations',
143
+ 'menu-automations-new': 'automations',
144
+ 'menu-automations-import': 'automations',
145
+ >>>>>>> Clean
137
146
  'menu-terminal-claude': 'terminal',
138
147
  'menu-terminal-codex': 'terminal',
139
148
  'menu-terminal-gemini': 'terminal',
@@ -317,6 +326,7 @@ function wireAutomationsMenu() {
317
326
  emit('automations:open');
318
327
  });
319
328
  }
329
+ <<<<<<< HEAD
320
330
  const schedulesItem = $('menu-automations-schedules');
321
331
  if (schedulesItem) {
322
332
  schedulesItem.addEventListener('click', () => {
@@ -345,6 +355,36 @@ function wireAutomationsMenu() {
345
355
  emit('automations:open-schedules', { tab: 'groups' });
346
356
  });
347
357
  }
358
+ =======
359
+ const schedulesItem = $('menu-automations-schedules');
360
+ if (schedulesItem) {
361
+ schedulesItem.addEventListener('click', () => {
362
+ closeAll();
363
+ emit('automations:open-schedules');
364
+ });
365
+ }
366
+ const newScheduleItem = $('menu-automations-schedule-new');
367
+ if (newScheduleItem) {
368
+ newScheduleItem.addEventListener('click', () => {
369
+ closeAll();
370
+ emit('automations:open-schedules', { action: 'new-schedule' });
371
+ });
372
+ }
373
+ const quickTimerItem = $('menu-automations-quick-timer');
374
+ if (quickTimerItem) {
375
+ quickTimerItem.addEventListener('click', () => {
376
+ closeAll();
377
+ emit('automations:open-schedules', { action: 'quick-timer' });
378
+ });
379
+ }
380
+ const scheduleGroupsItem = $('menu-automations-schedule-groups');
381
+ if (scheduleGroupsItem) {
382
+ scheduleGroupsItem.addEventListener('click', () => {
383
+ closeAll();
384
+ emit('automations:open-schedules', { tab: 'groups' });
385
+ });
386
+ }
387
+ >>>>>>> Clean
348
388
  const newItem = $('menu-automations-new');
349
389
  if (newItem) {
350
390
  newItem.addEventListener('click', () => {
@@ -4,10 +4,25 @@
4
4
  // and provides a 3-step modal wizard with blocking backup gate.
5
5
  // ═══════════════════════════════════════════
6
6
 
7
+ import { showUpdateToast } from './ui-tool-updates.js';
8
+
7
9
  const $ = (id) => document.getElementById(id);
10
+ const SYNABUN_GITHUB_URL = 'https://github.com/danilokhury/Synabun';
8
11
 
9
12
  let _versionData = null;
10
13
  let _btnWired = false;
14
+ <<<<<<< HEAD
15
+ =======
16
+ let _pollWired = false;
17
+
18
+ function sourceLabel(data) {
19
+ const src = data?.source;
20
+ if (src === 'both') return 'npm + GitHub';
21
+ if (src === 'github') return 'GitHub';
22
+ if (src === 'npm') return 'npm';
23
+ return src || 'update';
24
+ }
25
+ >>>>>>> Clean
11
26
 
12
27
  function applyTopRightButton(data) {
13
28
  const btn = $('topright-update-btn');
@@ -17,7 +32,11 @@ function applyTopRightButton(data) {
17
32
  btn.style.display = has ? '' : 'none';
18
33
  if (badge) badge.textContent = has ? '!' : '';
19
34
  if (has) {
35
+ <<<<<<< HEAD
20
36
  const srcLabel = data.source === 'both' ? 'npm + GitHub' : data.source === 'github' ? 'GitHub' : 'npm';
37
+ =======
38
+ const srcLabel = sourceLabel(data);
39
+ >>>>>>> Clean
21
40
  btn.dataset.tooltip = `Update: v${data.current} → v${data.latest} (${srcLabel})`;
22
41
  }
23
42
  if (!_btnWired) {
@@ -28,6 +47,26 @@ function applyTopRightButton(data) {
28
47
  });
29
48
  }
30
49
  }
50
+ <<<<<<< HEAD
51
+ =======
52
+
53
+ function maybeShowUpdateAlert(data, { once = true } = {}) {
54
+ if (!data?.updateAvailable) return;
55
+ const key = `synabun-update-alert:${data.current}->${data.latest}:${sourceLabel(data)}`;
56
+ try {
57
+ if (once && sessionStorage.getItem(key) === '1') return;
58
+ sessionStorage.setItem(key, '1');
59
+ } catch {}
60
+
61
+ setTimeout(() => {
62
+ showUpdateToast({
63
+ updates: [{ label: `SynaBun v${data.current} → v${data.latest}`, source: sourceLabel(data) }],
64
+ errors: [],
65
+ onClick: () => openSynabunUpdateModal(),
66
+ });
67
+ }, 500);
68
+ }
69
+ >>>>>>> Clean
31
70
 
32
71
  export async function initUpdate() {
33
72
  try {
@@ -35,18 +74,37 @@ export async function initUpdate() {
35
74
  if (!res.ok) return;
36
75
  _versionData = await res.json();
37
76
  applyTopRightButton(_versionData);
77
+ <<<<<<< HEAD
78
+ =======
79
+ maybeShowUpdateAlert(_versionData);
80
+
81
+ if (!_pollWired) {
82
+ _pollWired = true;
83
+ setInterval(() => {
84
+ forceCheckSynabunUpdate({ alert: true }).catch(() => {});
85
+ }, 30 * 60 * 1000);
86
+ }
87
+ >>>>>>> Clean
38
88
  } catch { /* silent — no update UI if check fails */ }
39
89
  }
40
90
 
41
91
  // Force a fresh server-side check (bypasses 1h cache via ?force=1) and
42
92
  // re-applies the top-right button + badge state. Returns the latest
43
93
  // _synabunUpdateCache shape so callers can compose toasts.
94
+ <<<<<<< HEAD
44
95
  export async function forceCheckSynabunUpdate() {
96
+ =======
97
+ export async function forceCheckSynabunUpdate({ alert = false } = {}) {
98
+ >>>>>>> Clean
45
99
  try {
46
100
  const res = await fetch('/api/system/version?force=1');
47
101
  if (!res.ok) return null;
48
102
  _versionData = await res.json();
49
103
  applyTopRightButton(_versionData);
104
+ <<<<<<< HEAD
105
+ =======
106
+ if (alert) maybeShowUpdateAlert(_versionData);
107
+ >>>>>>> Clean
50
108
  return _versionData;
51
109
  } catch {
52
110
  return null;
@@ -149,6 +207,7 @@ function openUpdateModal() {
149
207
  }
150
208
 
151
209
  function renderStep3() {
210
+ <<<<<<< HEAD
152
211
  // Channel-aware install command. Beta users staying on beta keep the
153
212
  // @beta tag; both targets exist on stable so we surface a single line.
154
213
  const channel = _versionData.installedChannel || 'stable';
@@ -156,11 +215,41 @@ function openUpdateModal() {
156
215
  const targetLooksBeta = parseSemverPre(target);
157
216
  const installTag = (channel === 'prerelease' && targetLooksBeta) ? 'beta' : 'latest';
158
217
  const installCmd = `npm install -g synabun@${installTag}`;
218
+ =======
219
+ const installPlan = _versionData.installPlan || {};
220
+ const canAutoUpdate = installPlan.canAutoUpdate === true;
221
+ const repoUrl = installPlan.openUrl || SYNABUN_GITHUB_URL;
222
+ const installCmd = canAutoUpdate
223
+ ? (installPlan.displayCommand || 'npm i -g synabun@latest')
224
+ : repoUrl;
225
+ const installIntro = canAutoUpdate
226
+ ? 'SynaBun will close and run this command in a new terminal window:'
227
+ : 'This SynaBun install was not detected as an npm install. Open the GitHub repository to update from your checkout or installer:';
228
+ const title = canAutoUpdate ? 'Run the Update' : 'Update from GitHub';
229
+ const installSource = canAutoUpdate
230
+ ? 'Installing from npm.'
231
+ : (installPlan.manualHint || 'Open the SynaBun repository for the correct update path.');
232
+ const autoRestartControl = canAutoUpdate ? `
233
+ <p style="font-size:11px;color:var(--t-muted);opacity:.75;margin:10px 0 0">
234
+ SynaBun will relaunch automatically after the update finishes.
235
+ </p>` : '';
236
+ const manualButton = canAutoUpdate ? `
237
+ <button class="action-btn action-btn--ghost" id="update-manual" title="Run the command yourself in your own terminal">Manual</button>` : '';
238
+ const updateNowButton = canAutoUpdate ? `
239
+ <button class="action-btn action-btn--primary" id="update-run-now">
240
+ <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:6px"><polyline points="8 17 12 21 16 17"/><line x1="12" y1="12" x2="12" y2="21"/><path d="M20.88 18.09A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.29"/></svg>
241
+ Update Now
242
+ </button>` : `
243
+ <button class="action-btn action-btn--primary" id="update-run-now" data-url="${esc(repoUrl)}">
244
+ <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:6px"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>
245
+ Open GitHub
246
+ </button>`;
247
+ >>>>>>> Clean
159
248
 
160
249
  return `
161
- <h3 class="update-modal-title">Run the Update</h3>
250
+ <h3 class="update-modal-title">${title}</h3>
162
251
  <p style="font-size:12px;color:var(--t-muted);margin:4px 0 12px">
163
- Run this command in your terminal to update SynaBun:
252
+ ${esc(installIntro)}
164
253
  </p>
165
254
  <div class="update-code-block">
166
255
  <code>${esc(installCmd)}</code>
@@ -168,19 +257,23 @@ function openUpdateModal() {
168
257
  <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
169
258
  </button>
170
259
  </div>
171
- <p style="font-size:12px;color:var(--t-muted);margin:12px 0 8px">
172
- Then restart the Neural Interface:
173
- </p>
174
- <div class="update-code-block">
175
- <code>node neural-interface/server.js</code>
176
- <button class="update-copy-btn" data-copy="node neural-interface/server.js" title="Copy">
177
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
178
- </button>
260
+ <p style="font-size:11px;color:var(--t-muted);opacity:.75;margin:8px 0 0">${esc(installSource)}</p>
261
+ ${autoRestartControl}
262
+
263
+ <div class="update-run-status" id="update-run-status" style="display:none;margin:10px 0 0">
264
+ <span class="update-backup-icon" id="update-run-icon"></span>
265
+ <span id="update-run-text"></span>
179
266
  </div>
180
- <div class="tag-delete-modal-actions" style="margin-top:18px">
267
+
268
+ <div class="tag-delete-modal-actions" style="margin-top:14px;flex-wrap:wrap;gap:8px">
181
269
  <button class="action-btn action-btn--ghost" id="update-back">Back</button>
182
- <button class="action-btn action-btn--primary" id="update-done">Done</button>
183
- </div>`;
270
+ ${manualButton}
271
+ ${updateNowButton}
272
+ </div>
273
+
274
+ <p id="update-manual-note" style="display:none;font-size:11px;color:var(--t-muted);opacity:.75;margin:10px 0 0">
275
+ After running the command, restart SynaBun with <code>synabun</code>.
276
+ </p>`;
184
277
  }
185
278
 
186
279
  function bindEvents() {
@@ -221,6 +314,16 @@ function openUpdateModal() {
221
314
  // Backup download
222
315
  overlay.querySelector('#update-backup-download')?.addEventListener('click', handleBackupDownload);
223
316
 
317
+ // Update Now — kick off click-to-update flow
318
+ overlay.querySelector('#update-run-now')?.addEventListener('click', handleRunUpdate);
319
+
320
+ // Manual — surface the command + relaunch hint, hide auto-update controls
321
+ overlay.querySelector('#update-manual')?.addEventListener('click', () => {
322
+ overlay.querySelector('#update-manual-note').style.display = '';
323
+ const runNow = overlay.querySelector('#update-run-now');
324
+ if (runNow) runNow.style.display = 'none';
325
+ });
326
+
224
327
  // Skip checkbox
225
328
  const skipCb = overlay.querySelector('#update-skip-backup');
226
329
  if (skipCb) {
@@ -232,6 +335,78 @@ function openUpdateModal() {
232
335
  }
233
336
  }
234
337
 
338
+ async function handleRunUpdate() {
339
+ const installPlan = _versionData?.installPlan || {};
340
+ const canAutoUpdate = installPlan.canAutoUpdate === true;
341
+ const repoUrl = installPlan.openUrl || SYNABUN_GITHUB_URL;
342
+
343
+ // Auto-restart is always on; server forces it true regardless of payload.
344
+ const autoRestart = true;
345
+
346
+ const runBtn = overlay.querySelector('#update-run-now');
347
+ const backBtn = overlay.querySelector('#update-back');
348
+ const manualBtn = overlay.querySelector('#update-manual');
349
+ const status = overlay.querySelector('#update-run-status');
350
+ const icon = overlay.querySelector('#update-run-icon');
351
+ const text = overlay.querySelector('#update-run-text');
352
+
353
+ if (!runBtn || !backBtn || !status || !icon || !text) return;
354
+
355
+ if (!canAutoUpdate) {
356
+ window.open(repoUrl, '_blank', 'noopener,noreferrer');
357
+ status.style.display = 'flex';
358
+ icon.className = 'update-backup-icon';
359
+ icon.textContent = '↗';
360
+ icon.style.color = '#4fc3f7';
361
+ text.innerHTML = `<strong>GitHub repository opened</strong><span class="update-run-detail">Use the repository instructions to update this non-npm installation.</span>`;
362
+ return;
363
+ }
364
+
365
+ runBtn.disabled = true;
366
+ backBtn.disabled = true;
367
+ if (manualBtn) manualBtn.disabled = true;
368
+ runBtn.innerHTML = '<span class="update-spinner"></span> Launching updater...';
369
+ status.style.display = 'flex';
370
+ icon.className = 'update-backup-icon spin';
371
+ icon.textContent = '⟳';
372
+ icon.style.color = '';
373
+ text.innerHTML = '<strong>Launching updater</strong><span class="update-run-detail">Opening a terminal window for the npm update.</span>';
374
+
375
+ try {
376
+ const res = await fetch('/api/system/run-update', {
377
+ method: 'POST',
378
+ headers: { 'Content-Type': 'application/json' },
379
+ body: JSON.stringify({ autoRestart }),
380
+ });
381
+ if (!res.ok) {
382
+ let msg = `Updater failed (${res.status})`;
383
+ try { const b = await res.json(); msg = b.error || msg; } catch {}
384
+ throw new Error(msg);
385
+ }
386
+ await res.json();
387
+
388
+ icon.className = 'update-backup-icon';
389
+ icon.textContent = '✓';
390
+ icon.style.color = '#34c759';
391
+ text.innerHTML = `<strong>Updater terminal launched</strong><span class="update-run-detail">SynaBun is shutting down so npm can replace the package. Watch the terminal for install progress; SynaBun will relaunch automatically when it finishes.</span><span class="update-run-hint">If no terminal window appears, run <code>${esc(installPlan.displayCommand || 'npm i -g synabun@latest')}</code> manually.</span>`;
392
+ runBtn.innerHTML = 'Updater running…';
393
+
394
+ // Server is shutting down. Optionally redirect to a friendly offline
395
+ // notice — for now leave the page; user already understands from copy.
396
+ // Re-enable Back so they can close the modal.
397
+ setTimeout(() => { backBtn.disabled = false; }, 2500);
398
+ } catch (err) {
399
+ icon.className = 'update-backup-icon';
400
+ icon.textContent = '✗';
401
+ icon.style.color = 'var(--accent-red)';
402
+ text.textContent = 'Failed to launch updater: ' + err.message;
403
+ runBtn.disabled = false;
404
+ backBtn.disabled = false;
405
+ if (manualBtn) manualBtn.disabled = false;
406
+ runBtn.innerHTML = `<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:6px"><polyline points="8 17 12 21 16 17"/><line x1="12" y1="12" x2="12" y2="21"/><path d="M20.88 18.09A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.29"/></svg> Retry`;
407
+ }
408
+ }
409
+
235
410
  async function handleBackupDownload() {
236
411
  const btn = overlay.querySelector('#update-backup-download');
237
412
  const status = overlay.querySelector('#update-backup-status');