Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 94a1617519 | |||
| 0920d68ac1 | |||
| 2e3c94af64 |
+5
-5
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"version": "0.1.0",
|
"version": "0.1.2",
|
||||||
"notes": "Initial Release — Tauri-Admin-UI fuer den Rapport-Compose-Stack mit Live-Status, Backup/Restore, Auto-Recovery und LAN-WebUI.",
|
"notes": "Update-Modal mit Live-Progress + sichtbare Errors.",
|
||||||
"pub_date": "2026-05-24T13:57:04Z",
|
"pub_date": "2026-05-24T16:15:09Z",
|
||||||
"platforms": {
|
"platforms": {
|
||||||
"darwin-aarch64": {
|
"darwin-aarch64": {
|
||||||
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTYUg0T2hQVVkrUSthSktZM0xuSWthSzBJdjAyY2g2eVJCd0NJVjB4NXNQQUJEbWIyOEpNem9OQUpWRkFNQS9kWFd0Myt1Y1k4M2Y2ZzFHZFFUVVdMTkJMRzV4Yk1kd3drPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzc5NjI4Njc4CWZpbGU6UkFQUE9SVCBTZXJ2ZXIuYXBwLnRhci5negp3ajh0Ykp0ZDZDRXdlV3VlU2tjYlFYdUxRVThjL29sWVVwbnYzcy9wMzRIVW1nQ0hzOHdOZ3ZKcXpoVE1BNE45Y0VpQkY2M2dCaDlORjF0UjZYK0pEZz09Cg==",
|
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTYUg0T2hQVVkrUTFxS0FKYUdMZlNtY0RCcWRTMXJoL002NGlvaXRHWm5hTmVKd1JQbnJucG84Qm1lZ1lRdG1PZ1orQzZDUXJqNUYzaXpjWTd2R1l4aGkrZi9xVmpBQmc4PQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzc5NjM5MTgyCWZpbGU6UkFQUE9SVCBTZXJ2ZXIuYXBwLnRhci5negpnRWRvUlVSeDErQ2szQjZKRC9KSjErNWVYNWhyekdtUlArQnZFa1dwRlMwYUNRdG1hSVBRS0FHSU5wOG01RnBEbklaWlA0TVBxTTlxQmZkbHJpTTBDUT09Cg==",
|
||||||
"url": "https://git.kgva.ch/karim/RAPPORT-SERVER-APP/releases/download/v0.1.0/RAPPORT%20Server.app.tar.gz"
|
"url": "https://git.kgva.ch/karim/RAPPORT-SERVER-APP/releases/download/v0.1.2/RAPPORT%20Server.app.tar.gz"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "rapport-server-app",
|
"name": "rapport-server-app",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Doppelklick-Self-Hosting f\u00fcr Rapport \u2014 Tauri-App, die Postgres, GoTrue, PostgREST, Realtime und Storage als Subprozesse b\u00fcndelt.",
|
"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]]
|
[[package]]
|
||||||
name = "rapport-server-app"
|
name = "rapport-server-app"
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"axum-server",
|
"axum-server",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rapport-server-app"
|
name = "rapport-server-app"
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Karim Gabriele Varano"]
|
authors = ["Karim Gabriele Varano"]
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2.0.0",
|
"$schema": "https://schema.tauri.app/config/2.0.0",
|
||||||
"productName": "RAPPORT Server",
|
"productName": "RAPPORT Server",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"identifier": "com.rapport.server-app",
|
"identifier": "com.rapport.server-app",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
// Background-Check fuer App-Updates. Im Tauri-Kontext: nutzt das offizielle
|
// Auto-Update-Banner. Pollt latest.json alle 6h.
|
||||||
// @tauri-apps/plugin-updater. Im Browser (Web-UI): kein App-Update moeglich
|
// Bei Klick: Modal-Overlay mit Live-Fortschritt + sichtbare Errors.
|
||||||
// (Browser kann nicht die Server-Mac-App neu installieren) — Banner versteckt.
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { runtime } from '../api.js'
|
import { runtime, api } from '../api.js'
|
||||||
import { api } from '../api.js'
|
|
||||||
|
|
||||||
const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000
|
const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000
|
||||||
|
|
||||||
export default function AppUpdateBanner() {
|
export default function AppUpdateBanner() {
|
||||||
const [update, setUpdate] = useState(null)
|
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 [error, setError] = useState(null)
|
||||||
const [progress, setProgress] = useState(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (runtime !== 'tauri') return
|
if (runtime !== 'tauri') return
|
||||||
@@ -24,10 +23,7 @@ export default function AppUpdateBanner() {
|
|||||||
const u = await check()
|
const u = await check()
|
||||||
if (alive) setUpdate(u)
|
if (alive) setUpdate(u)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (alive) {
|
if (alive) console.warn('updater check:', e)
|
||||||
console.warn('updater check:', e)
|
|
||||||
setError(String(e))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,53 +34,149 @@ export default function AppUpdateBanner() {
|
|||||||
|
|
||||||
async function install() {
|
async function install() {
|
||||||
if (!update) return
|
if (!update) return
|
||||||
const ok = window.confirm(
|
setShowModal(true)
|
||||||
`Update auf v${update.version} installieren?\n\n` +
|
setError(null)
|
||||||
'Ablauf:\n' +
|
setProgress({ downloaded: 0, total: 0 })
|
||||||
' 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
|
|
||||||
|
|
||||||
setBusy(true); setError(null); setProgress('Pre-Backup...')
|
|
||||||
try {
|
try {
|
||||||
// 1) Pre-Backup
|
setStep('backup')
|
||||||
await api.backupNow()
|
console.log('[update] starting pre-backup')
|
||||||
setProgress('Update wird heruntergeladen...')
|
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) => {
|
await update.downloadAndInstall((event) => {
|
||||||
// event.event: 'Started' | 'Progress' | 'Finished'
|
console.log('[update] event:', event.event, event.data ?? '')
|
||||||
if (event.event === 'Progress') {
|
if (event.event === 'Started') {
|
||||||
setProgress(`Download: ${event.data.chunkLength} Bytes`)
|
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') {
|
} 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) {
|
} catch (e) {
|
||||||
setError(String(e))
|
console.error('[update] failed:', e)
|
||||||
setBusy(false)
|
setStep('error')
|
||||||
setProgress(null)
|
setError(String(e?.message ?? e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
if (step === 'idle' || step === 'error' || step === 'done') {
|
||||||
|
setShowModal(false)
|
||||||
|
setStep('idle')
|
||||||
|
setError(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!update) return null
|
if (!update) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div className="update-banner">
|
<div className="update-banner">
|
||||||
<span className="material-symbols-outlined update-bell">system_update</span>
|
<span className="material-symbols-outlined update-bell">system_update</span>
|
||||||
<span className="update-text">
|
<span className="update-text">
|
||||||
Update verfuegbar: <strong>v{update.version}</strong>
|
Update verfuegbar: <strong>v{update.version}</strong>
|
||||||
{update.body && <span className="muted"> · {update.body.split('\n')[0]}</span>}
|
{update.body && <span className="muted"> · {update.body.split('\n')[0]}</span>}
|
||||||
</span>
|
</span>
|
||||||
<button onClick={install} disabled={busy}>
|
<button onClick={() => setShowModal(true)} disabled={showModal && step !== 'idle' && step !== 'error'}>
|
||||||
{busy ? (progress ?? 'Installiere ...') : 'Backup + Installieren'}
|
Backup + Installieren
|
||||||
</button>
|
</button>
|
||||||
{error && <span className="error-text">{error}</span>}
|
</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>
|
</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-inline { font-size: 16px; vertical-align: -3px; margin-right: 6px; }
|
||||||
.icon-heading { font-size: 20px; vertical-align: -4px; margin-right: 8px; }
|
.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 */
|
/* App update banner */
|
||||||
.update-banner {
|
.update-banner {
|
||||||
display: flex; align-items: center; gap: 12px;
|
display: flex; align-items: center; gap: 12px;
|
||||||
|
|||||||
Reference in New Issue
Block a user