Add built-in auto-updater with tab-bar update pill

Wires electrobun's Updater to the /dl download host. The packaged app checks
https://miniscp.com/dl on launch + every 6h; when a newer build exists, an
accent-colored "update tool" appears next to Connections in the tab bar. It
announces the update, becomes its own inline progress bar while downloading,
then flips to "Restart to apply" (two-step: download, then user-triggered
relaunch).

- electrobun.config.ts: release.baseUrl = https://miniscp.com/dl (baked into
  version.json at build; miniscp.com fronted by an external proxy for TLS).
- rpc.ts: checkForUpdate/downloadUpdate/applyUpdate requests + updateStatus push.
- rpc-handlers.ts + index.ts: call Updater; poll on launch+6h; stream
  onStatusChange progress. checkForUpdate self-guards the dev channel.
- rpc.service.ts + new updater.service.ts: signal-based state
  (available -> downloading -> ready, ready derived from downloadUpdate ok).
- tab-bar.ts: single bg-primary pill; progress fills the pill itself.
- icons.ts: register mynaDownload. i18n: tabBar + menu keys across all locales.
- menu.ts + app.ts: "Check for Updates…" app-menu item -> UpdaterService.check().
- docs: build-and-release + architecture updated.

Unsigned builds still update (applyUpdate strips the macOS quarantine xattr).
Verified: tsc --noEmit (exit 0) + build:ui AOT (exit 0). Live pill + full
download/apply flow require a two-build test on the host.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-14 16:43:40 +02:00
co-authored by Claude
parent 43f1168d6a
commit a40433b99e
19 changed files with 261 additions and 4 deletions
+57
View File
@@ -0,0 +1,57 @@
import { Injectable, inject, signal } from '@angular/core';
import { RpcService } from './rpc.service';
export type UpdaterState = 'idle' | 'available' | 'downloading' | 'ready' | 'error';
/**
* Drives the tab-bar update pill. The backend pushes updateStatus events
* (available / downloading progress / error); "ready" is derived locally when
* downloadUpdate resolves ok. Two-step UX: available -> download -> ready ->
* apply (relaunch).
*/
@Injectable({ providedIn: 'root' })
export class UpdaterService {
private rpc = inject(RpcService);
readonly state = signal<UpdaterState>('idle');
readonly version = signal('');
readonly progress = signal(0);
constructor() {
if (!this.rpc.available) return;
this.rpc.onUpdateStatus((s) => {
if (s.phase === 'available') {
this.version.set(s.version ?? '');
this.state.set('available');
} else if (s.phase === 'downloading') {
this.state.set('downloading');
this.progress.set(s.progress ?? 0);
} else if (s.phase === 'error') {
this.state.set('error');
}
});
}
async download(): Promise<void> {
if (this.state() !== 'available') return;
this.state.set('downloading');
this.progress.set(0);
const r = await this.rpc.request.downloadUpdate({});
this.state.set(r?.ok ? 'ready' : 'error');
}
apply(): void {
if (this.state() !== 'ready') return;
void this.rpc.request.applyUpdate({}); // quits + relaunches
}
/** Manual re-check (menu item). */
async check(): Promise<void> {
if (!this.rpc.available) return;
const r = await this.rpc.request.checkForUpdate({});
if (r?.ok && r.updateAvailable) {
this.version.set(r.version);
this.state.set('available');
}
}
}