synabun 2026.4.26 → 2026.4.27
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.
- package/CHANGELOG.md +11 -126
- package/neural-interface/public/shared/cdx/cdx-panel.js +4 -4
- package/neural-interface/public/shared/cdx/cdx-styles.js +1 -1
- package/neural-interface/public/shared/ocp/ocp-panel.js +5 -4
- package/neural-interface/public/shared/ocp/ocp-styles.js +1 -1
- package/neural-interface/public/shared/styles.css +257 -151
- package/neural-interface/public/shared/ui-claude-panel.js +5 -5
- package/neural-interface/public/shared/ui-menubar.js +64 -36
- package/neural-interface/public/shared/ui-tool-updates.js +77 -3
- package/neural-interface/public/shared/ui-update.js +234 -32
- package/neural-interface/server.js +926 -74
- package/package.json +4 -1
- package/postinstall.js +20 -0
- package/preuninstall.js +140 -6
- package/updater.cmd +31 -0
- package/updater.mjs +245 -0
- package/updater.sh +29 -0
|
@@ -174,7 +174,81 @@ export async function initToolUpdates() {
|
|
|
174
174
|
export async function forceCheckToolUpdates() {
|
|
175
175
|
try {
|
|
176
176
|
const res = await fetch('/api/system/tool-versions?force=1');
|
|
177
|
-
if (!res.ok) return;
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
if (!res.ok) return null;
|
|
178
|
+
const data = await res.json();
|
|
179
|
+
applyBadges(data);
|
|
180
|
+
return data;
|
|
181
|
+
} catch { return null; }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Re-export human-readable tool labels so toast/menu code can build summaries
|
|
185
|
+
// without redefining the label map.
|
|
186
|
+
export { TOOL_LABELS };
|
|
187
|
+
|
|
188
|
+
// ── Update-check toast ──
|
|
189
|
+
// Bottom-right ephemeral pill. One of three modes: updates available (with
|
|
190
|
+
// list + optional click handler), all-clear, or check-failed. Auto-dismisses.
|
|
191
|
+
|
|
192
|
+
function _toastEsc(s) {
|
|
193
|
+
const d = document.createElement('div');
|
|
194
|
+
d.textContent = String(s ?? '');
|
|
195
|
+
return d.innerHTML;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function showUpdateToast({ updates, errors, onClick } = {}) {
|
|
199
|
+
document.querySelectorAll('.update-check-toast').forEach(el => el.remove());
|
|
200
|
+
|
|
201
|
+
const toast = document.createElement('div');
|
|
202
|
+
toast.className = 'update-check-toast';
|
|
203
|
+
|
|
204
|
+
const hasUpdates = Array.isArray(updates) && updates.length > 0;
|
|
205
|
+
const hasErrors = Array.isArray(errors) && errors.length > 0;
|
|
206
|
+
const clickable = hasUpdates && typeof onClick === 'function';
|
|
207
|
+
if (clickable) toast.classList.add('update-check-toast--clickable');
|
|
208
|
+
|
|
209
|
+
if (hasUpdates) {
|
|
210
|
+
const items = updates.map(u => {
|
|
211
|
+
const label = _toastEsc(u?.label || '');
|
|
212
|
+
const src = u?.source ? `<span class="utc-src">${_toastEsc(u.source)}</span>` : '';
|
|
213
|
+
return `<li>${label}${src}</li>`;
|
|
214
|
+
}).join('');
|
|
215
|
+
toast.innerHTML = `
|
|
216
|
+
<div class="utc-icon utc-icon--up">↑</div>
|
|
217
|
+
<div class="utc-body">
|
|
218
|
+
<div class="utc-title">Updates available</div>
|
|
219
|
+
<ul class="utc-list">${items}</ul>
|
|
220
|
+
${clickable ? '<div class="utc-hint">Click to open update guide</div>' : ''}
|
|
221
|
+
</div>`;
|
|
222
|
+
} else if (hasErrors) {
|
|
223
|
+
toast.innerHTML = `
|
|
224
|
+
<div class="utc-icon utc-icon--err">!</div>
|
|
225
|
+
<div class="utc-body">
|
|
226
|
+
<div class="utc-title">Update check failed</div>
|
|
227
|
+
<div class="utc-sub">${_toastEsc(errors.join(' · '))}</div>
|
|
228
|
+
</div>`;
|
|
229
|
+
} else {
|
|
230
|
+
toast.innerHTML = `
|
|
231
|
+
<div class="utc-icon utc-icon--ok">✓</div>
|
|
232
|
+
<div class="utc-body">
|
|
233
|
+
<div class="utc-title">All up to date</div>
|
|
234
|
+
</div>`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (clickable) {
|
|
238
|
+
toast.addEventListener('click', () => {
|
|
239
|
+
try { onClick(); } catch {}
|
|
240
|
+
toast.classList.remove('show');
|
|
241
|
+
setTimeout(() => toast.remove(), 250);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
document.body.appendChild(toast);
|
|
246
|
+
requestAnimationFrame(() => toast.classList.add('show'));
|
|
247
|
+
|
|
248
|
+
const ttl = hasUpdates ? 7000 : hasErrors ? 5000 : 3500;
|
|
249
|
+
setTimeout(() => {
|
|
250
|
+
if (!toast.isConnected) return;
|
|
251
|
+
toast.classList.remove('show');
|
|
252
|
+
setTimeout(() => toast.remove(), 250);
|
|
253
|
+
}, ttl);
|
|
180
254
|
}
|
|
@@ -4,30 +4,96 @@
|
|
|
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;
|
|
13
|
+
let _btnWired = false;
|
|
14
|
+
let _pollWired = false;
|
|
15
|
+
|
|
16
|
+
function sourceLabel(data) {
|
|
17
|
+
const src = data?.source;
|
|
18
|
+
if (src === 'both') return 'npm + GitHub';
|
|
19
|
+
if (src === 'github') return 'GitHub';
|
|
20
|
+
if (src === 'npm') return 'npm';
|
|
21
|
+
return src || 'update';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function applyTopRightButton(data) {
|
|
25
|
+
const btn = $('topright-update-btn');
|
|
26
|
+
const badge = $('update-badge');
|
|
27
|
+
if (!btn) return;
|
|
28
|
+
const has = !!data?.updateAvailable;
|
|
29
|
+
btn.style.display = has ? '' : 'none';
|
|
30
|
+
if (badge) badge.textContent = has ? '!' : '';
|
|
31
|
+
if (has) {
|
|
32
|
+
const srcLabel = sourceLabel(data);
|
|
33
|
+
btn.dataset.tooltip = `Update: v${data.current} → v${data.latest} (${srcLabel})`;
|
|
34
|
+
}
|
|
35
|
+
if (!_btnWired) {
|
|
36
|
+
_btnWired = true;
|
|
37
|
+
btn.addEventListener('click', (e) => {
|
|
38
|
+
e.stopPropagation();
|
|
39
|
+
openUpdateModal();
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function maybeShowUpdateAlert(data, { once = true } = {}) {
|
|
45
|
+
if (!data?.updateAvailable) return;
|
|
46
|
+
const key = `synabun-update-alert:${data.current}->${data.latest}:${sourceLabel(data)}`;
|
|
47
|
+
try {
|
|
48
|
+
if (once && sessionStorage.getItem(key) === '1') return;
|
|
49
|
+
sessionStorage.setItem(key, '1');
|
|
50
|
+
} catch {}
|
|
51
|
+
|
|
52
|
+
setTimeout(() => {
|
|
53
|
+
showUpdateToast({
|
|
54
|
+
updates: [{ label: `SynaBun v${data.current} → v${data.latest}`, source: sourceLabel(data) }],
|
|
55
|
+
errors: [],
|
|
56
|
+
onClick: () => openSynabunUpdateModal(),
|
|
57
|
+
});
|
|
58
|
+
}, 500);
|
|
59
|
+
}
|
|
10
60
|
|
|
11
61
|
export async function initUpdate() {
|
|
12
62
|
try {
|
|
13
63
|
const res = await fetch('/api/system/version');
|
|
14
64
|
if (!res.ok) return;
|
|
15
65
|
_versionData = await res.json();
|
|
66
|
+
applyTopRightButton(_versionData);
|
|
67
|
+
maybeShowUpdateAlert(_versionData);
|
|
68
|
+
|
|
69
|
+
if (!_pollWired) {
|
|
70
|
+
_pollWired = true;
|
|
71
|
+
setInterval(() => {
|
|
72
|
+
forceCheckSynabunUpdate({ alert: true }).catch(() => {});
|
|
73
|
+
}, 30 * 60 * 1000);
|
|
74
|
+
}
|
|
75
|
+
} catch { /* silent — no update UI if check fails */ }
|
|
76
|
+
}
|
|
16
77
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
78
|
+
// Force a fresh server-side check (bypasses 1h cache via ?force=1) and
|
|
79
|
+
// re-applies the top-right button + badge state. Returns the latest
|
|
80
|
+
// _synabunUpdateCache shape so callers can compose toasts.
|
|
81
|
+
export async function forceCheckSynabunUpdate({ alert = false } = {}) {
|
|
82
|
+
try {
|
|
83
|
+
const res = await fetch('/api/system/version?force=1');
|
|
84
|
+
if (!res.ok) return null;
|
|
85
|
+
_versionData = await res.json();
|
|
86
|
+
applyTopRightButton(_versionData);
|
|
87
|
+
if (alert) maybeShowUpdateAlert(_versionData);
|
|
88
|
+
return _versionData;
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
25
93
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
});
|
|
30
|
-
} catch { /* silent — no update UI if check fails */ }
|
|
94
|
+
export function openSynabunUpdateModal() {
|
|
95
|
+
if (!_versionData?.updateAvailable) return;
|
|
96
|
+
openUpdateModal();
|
|
31
97
|
}
|
|
32
98
|
|
|
33
99
|
// ── Modal Wizard ──
|
|
@@ -59,16 +125,37 @@ function openUpdateModal() {
|
|
|
59
125
|
}
|
|
60
126
|
|
|
61
127
|
function renderStep1() {
|
|
128
|
+
const src = _versionData.source;
|
|
129
|
+
const channel = _versionData.installedChannel || 'stable';
|
|
130
|
+
const channelTag = channel === 'prerelease'
|
|
131
|
+
? '<span class="update-channel-pill update-channel-pill--beta">beta</span>'
|
|
132
|
+
: '';
|
|
133
|
+
|
|
134
|
+
const srcCopy = src === 'both'
|
|
135
|
+
? 'A new version of SynaBun is available on npm and GitHub.'
|
|
136
|
+
: src === 'github'
|
|
137
|
+
? 'A new SynaBun release is tagged on GitHub. The npm publish may still be in progress.'
|
|
138
|
+
: 'A new version of SynaBun has been published to npm.';
|
|
139
|
+
|
|
140
|
+
const npmStable = _versionData.npmLatestStable
|
|
141
|
+
? `npm latest v${esc(_versionData.npmLatestStable)}`
|
|
142
|
+
: 'npm latest —';
|
|
143
|
+
const npmBeta = _versionData.npmLatestBeta
|
|
144
|
+
? ` · npm beta v${esc(_versionData.npmLatestBeta)}`
|
|
145
|
+
: '';
|
|
146
|
+
const gitBit = _versionData.gitLatest
|
|
147
|
+
? ` · GitHub v${esc(_versionData.gitLatest)}`
|
|
148
|
+
: ' · GitHub —';
|
|
149
|
+
|
|
62
150
|
return `
|
|
63
|
-
<h3 class="update-modal-title">Update Available</h3>
|
|
151
|
+
<h3 class="update-modal-title">Update Available ${channelTag}</h3>
|
|
64
152
|
<div class="update-version-diff">
|
|
65
153
|
<span class="update-ver update-ver--old">v${esc(_versionData.current)}</span>
|
|
66
154
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
|
|
67
155
|
<span class="update-ver update-ver--new">v${esc(_versionData.latest)}</span>
|
|
68
156
|
</div>
|
|
69
|
-
<p style="font-size:12px;color:var(--t-muted);margin:12px 0
|
|
70
|
-
|
|
71
|
-
</p>
|
|
157
|
+
<p style="font-size:12px;color:var(--t-muted);margin:12px 0 6px">${srcCopy}</p>
|
|
158
|
+
<p style="font-size:11px;color:var(--t-muted);opacity:.75;margin:0 0 18px">${npmStable}${npmBeta}${gitBit}</p>
|
|
72
159
|
<div class="tag-delete-modal-actions">
|
|
73
160
|
<button class="action-btn action-btn--ghost" id="update-cancel">Cancel</button>
|
|
74
161
|
<button class="action-btn action-btn--primary" id="update-next">Next</button>
|
|
@@ -100,30 +187,63 @@ function openUpdateModal() {
|
|
|
100
187
|
}
|
|
101
188
|
|
|
102
189
|
function renderStep3() {
|
|
190
|
+
const installPlan = _versionData.installPlan || {};
|
|
191
|
+
const canAutoUpdate = installPlan.canAutoUpdate === true;
|
|
192
|
+
const repoUrl = installPlan.openUrl || SYNABUN_GITHUB_URL;
|
|
193
|
+
const installCmd = canAutoUpdate
|
|
194
|
+
? (installPlan.displayCommand || 'npm i -g synabun@latest')
|
|
195
|
+
: repoUrl;
|
|
196
|
+
const installIntro = canAutoUpdate
|
|
197
|
+
? 'SynaBun will close and run this command in a new terminal window:'
|
|
198
|
+
: 'This SynaBun install was not detected as an npm install. Open the GitHub repository to update from your checkout or installer:';
|
|
199
|
+
const title = canAutoUpdate ? 'Run the Update' : 'Update from GitHub';
|
|
200
|
+
const installSource = canAutoUpdate
|
|
201
|
+
? 'Installing from npm.'
|
|
202
|
+
: (installPlan.manualHint || 'Open the SynaBun repository for the correct update path.');
|
|
203
|
+
const autoRestartControl = canAutoUpdate ? `
|
|
204
|
+
<p style="font-size:11px;color:var(--t-muted);opacity:.75;margin:10px 0 0">
|
|
205
|
+
SynaBun will relaunch automatically after the update finishes.
|
|
206
|
+
</p>` : '';
|
|
207
|
+
const manualButton = canAutoUpdate ? `
|
|
208
|
+
<button class="action-btn action-btn--ghost" id="update-manual" title="Run the command yourself in your own terminal">Manual</button>` : '';
|
|
209
|
+
const updateNowButton = canAutoUpdate ? `
|
|
210
|
+
<button class="action-btn action-btn--primary" id="update-run-now">
|
|
211
|
+
<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>
|
|
212
|
+
Update Now
|
|
213
|
+
</button>` : `
|
|
214
|
+
<button class="action-btn action-btn--primary" id="update-run-now" data-url="${esc(repoUrl)}">
|
|
215
|
+
<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>
|
|
216
|
+
Open GitHub
|
|
217
|
+
</button>`;
|
|
218
|
+
|
|
103
219
|
return `
|
|
104
|
-
<h3 class="update-modal-title"
|
|
220
|
+
<h3 class="update-modal-title">${title}</h3>
|
|
105
221
|
<p style="font-size:12px;color:var(--t-muted);margin:4px 0 12px">
|
|
106
|
-
|
|
222
|
+
${esc(installIntro)}
|
|
107
223
|
</p>
|
|
108
224
|
<div class="update-code-block">
|
|
109
|
-
<code
|
|
110
|
-
<button class="update-copy-btn" data-copy="
|
|
225
|
+
<code>${esc(installCmd)}</code>
|
|
226
|
+
<button class="update-copy-btn" data-copy="${esc(installCmd)}" title="Copy">
|
|
111
227
|
<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>
|
|
112
228
|
</button>
|
|
113
229
|
</div>
|
|
114
|
-
<p style="font-size:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
<div class="update-
|
|
118
|
-
<
|
|
119
|
-
<
|
|
120
|
-
<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>
|
|
121
|
-
</button>
|
|
230
|
+
<p style="font-size:11px;color:var(--t-muted);opacity:.75;margin:8px 0 0">${esc(installSource)}</p>
|
|
231
|
+
${autoRestartControl}
|
|
232
|
+
|
|
233
|
+
<div class="update-run-status" id="update-run-status" style="display:none;margin:10px 0 0">
|
|
234
|
+
<span class="update-backup-icon" id="update-run-icon"></span>
|
|
235
|
+
<span id="update-run-text"></span>
|
|
122
236
|
</div>
|
|
123
|
-
|
|
237
|
+
|
|
238
|
+
<div class="tag-delete-modal-actions" style="margin-top:14px;flex-wrap:wrap;gap:8px">
|
|
124
239
|
<button class="action-btn action-btn--ghost" id="update-back">Back</button>
|
|
125
|
-
|
|
126
|
-
|
|
240
|
+
${manualButton}
|
|
241
|
+
${updateNowButton}
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<p id="update-manual-note" style="display:none;font-size:11px;color:var(--t-muted);opacity:.75;margin:10px 0 0">
|
|
245
|
+
After running the command, restart SynaBun with <code>synabun</code>.
|
|
246
|
+
</p>`;
|
|
127
247
|
}
|
|
128
248
|
|
|
129
249
|
function bindEvents() {
|
|
@@ -164,6 +284,16 @@ function openUpdateModal() {
|
|
|
164
284
|
// Backup download
|
|
165
285
|
overlay.querySelector('#update-backup-download')?.addEventListener('click', handleBackupDownload);
|
|
166
286
|
|
|
287
|
+
// Update Now — kick off click-to-update flow
|
|
288
|
+
overlay.querySelector('#update-run-now')?.addEventListener('click', handleRunUpdate);
|
|
289
|
+
|
|
290
|
+
// Manual — surface the command + relaunch hint, hide auto-update controls
|
|
291
|
+
overlay.querySelector('#update-manual')?.addEventListener('click', () => {
|
|
292
|
+
overlay.querySelector('#update-manual-note').style.display = '';
|
|
293
|
+
const runNow = overlay.querySelector('#update-run-now');
|
|
294
|
+
if (runNow) runNow.style.display = 'none';
|
|
295
|
+
});
|
|
296
|
+
|
|
167
297
|
// Skip checkbox
|
|
168
298
|
const skipCb = overlay.querySelector('#update-skip-backup');
|
|
169
299
|
if (skipCb) {
|
|
@@ -175,6 +305,78 @@ function openUpdateModal() {
|
|
|
175
305
|
}
|
|
176
306
|
}
|
|
177
307
|
|
|
308
|
+
async function handleRunUpdate() {
|
|
309
|
+
const installPlan = _versionData?.installPlan || {};
|
|
310
|
+
const canAutoUpdate = installPlan.canAutoUpdate === true;
|
|
311
|
+
const repoUrl = installPlan.openUrl || SYNABUN_GITHUB_URL;
|
|
312
|
+
|
|
313
|
+
// Auto-restart is always on; server forces it true regardless of payload.
|
|
314
|
+
const autoRestart = true;
|
|
315
|
+
|
|
316
|
+
const runBtn = overlay.querySelector('#update-run-now');
|
|
317
|
+
const backBtn = overlay.querySelector('#update-back');
|
|
318
|
+
const manualBtn = overlay.querySelector('#update-manual');
|
|
319
|
+
const status = overlay.querySelector('#update-run-status');
|
|
320
|
+
const icon = overlay.querySelector('#update-run-icon');
|
|
321
|
+
const text = overlay.querySelector('#update-run-text');
|
|
322
|
+
|
|
323
|
+
if (!runBtn || !backBtn || !status || !icon || !text) return;
|
|
324
|
+
|
|
325
|
+
if (!canAutoUpdate) {
|
|
326
|
+
window.open(repoUrl, '_blank', 'noopener,noreferrer');
|
|
327
|
+
status.style.display = 'flex';
|
|
328
|
+
icon.className = 'update-backup-icon';
|
|
329
|
+
icon.textContent = '↗';
|
|
330
|
+
icon.style.color = '#4fc3f7';
|
|
331
|
+
text.innerHTML = `<strong>GitHub repository opened</strong><span class="update-run-detail">Use the repository instructions to update this non-npm installation.</span>`;
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
runBtn.disabled = true;
|
|
336
|
+
backBtn.disabled = true;
|
|
337
|
+
if (manualBtn) manualBtn.disabled = true;
|
|
338
|
+
runBtn.innerHTML = '<span class="update-spinner"></span> Launching updater...';
|
|
339
|
+
status.style.display = 'flex';
|
|
340
|
+
icon.className = 'update-backup-icon spin';
|
|
341
|
+
icon.textContent = '⟳';
|
|
342
|
+
icon.style.color = '';
|
|
343
|
+
text.innerHTML = '<strong>Launching updater</strong><span class="update-run-detail">Opening a terminal window for the npm update.</span>';
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
const res = await fetch('/api/system/run-update', {
|
|
347
|
+
method: 'POST',
|
|
348
|
+
headers: { 'Content-Type': 'application/json' },
|
|
349
|
+
body: JSON.stringify({ autoRestart }),
|
|
350
|
+
});
|
|
351
|
+
if (!res.ok) {
|
|
352
|
+
let msg = `Updater failed (${res.status})`;
|
|
353
|
+
try { const b = await res.json(); msg = b.error || msg; } catch {}
|
|
354
|
+
throw new Error(msg);
|
|
355
|
+
}
|
|
356
|
+
await res.json();
|
|
357
|
+
|
|
358
|
+
icon.className = 'update-backup-icon';
|
|
359
|
+
icon.textContent = '✓';
|
|
360
|
+
icon.style.color = '#34c759';
|
|
361
|
+
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>`;
|
|
362
|
+
runBtn.innerHTML = 'Updater running…';
|
|
363
|
+
|
|
364
|
+
// Server is shutting down. Optionally redirect to a friendly offline
|
|
365
|
+
// notice — for now leave the page; user already understands from copy.
|
|
366
|
+
// Re-enable Back so they can close the modal.
|
|
367
|
+
setTimeout(() => { backBtn.disabled = false; }, 2500);
|
|
368
|
+
} catch (err) {
|
|
369
|
+
icon.className = 'update-backup-icon';
|
|
370
|
+
icon.textContent = '✗';
|
|
371
|
+
icon.style.color = 'var(--accent-red)';
|
|
372
|
+
text.textContent = 'Failed to launch updater: ' + err.message;
|
|
373
|
+
runBtn.disabled = false;
|
|
374
|
+
backBtn.disabled = false;
|
|
375
|
+
if (manualBtn) manualBtn.disabled = false;
|
|
376
|
+
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`;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
178
380
|
async function handleBackupDownload() {
|
|
179
381
|
const btn = overlay.querySelector('#update-backup-download');
|
|
180
382
|
const status = overlay.querySelector('#update-backup-status');
|