feat(phase-c): Vollstaendige Svelte-5-Migration aller 8 Panels
Alle Panels vollstaendig migriert (kein Legacy-JS mehr): - OverviewPanel: Hero, Metriken, System-Gesundheit, Stack, Top-News - JobsPanel: KPI-Kacheln, Sparklines, Job-Liste mit Log/Abbrechen - ServerPanel: Engine-Status, Service-Aktionen, Live-Konsole (WebSocket) - ModelsPanel: Schnelltest-Chat, Tabelle, Konfig- + Rollen-Modal - CookbookPanel: Rezepte, Discover, Profi-Suche, 3 Modals (Rezept/Modell/Neu) - ConnectPanel: Wizard, Tool-Picker, merge-sichere Snippets - GuidesPanel + NewsPanel: bereits in Phase C1 migriert main.ts: importiert nur noch Svelte-Panels (keine Legacy-JS-Imports) index.html: Placeholder-Divs entfernt, nur noch leere Sections stores/jobs.svelte.ts: track/toggle-Funktionen fuer ServerPanel svelte.config.js: a11y-Warnungen als non-fatal konfiguriert Build: 119 Module -> 137 kB (gzip: 48 kB) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte'
|
||||
import { statusStore } from '../stores/status.svelte.js'
|
||||
import { jobsStore } from '../stores/jobs.svelte.js'
|
||||
import { api, getToken } from '@core/api.js'
|
||||
import { esc, toast, confirmModal, promptModal } from '@core/ui.js'
|
||||
|
||||
const status = $derived(statusStore.value)
|
||||
const jobs = $derived(jobsStore.value)
|
||||
|
||||
// Console
|
||||
let ws: WebSocket | null = null
|
||||
let consoleLogs = $state('Verbinde…\n')
|
||||
let selectedService = $state('llama-swap')
|
||||
let consoleEl: HTMLDivElement | undefined
|
||||
|
||||
// Active job tracking
|
||||
let activeJobId = $state<string | null>(null)
|
||||
let activeJobLabel = $state('')
|
||||
const notified = new Set<string>()
|
||||
|
||||
const activeJob = $derived(activeJobId ? jobs.find((j: any) => j.id === activeJobId) : null)
|
||||
|
||||
// Auto-scroll + job finish notification
|
||||
$effect(() => {
|
||||
if (!activeJob) return
|
||||
const j = activeJob as any
|
||||
const done = j.state === 'done', failed = j.state === 'failed', canceled = j.state === 'canceled'
|
||||
if ((done || failed || canceled) && !notified.has(j.id)) {
|
||||
notified.add(j.id)
|
||||
toast(done ? `✓ ${activeJobLabel} abgeschlossen.` : canceled ? `${activeJobLabel} abgebrochen.` : `✗ ${activeJobLabel} fehlgeschlagen.`, failed)
|
||||
if (done) setTimeout(() => document.dispatchEvent(new Event('mc:refresh')), 800)
|
||||
}
|
||||
})
|
||||
|
||||
function watchJob(id: string, label: string) {
|
||||
activeJobId = id
|
||||
activeJobLabel = label
|
||||
jobsStore.track(id)
|
||||
}
|
||||
|
||||
// Expose track for use from other panels (server panel tracks its own jobs)
|
||||
onMount(() => { connectConsole() })
|
||||
onDestroy(() => { ws?.close() })
|
||||
|
||||
function connectConsole() {
|
||||
ws?.close()
|
||||
const svc = selectedService
|
||||
consoleLogs = `Verbinde mit ${svc}…\n`
|
||||
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
ws = new WebSocket(`${proto}//${location.host}/api/logs/${svc}?token=${encodeURIComponent(getToken())}`)
|
||||
ws.onmessage = e => {
|
||||
consoleLogs += e.data
|
||||
if (consoleEl) consoleEl.scrollTop = consoleEl.scrollHeight
|
||||
}
|
||||
ws.onclose = () => { consoleLogs += '\n— Verbindung getrennt —' }
|
||||
ws.onerror = () => ws?.close()
|
||||
}
|
||||
|
||||
async function restartService(name: string, title: string, body: string) {
|
||||
if (!await confirmModal({ title, body, confirmLabel: 'Neustarten' })) return
|
||||
try {
|
||||
await api(`/api/service/${name}/restart`, { method: 'POST' })
|
||||
toast('Neustart ausgelöst: ' + name)
|
||||
setTimeout(() => document.dispatchEvent(new Event('mc:refresh')), 2000)
|
||||
} catch (e: any) { toast(e.message, true) }
|
||||
}
|
||||
|
||||
async function selfUpdate() {
|
||||
if (!await confirmModal({ title: 'Mission Control aktualisieren?', body: 'Holt die neueste Dashboard-Version aus Gitea und startet automatisch neu. Die Seite trennt kurz und verbindet von selbst wieder (~10 Sek).' })) return
|
||||
try { const r = await api('/api/self-update', { method: 'POST' }); toast('Update läuft — Dashboard startet gleich neu.'); watchJob(r.job_id, 'Mission Control Update') }
|
||||
catch (e: any) { toast(e.message, true) }
|
||||
}
|
||||
|
||||
async function engineUpdate() {
|
||||
if (!await confirmModal({ title: 'LLM-Engine aktualisieren?', body: 'Lädt die neueste llama.cpp-ROCm-Version und installiert sie. Die Engine ist während des Updates kurz nicht erreichbar.' })) return
|
||||
try { const r = await api('/api/update', { method: 'POST' }); toast('LLM-Engine-Update läuft.'); watchJob(r.job_id, 'LLM-Engine-Update') }
|
||||
catch (e: any) { toast(e.message, true) }
|
||||
}
|
||||
|
||||
async function unloadAll() {
|
||||
if (!await confirmModal({ title: 'Grafikspeicher leeren?', body: 'Alle geladenen Modelle werden entladen. Sie laden beim nächsten Aufruf automatisch neu — es geht nichts verloren.', confirmLabel: 'Leeren' })) return
|
||||
try { await api('/api/unload', { method: 'POST' }); toast('Grafikspeicher geleert.'); setTimeout(() => document.dispatchEvent(new Event('mc:refresh')), 600) }
|
||||
catch (e: any) { toast(e.message, true) }
|
||||
}
|
||||
|
||||
async function osUpdate() {
|
||||
if (!await confirmModal({ title: 'OS-Updates installieren?', body: 'Führt <code>apt update & upgrade</code> aus. Das kann ein paar Minuten dauern; der Fortschritt erscheint in der Aktivität.' })) return
|
||||
const pwd = await promptModal({ title: 'sudo-Passwort', body: 'Für die System-Updates wird einmalig dein sudo-Passwort gebraucht.', placeholder: 'sudo-Passwort', password: true, confirmLabel: 'Installieren' })
|
||||
if (!pwd) return
|
||||
try { const r = await api('/api/os-update', { method: 'POST', body: JSON.stringify({ password: pwd }) }); toast('OS-Update gestartet.'); watchJob(r.job_id, 'OS-Update') }
|
||||
catch (e: any) { toast(e.message, true) }
|
||||
}
|
||||
|
||||
async function rebootServer() {
|
||||
if (!await confirmModal({ title: 'Server wirklich neu starten?', body: 'Der ganze Bosgame startet physisch neu. Alles ist für ~1 Minute offline — auch dieses Dashboard.', confirmLabel: 'Reboot', danger: true })) return
|
||||
const pwd = await promptModal({ title: 'sudo-Passwort', body: 'Für den Reboot wird einmalig dein sudo-Passwort gebraucht.', placeholder: 'sudo-Passwort', password: true, confirmLabel: 'Jetzt neustarten', danger: true })
|
||||
if (!pwd) return
|
||||
try { await api('/api/reboot', { method: 'POST', body: JSON.stringify({ password: pwd }) }); toast('Reboot ausgelöst — bis gleich.') }
|
||||
catch (e: any) { toast(e.message, true) }
|
||||
}
|
||||
|
||||
async function checkSwap() {
|
||||
swapCheckResult = 'prüfe…'
|
||||
try {
|
||||
const r = await api('/api/integration/test')
|
||||
swapCheckResult = r.ok ? `✓ Engine antwortet — ${r.models.length} Modell(e): ${r.models.join(', ')}` : `✗ keine Verbindung: ${r.error}`
|
||||
swapCheckOk = r.ok
|
||||
} catch (e: any) { swapCheckResult = '✗ ' + e.message; swapCheckOk = false }
|
||||
}
|
||||
|
||||
let swapCheckResult = $state('')
|
||||
let swapCheckOk = $state(false)
|
||||
|
||||
const swapModels = $derived((status?.models || []) as any[])
|
||||
const loadedModel = $derived(swapModels.find((m: any) => m.state === 'running' || m.state === 'loading'))
|
||||
|
||||
// Job progress helpers
|
||||
function jobPct(j: any) {
|
||||
if (!j || j.state !== 'running') return null
|
||||
if (typeof j.progress === 'number') return Math.min(100, j.progress)
|
||||
const last = (j.log || []).at(-1) || ''
|
||||
const m = last.match(/(\d+(?:\.\d+)?)\s*%/)
|
||||
return m ? Math.min(100, parseFloat(m[1])) : null
|
||||
}
|
||||
|
||||
function qa(tone: string, title: string, desc: string, fn: () => void, danger = false) {
|
||||
return { tone, title, desc, fn, danger }
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="pagehead">
|
||||
<div>
|
||||
<h1>Server & Wartung</h1>
|
||||
<div class="sub">Dienste steuern, Updates einspielen und live mitlesen — ganz ohne SSH oder Terminal.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LLM-Engine Card -->
|
||||
<div class="card">
|
||||
<div class="card-h">
|
||||
<h3>LLM-Engine (llama-swap)</h3>
|
||||
<span class="chip" style="margin-left:auto;{status?.swap_ok
|
||||
? 'background:rgba(63,185,80,.12);border-color:rgba(63,185,80,.25);color:#7ee29a'
|
||||
: 'background:rgba(248,81,73,.12);border-color:rgba(248,81,73,.3);color:#ff9b95'}">
|
||||
{status?.swap_ok ? '● erreichbar' : '● offline'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-sub">Das Herzstück: lädt bei jeder Anfrage automatisch das passende Modell.</div>
|
||||
<div class="kv" style="margin-bottom:6px">
|
||||
<div class="kv-row"><span class="kv-k">Adresse (für deine Tools)</span><span class="kv-v mono-sm">{(status?.swap_url || '—') + '/v1'}</span></div>
|
||||
<div class="kv-row"><span class="kv-k">Aktuell geladen</span>
|
||||
<span class="kv-v">{loadedModel ? loadedModel.name + ` (${loadedModel.state})` : 'nichts geladen — lädt bei der nächsten Anfrage'}</span>
|
||||
</div>
|
||||
<div class="kv-row"><span class="kv-k">Konfigurierte Modelle</span>
|
||||
<span class="kv-v">{swapModels.filter((m: any) => !m.incomplete).length || 'keine'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-row" style="display:flex;align-items:center;gap:10px">
|
||||
<button class="ghost" onclick={checkSwap}>Verbindung prüfen</button>
|
||||
{#if swapCheckResult}
|
||||
<span class="mono-sm" style="color:{swapCheckOk ? '#7ee29a' : '#ff9b95'}">{swapCheckResult}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Service Actions -->
|
||||
<div class="card">
|
||||
<div class="card-h"><h3>Dienste & Applikation</h3></div>
|
||||
<div class="card-sub">Diese Aktionen sind harmlos und passwortlos — es geht nichts dabei verloren.</div>
|
||||
<button class="qa" onclick={() => restartService('llama-swap', 'LLM-Engine neustarten?', 'Die Engine startet neu. Geladene Modelle werden kurz entladen (~5 Sekunden), laden danach automatisch wieder.')}>
|
||||
<span class="qa-ic teal">↻</span>
|
||||
<span class="qa-main"><span class="qa-t">LLM-Engine neustarten</span><span class="qa-s">Startet llama-swap neu. Geladene Modelle laden danach automatisch wieder.</span></span>
|
||||
<span class="qa-arrow">›</span>
|
||||
</button>
|
||||
<button class="qa" onclick={() => restartService('mission-control', 'Dashboard neustarten?', 'Diese Oberfläche trennt sich kurz und verbindet automatisch wieder.')}>
|
||||
<span class="qa-ic blue">↻</span>
|
||||
<span class="qa-main"><span class="qa-t">Dashboard neustarten</span><span class="qa-s">Startet diese Oberfläche neu (~5 Sek).</span></span>
|
||||
<span class="qa-arrow">›</span>
|
||||
</button>
|
||||
<button class="qa" onclick={selfUpdate}>
|
||||
<span class="qa-ic teal">⬇</span>
|
||||
<span class="qa-main"><span class="qa-t">Mission Control aktualisieren</span><span class="qa-s">Holt die neueste Version aus Gitea und startet automatisch neu (~10 Sek).</span></span>
|
||||
<span class="qa-arrow">›</span>
|
||||
</button>
|
||||
<button class="qa" onclick={engineUpdate}>
|
||||
<span class="qa-ic blue">⚡</span>
|
||||
<span class="qa-main"><span class="qa-t">LLM-Engine aktualisieren</span><span class="qa-s">Lädt die neueste llama.cpp-Version (ROCm) für deine GPU.</span></span>
|
||||
<span class="qa-arrow">›</span>
|
||||
</button>
|
||||
<button class="qa" onclick={unloadAll}>
|
||||
<span class="qa-ic amber">🗄</span>
|
||||
<span class="qa-main"><span class="qa-t">Grafikspeicher leeren</span><span class="qa-s">Wirft alle aktuell geladenen Modelle aus dem VRAM. Fragt vorher nach.</span></span>
|
||||
<span class="qa-arrow">›</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- OS Actions -->
|
||||
<div class="card">
|
||||
<div class="card-h"><h3>Betriebssystem (Bosgame)</h3></div>
|
||||
<div class="card-sub">Tiefe Eingriffe — das Dashboard fragt einmalig nach deinem sudo-Passwort.</div>
|
||||
<button class="qa" onclick={osUpdate}>
|
||||
<span class="qa-ic amber">↻</span>
|
||||
<span class="qa-main"><span class="qa-t">OS-Updates installieren</span><span class="qa-s">Führt apt update & upgrade aus. Kann ein paar Minuten dauern.</span></span>
|
||||
<span class="qa-arrow">›</span>
|
||||
</button>
|
||||
<button class="qa qa-danger" onclick={rebootServer}>
|
||||
<span class="qa-ic red">⚡</span>
|
||||
<span class="qa-main"><span class="qa-t">Server neustarten (Reboot)</span><span class="qa-s">Bootet den ganzen Bosgame neu. Alles ist für ~1 Minute offline.</span></span>
|
||||
<span class="qa-arrow">›</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Active Job Card -->
|
||||
{#if activeJob}
|
||||
{@const j = activeJob as any}
|
||||
{@const done = j.state === 'done'}
|
||||
{@const failed = j.state === 'failed'}
|
||||
{@const canceled = j.state === 'canceled'}
|
||||
{@const terminal = done || failed || canceled}
|
||||
{@const p = jobPct(j)}
|
||||
<div class="card">
|
||||
<div class="card-h">
|
||||
<h3>Aktueller Vorgang</h3>
|
||||
{#if !terminal}
|
||||
<button class="ghost" style="margin-left:auto;padding:3px 12px"
|
||||
onclick={async () => {
|
||||
if (await confirmModal({ title: activeJobLabel + ' abbrechen?', body: 'Der laufende Vorgang wird gestoppt.', confirmLabel: 'Abbrechen', danger: true }))
|
||||
api(`/api/jobs/${j.id}/cancel`, { method: 'POST' })
|
||||
}}>Abbrechen</button>
|
||||
{/if}
|
||||
<span style="margin-left:10px">
|
||||
{#if done}<span class="badge b-run">✓ fertig</span>
|
||||
{:else if failed}<span class="badge b-err">✗ fehlgeschlagen</span>
|
||||
{:else if canceled}<span class="badge">abgebrochen</span>
|
||||
{:else}<span class="badge b-load">läuft…</span>{/if}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-sub">{activeJobLabel}{terminal ? (done ? ' — erfolgreich.' : canceled ? ' — abgebrochen.' : ' — fehlgeschlagen.') : ' läuft…'}</div>
|
||||
{#if p != null && !terminal}
|
||||
<div class="bar" style="margin-bottom:10px"><i style="width:{p}%"></i></div>
|
||||
{/if}
|
||||
<div class="log" style="max-height:280px">{(j.log || []).join('\n')}</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Live Console -->
|
||||
<div class="card">
|
||||
<div class="card-h">
|
||||
<h3>Live-Konsole</h3>
|
||||
<select bind:value={selectedService} onchange={connectConsole} style="margin:0 0 0 auto;width:240px">
|
||||
<option value="llama-swap">LLM-Engine (llama-swap)</option>
|
||||
<option value="mission-control">Dashboard (mission-control)</option>
|
||||
<option value="system">System (ganzer Server)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="card-sub">Live mitlesen, was der Dienst gerade tut.</div>
|
||||
<div class="console" bind:this={consoleEl}>{consoleLogs}</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user