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:
Hitonabi
2026-06-22 22:08:08 +02:00
parent 27b229b3d7
commit 86831a3627
12 changed files with 1755 additions and 578 deletions
+259
View File
@@ -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 &amp; 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 &amp; 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 &amp; 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>