v0.1.2 - Update-Modal mit Live-Progress + Error-Display
- AppUpdateBanner: dialog statt window.confirm, full Modal-Overlay - 3 Steps visualisiert: Pre-Backup -> Download -> Installation mit animierten Icons (radio_button -> progress_activity (spin) -> check_circle) - Live-Download-Progress: 'X.X MB / Y.Y MB (NN%)' - Errors fett im Modal sichtbar mit pre-formatierter Stack - Console.log fuer jeden Schritt fuer DevTools-Debugging
This commit is contained in:
+1
-1
@@ -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.",
|
||||
|
||||
Generated
+1
-1
@@ -3567,7 +3567,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rapport-server-app"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"axum-server",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 (
|
||||
<div className="update-banner">
|
||||
<span className="material-symbols-outlined update-bell">system_update</span>
|
||||
<span className="update-text">
|
||||
Update verfuegbar: <strong>v{update.version}</strong>
|
||||
{update.body && <span className="muted"> · {update.body.split('\n')[0]}</span>}
|
||||
</span>
|
||||
<button onClick={install} disabled={busy}>
|
||||
{busy ? (progress ?? 'Installiere ...') : 'Backup + Installieren'}
|
||||
</button>
|
||||
{error && <span className="error-text">{error}</span>}
|
||||
<>
|
||||
<div className="update-banner">
|
||||
<span className="material-symbols-outlined update-bell">system_update</span>
|
||||
<span className="update-text">
|
||||
Update verfuegbar: <strong>v{update.version}</strong>
|
||||
{update.body && <span className="muted"> · {update.body.split('\n')[0]}</span>}
|
||||
</span>
|
||||
<button onClick={() => setShowModal(true)} disabled={showModal && step !== 'idle' && step !== 'error'}>
|
||||
Backup + Installieren
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showModal && (
|
||||
<div className="update-modal-backdrop" onClick={step === 'idle' ? close : undefined}>
|
||||
<div className="update-modal" onClick={e => e.stopPropagation()}>
|
||||
<h2>Update auf v{update.version}</h2>
|
||||
|
||||
{step === 'idle' && (
|
||||
<>
|
||||
<p className="muted">Ablauf:</p>
|
||||
<ol className="update-steps">
|
||||
<li>pg_dumpall Pre-Backup (~10 Sek)</li>
|
||||
<li>Neue Binary herunterladen + Signatur pruefen (~10-30 Sek)</li>
|
||||
<li>App-Restart mit neuer Version</li>
|
||||
</ol>
|
||||
<p className="muted">
|
||||
Container laufen waehrend Restart weiter — keine Downtime auf der
|
||||
Service-Seite. Admin-UI ist ~10s nicht verfuegbar.
|
||||
</p>
|
||||
<div className="row" style={{ marginTop: 20, justifyContent: 'flex-end' }}>
|
||||
<button onClick={close}>Abbrechen</button>
|
||||
<button onClick={install} className="setup-primary">Backup + Installieren</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{step !== 'idle' && (
|
||||
<div className="update-progress">
|
||||
<Step name="Pre-Backup" state={stepState('backup', step)} />
|
||||
<Step name="Download" state={stepState('download', step)} extra={progressLabel(progress, step)} />
|
||||
<Step name="Installation" state={stepState('install', step)} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 'error' && (
|
||||
<>
|
||||
<p className="error-text" style={{ marginTop: 16 }}>
|
||||
<strong>Fehler:</strong>
|
||||
</p>
|
||||
<pre className="update-error-pre">{error}</pre>
|
||||
<div className="row" style={{ marginTop: 16, justifyContent: 'flex-end' }}>
|
||||
<button onClick={close}>Schliessen</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{step === 'done' && (
|
||||
<p className="success-text">App restartet jetzt mit der neuen Version ...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="update-step" data-state={state}>
|
||||
<span className="material-symbols-outlined update-step-icon">{icon}</span>
|
||||
<span className="update-step-name">{name}</span>
|
||||
{extra && <span className="update-step-extra muted">{extra}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user