feat(update): one-click desktop self-update (AppImage, .deb, Windows)
Same change as Guará: update_apply used to flatly refuse to run for
the GUI binary and point the user to a manual download instead. Since
the desktop app already elevates to root before showing its window,
the process applying the update already has everything it needs — no
second authentication round is required.
- New sabia_core::update::apply_desktop[_with_progress], alongside the
existing CLI-only apply() (left untouched): detects the install kind
at runtime (AppImage via the APPIMAGE env var, same FUSE-mount issue
elevated_exe() already works around and this reuses; Deb otherwise
on Linux; WindowsInstaller on Windows), matches the right release
asset by the GUI bundle naming (_amd64/_aarch64.AppImage,
_amd64/_arm64.deb, _x64-setup.exe), verifies SHA-256 when a sidecar
is published, and installs:
- AppImage: downloads to a temp file in the same directory, renames
over the running file, execs it in place (same PID, already root).
- .deb: runs dpkg -i directly (already root), execs current_exe()
again now that dpkg replaced it on disk.
- Windows: runs the installer silently and detached, exits
immediately to release the file lock. Does not claim to relaunch
the app afterwards — no documented NSIS or Tauri flag does that;
needs a real Windows machine to confirm actual post-install
behavior.
- Engine::update_apply_desktop wires it into the Tauri update_apply
command (now takes AppHandle, emits update-progress events).
- Fixed a real gap: the existing update button handler changed its own
label when an update was available but never actually called apply
— wired it for real, with per-phase progress feedback in the
interface's existing language (download/verify/install/restart).
- scripts/build-gui.sh: generates a checksum file next to every file
in dist-gui/, matching the CLI release archive convention.
- 3 new unit tests: install-kind detection, GUI asset suffix matching
against real release naming, existing CLI-refusal test unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VAyr8d7HuAdUGB6aA5xmDD
This commit is contained in:
1 parent
7d69f2e4b2
commit
6191c7bbc6
6 files changed
+455
-14
No files matched your search
+48
-4
@@ -267,6 +267,8 @@ async function tauriApi(path, opts = {}) {
|
||||
return await invoke('export_all_hc22000', { iface: q.get('iface'), out: q.get('out') });
|
||||
case 'GET /api/update/check':
|
||||
return await invoke('update_check');
|
||||
case 'POST /api/update/apply':
|
||||
return await invoke('update_apply');
|
||||
default:
|
||||
return { error: 'rota desconhecida' };
|
||||
}
|
||||
@@ -371,12 +373,54 @@ els.btnScan.addEventListener('click', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
let updateStatus = null;
|
||||
let updateInProgress = false;
|
||||
|
||||
els.btnUpdate.addEventListener('click', async () => {
|
||||
if (updateInProgress) return;
|
||||
if (updateStatus?.update_available) {
|
||||
updateInProgress = true;
|
||||
els.btnUpdate.disabled = true;
|
||||
els.btnUpdate.textContent = 'baixando…';
|
||||
|
||||
let unlisten = null;
|
||||
if (tauri?.event?.listen) {
|
||||
try {
|
||||
unlisten = await tauri.event.listen('update-progress', (event) => {
|
||||
const phase = event.payload?.phase;
|
||||
if (phase === 'downloading') els.btnUpdate.textContent = 'baixando…';
|
||||
else if (phase === 'verifying') els.btnUpdate.textContent = 'verificando…';
|
||||
else if (phase === 'installing') els.btnUpdate.textContent = 'instalando…';
|
||||
else if (phase === 'restarting') els.btnUpdate.textContent = 'reiniciando…';
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
try {
|
||||
await api('/api/update/apply', { method: 'POST' });
|
||||
els.btnUpdate.textContent = 'já atualizado';
|
||||
} catch (err) {
|
||||
updateInProgress = false;
|
||||
els.btnUpdate.disabled = false;
|
||||
els.btnUpdate.textContent = `atualizar para ${updateStatus.latest_version}`;
|
||||
alert('Falha na atualização: ' + (err?.message || err));
|
||||
} finally {
|
||||
if (unlisten) unlisten();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
els.btnUpdate.textContent = 'checando…';
|
||||
const status = await api('/api/update/check');
|
||||
els.btnUpdate.textContent = status.update_available
|
||||
? `atualizar para ${status.latest_version}`
|
||||
: 'já atualizado';
|
||||
try {
|
||||
const status = await api('/api/update/check');
|
||||
updateStatus = status;
|
||||
els.btnUpdate.textContent = status?.update_available
|
||||
? `atualizar para ${status.latest_version}`
|
||||
: 'já atualizado';
|
||||
} catch (err) {
|
||||
els.btnUpdate.textContent = 'checar atualização';
|
||||
alert('Não foi possível verificar atualizações: ' + (err?.message || err));
|
||||
}
|
||||
});
|
||||
|
||||
const SEC_RANK = {
|
||||
|
||||
@@ -12,6 +12,12 @@ export const EN = {
|
||||
'checando…': 'checking…',
|
||||
'atualizar para {n}': 'update to {n}',
|
||||
'já atualizado': 'already up to date',
|
||||
'baixando…': 'downloading…',
|
||||
'verificando…': 'verifying…',
|
||||
'instalando…': 'installing…',
|
||||
'reiniciando…': 'restarting…',
|
||||
'Falha na atualização:': 'Update failed:',
|
||||
'Não foi possível verificar atualizações:': 'Could not check for updates:',
|
||||
|
||||
// topbar
|
||||
'Painel de captura': 'Capture dashboard',
|
||||
|
||||
Reference in new issue
Block a user