diff --git a/package.json b/package.json index 40b39fc..35592b2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rapport-server-app", - "version": "0.1.1", + "version": "0.1.2", "private": true, "type": "module", "description": "Doppelklick-Self-Hosting f\u00fcr Rapport \u2014 Tauri-App, die Postgres, GoTrue, PostgREST, Realtime und Storage als Subprozesse b\u00fcndelt.", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c6d1ace..0c40874 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3567,7 +3567,7 @@ dependencies = [ [[package]] name = "rapport-server-app" -version = "0.1.1" +version = "0.1.2" dependencies = [ "axum", "axum-server", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 9edda30..456729f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rapport-server-app" -version = "0.1.1" +version = "0.1.2" edition = "2021" authors = ["Karim Gabriele Varano"] license = "AGPL-3.0-or-later" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index f320b72..4fc65b7 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2.0.0", "productName": "RAPPORT Server", - "version": "0.1.1", + "version": "0.1.2", "identifier": "com.rapport.server-app", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/components/AppUpdateBanner.jsx b/src/components/AppUpdateBanner.jsx index 724c1e1..1a7f0a3 100644 --- a/src/components/AppUpdateBanner.jsx +++ b/src/components/AppUpdateBanner.jsx @@ -1,17 +1,16 @@ -// Background-Check fuer App-Updates. Im Tauri-Kontext: nutzt das offizielle -// @tauri-apps/plugin-updater. Im Browser (Web-UI): kein App-Update moeglich -// (Browser kann nicht die Server-Mac-App neu installieren) — Banner versteckt. +// Auto-Update-Banner. Pollt latest.json alle 6h. +// Bei Klick: Modal-Overlay mit Live-Fortschritt + sichtbare Errors. import { useEffect, useState } from 'react' -import { runtime } from '../api.js' -import { api } from '../api.js' +import { runtime, api } from '../api.js' const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000 export default function AppUpdateBanner() { const [update, setUpdate] = useState(null) - const [busy, setBusy] = useState(false) + const [showModal, setShowModal] = useState(false) + const [step, setStep] = useState('idle') // idle|backup|download|install|done|error + const [progress, setProgress] = useState({ downloaded: 0, total: 0 }) const [error, setError] = useState(null) - const [progress, setProgress] = useState(null) useEffect(() => { if (runtime !== 'tauri') return @@ -24,10 +23,7 @@ export default function AppUpdateBanner() { const u = await check() if (alive) setUpdate(u) } catch (e) { - if (alive) { - console.warn('updater check:', e) - setError(String(e)) - } + if (alive) console.warn('updater check:', e) } } @@ -38,53 +34,149 @@ export default function AppUpdateBanner() { async function install() { if (!update) return - const ok = window.confirm( - `Update auf v${update.version} installieren?\n\n` + - 'Ablauf:\n' + - ' 1. pg_dumpall Pre-Backup (auto)\n' + - ' 2. Neue Binary herunterladen + Signatur pruefen\n' + - ' 3. App-Restart mit neuer Version\n\n' + - 'Container laufen waehrend Restart weiter — keine Downtime\n' + - 'auf der Service-Seite. Admin-UI ist ~10s nicht verfuegbar.' - ) - if (!ok) return + setShowModal(true) + setError(null) + setProgress({ downloaded: 0, total: 0 }) - setBusy(true); setError(null); setProgress('Pre-Backup...') try { - // 1) Pre-Backup - await api.backupNow() - setProgress('Update wird heruntergeladen...') + setStep('backup') + console.log('[update] starting pre-backup') + const backup = await api.backupNow() + console.log('[update] backup ok:', backup) - // 2) Download + Install (Tauri plugin uebernimmt restart) + setStep('download') + console.log('[update] starting download + install') + let totalBytes = 0 + let downloaded = 0 await update.downloadAndInstall((event) => { - // event.event: 'Started' | 'Progress' | 'Finished' - if (event.event === 'Progress') { - setProgress(`Download: ${event.data.chunkLength} Bytes`) + console.log('[update] event:', event.event, event.data ?? '') + if (event.event === 'Started') { + totalBytes = event.data?.contentLength ?? 0 + setProgress({ downloaded: 0, total: totalBytes }) + } else if (event.event === 'Progress') { + downloaded += event.data?.chunkLength ?? 0 + setProgress({ downloaded, total: totalBytes }) } else if (event.event === 'Finished') { - setProgress('Installiert — Restart...') + setStep('install') + setProgress({ downloaded: totalBytes, total: totalBytes }) } }) - // Tauri startet die App automatisch neu, dieser Code-Pfad erreicht's nie + // Tauri restartet automatisch — sollten wir nie sehen + setStep('done') } catch (e) { - setError(String(e)) - setBusy(false) - setProgress(null) + console.error('[update] failed:', e) + setStep('error') + setError(String(e?.message ?? e)) + } + } + + function close() { + if (step === 'idle' || step === 'error' || step === 'done') { + setShowModal(false) + setStep('idle') + setError(null) } } if (!update) return null return ( -
- system_update - - Update verfuegbar: v{update.version} - {update.body && · {update.body.split('\n')[0]}} - - - {error && {error}} + <> +
+ system_update + + Update verfuegbar: v{update.version} + {update.body && · {update.body.split('\n')[0]}} + + +
+ + {showModal && ( +
+
e.stopPropagation()}> +

Update auf v{update.version}

+ + {step === 'idle' && ( + <> +

Ablauf:

+
    +
  1. pg_dumpall Pre-Backup (~10 Sek)
  2. +
  3. Neue Binary herunterladen + Signatur pruefen (~10-30 Sek)
  4. +
  5. App-Restart mit neuer Version
  6. +
+

+ Container laufen waehrend Restart weiter — keine Downtime auf der + Service-Seite. Admin-UI ist ~10s nicht verfuegbar. +

+
+ + +
+ + )} + + {step !== 'idle' && ( +
+ + + +
+ )} + + {step === 'error' && ( + <> +

+ Fehler: +

+
{error}
+
+ +
+ + )} + + {step === 'done' && ( +

App restartet jetzt mit der neuen Version ...

+ )} +
+
+ )} + + ) +} + +function stepState(target, current) { + const order = ['idle', 'backup', 'download', 'install', 'done', 'error'] + const ti = order.indexOf(target) + const ci = order.indexOf(current) + if (current === 'error' && ci > ti) return 'error' + if (ci > ti) return 'done' + if (ci === ti) return 'active' + return 'pending' +} + +function progressLabel(p, step) { + if (step !== 'download') return '' + if (p.total === 0) return 'startet ...' + const mb = (b) => (b / 1024 / 1024).toFixed(1) + ' MB' + const pct = p.total > 0 ? Math.floor((p.downloaded / p.total) * 100) : 0 + return `${mb(p.downloaded)} / ${mb(p.total)} (${pct}%)` +} + +function Step({ name, state, extra }) { + const icon = { + pending: 'radio_button_unchecked', + active: 'progress_activity', + done: 'check_circle', + error: 'cancel', + }[state] + return ( +
+ {icon} + {name} + {extra && {extra}}
) } diff --git a/src/styles.css b/src/styles.css index afe8e92..e6665ea 100644 --- a/src/styles.css +++ b/src/styles.css @@ -378,6 +378,53 @@ button.setup-primary:hover:not(:disabled) { background: #4a8fff; } .icon-inline { font-size: 16px; vertical-align: -3px; margin-right: 6px; } .icon-heading { font-size: 20px; vertical-align: -4px; margin-right: 8px; } +/* Update modal */ +.update-modal-backdrop { + position: fixed; inset: 0; + background: rgba(0,0,0,0.5); + z-index: 1000; + display: flex; align-items: center; justify-content: center; +} +.update-modal { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 12px; + padding: 24px 28px; + width: 480px; max-width: calc(100vw - 40px); + box-shadow: 0 16px 48px rgba(0,0,0,0.5); +} +.update-modal h2 { margin: 0 0 12px; font-size: 18px; } +.update-steps { padding-left: 20px; margin: 8px 0 16px; font-size: 13px; } +.update-steps li { margin: 4px 0; } +.update-progress { display: flex; flex-direction: column; gap: 10px; margin-top: 16px; } +.update-step { + display: flex; align-items: center; gap: 10px; + padding: 10px 12px; + background: rgba(255,255,255,0.03); + border-radius: 6px; + font-size: 13px; +} +.update-step[data-state="active"] { background: rgba(106,168,255,0.12); } +.update-step[data-state="done"] { background: rgba(91,208,122,0.10); } +.update-step[data-state="error"] { background: rgba(239,90,90,0.12); } +.update-step-icon { font-size: 20px; line-height: 1; } +.update-step[data-state="pending"] .update-step-icon { color: var(--gray); } +.update-step[data-state="active"] .update-step-icon { color: var(--accent); animation: spin 1.4s linear infinite; } +.update-step[data-state="done"] .update-step-icon { color: var(--green); } +.update-step[data-state="error"] .update-step-icon { color: var(--red); } +.update-step-name { flex: 1; } +.update-step-extra { font-size: 11px; font-family: ui-monospace, monospace; } +@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } +.update-error-pre { + margin: 6px 0 0; padding: 10px 12px; + background: rgba(239,90,90,0.06); + border: 1px solid rgba(239,90,90,0.3); + border-radius: 4px; + font-size: 11px; max-height: 200px; overflow: auto; + white-space: pre-wrap; word-break: break-word; + color: var(--red); +} + /* App update banner */ .update-banner { display: flex; align-items: center; gap: 12px;