feat(dashboard): Dashboard-Redesign + Bugfixes + Pre-load
Bugs behoben:
- Topbar + Server-Panel: 'Kein Modell im VRAM' obwohl geladen → alle
States (running/ready/loading/starting) werden jetzt erkannt
- Topbar zeigt bei geladenem Modell: Name + ctx-Größe + State mit Farbe
Dashboard-Redesign (OverviewPanel):
- KPI-Kacheln mit Farbe: .kpi.green/.red/.blue/.muted statt .tile
(farbige Randlinien + farbige Werte wie im Referenz-Design)
- Icons pro Kachel (server, cpu, thermo, database)
- Stack-Liste: zeigt ctx/quant/GB, 'Laden'-Button wenn Modell bereit
aber nicht geladen, ∞-Chip für immer-aktive Modelle
- 4. Schnellstart-Button: 'Tools verbinden'
Feature: Modell Vorwärmen (Pre-load):
- POST /api/preload?model=<name> — sendet Mini-Request an llama-swap
damit es das Modell vorab lädt (max_tokens=1, kein Output)
- 'Laden'-Button je Modell in der Stack-Liste
Topbar CSS:
- Aktives Modell: teal-hinterlegter Pill wenn Modell geladen
- Kontext-Größe direkt neben Modellname angezeigt
Cookbook:
- Discover: genaue Zeitanzeige wann Quellen zuletzt befragt wurden
('vor X min / h / Tg.') statt nur 'aktuell'
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -37,11 +37,16 @@ function applyStatus(s: any) {
|
||||
const host = (s.swap_url || '').replace(/^https?:\/\//, '')
|
||||
dot && (dot.className = 'dot ' + (s.swap_ok ? 'on' : 'off'))
|
||||
label && (label.textContent = (s.swap_ok ? 'LLM-Engine: Online – ' : 'LLM-Engine: Offline – ') + host)
|
||||
const active = (s.models || []).find((m: any) => m.state === 'running')
|
||||
const LOADED = ['running', 'ready', 'loading', 'starting']
|
||||
const active = (s.models || []).find((m: any) => LOADED.includes(m.state))
|
||||
if (activeText) {
|
||||
activeText.innerHTML = active
|
||||
? `Geladen: <b style="color:var(--teal)">${active.name}</b>`
|
||||
: 'Kein Modell im VRAM'
|
||||
if (active) {
|
||||
const ctx = active.meta?.ctx ? ` <span style="color:var(--mut);font-size:11.5px">· ${Math.round(active.meta.ctx / 1024)}k ctx</span>` : ''
|
||||
const stateLabel = active.state === 'loading' || active.state === 'starting' ? ' <span style="color:var(--warn);font-size:11.5px">lädt…</span>' : ''
|
||||
activeText.innerHTML = `<b style="color:var(--accent)">${active.name}</b>${ctx}${stateLabel}`
|
||||
} else {
|
||||
activeText.innerHTML = '<span style="color:var(--mut)">Kein Modell im VRAM</span>'
|
||||
}
|
||||
}
|
||||
if (s.swap_ok) hideAlert()
|
||||
else showAlert(`LLM-Engine nicht erreichbar unter <b>${host}</b> – läuft der llama-swap Dienst?`, true)
|
||||
|
||||
@@ -302,12 +302,24 @@
|
||||
<div class="card">
|
||||
<div class="card-h">
|
||||
<h3>Aktuell die besten Modelle für dein System</h3>
|
||||
<span class="meta" id="cb-disc-when">{discoverData ? 'aktuell' : ''}</span>
|
||||
<span class="meta" style="margin-left:auto">
|
||||
{#if discoverData?.updated}
|
||||
{@const ageMin = Math.round((Date.now() / 1000 - discoverData.updated) / 60)}
|
||||
{#if ageMin < 60}
|
||||
Quellen befragt vor {ageMin} min
|
||||
{:else if ageMin < 1440}
|
||||
Quellen befragt vor {Math.round(ageMin / 60)} h
|
||||
{:else}
|
||||
Quellen befragt vor {Math.round(ageMin / 1440)} Tg.
|
||||
{/if}
|
||||
{#if discoverStale}<span style="color:var(--warn)"> · ⚠ Cache veraltet</span>{/if}
|
||||
{/if}
|
||||
</span>
|
||||
<button class="ghost" style="margin-left:8px" disabled={discoverLoading} onclick={() => loadDiscover(true)}>
|
||||
{discoverLoading ? 'Suche…' : 'Aktualisieren'}
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-sub">Täglich aktualisierte Top-Modelle je Kategorie — Hardware-Fit berechnet für dein System. {discoverStale ? '⚠ Cache veraltet.' : ''}</div>
|
||||
<div class="card-sub">Top-Modelle je Kategorie von HuggingFace — Hardware-Fit wird live für dein System berechnet.</div>
|
||||
{#if discoverLoading && !discoverData}
|
||||
<div class="empty" style="text-align:center;padding:30px">Frage Quellen ab…</div>
|
||||
{:else if discoverData?.categories}
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
import { statusStore } from '../stores/status.svelte.js'
|
||||
import { systemStore } from '../stores/system.svelte.js'
|
||||
import { api } from '@core/api.js'
|
||||
import { confirmModal, icon } from '@core/ui.js'
|
||||
import { confirmModal, icon, fmtBytes } from '@core/ui.js'
|
||||
|
||||
const s = $derived(statusStore.value)
|
||||
const sys = $derived(systemStore.value)
|
||||
const allModels = $derived((s?.models || []) as any[])
|
||||
const swapOk = $derived(s?.swap_ok ?? false)
|
||||
const activeModel = $derived(allModels.find((m: any) => ['running','ready','loading','starting'].includes(m.state)))
|
||||
const LOADED = ['running', 'ready', 'loading', 'starting']
|
||||
const activeModel = $derived(allModels.find((m: any) => LOADED.includes(m.state)))
|
||||
const configuredModels = $derived(allModels.filter((m: any) => !m.incomplete))
|
||||
|
||||
const heroTitle = $derived(
|
||||
!s ? 'Verbinde…'
|
||||
@@ -21,98 +23,123 @@
|
||||
!s ? ''
|
||||
: !swapOk ? 'Der llama-swap-Dienst antwortet gerade nicht.'
|
||||
: sys && sys.ram.percent >= 90 ? `Arbeitsspeicher bei ${Math.round(sys.ram.percent)} % — eventuell Speicher freigeben.`
|
||||
: `${allModels.length} ${allModels.length === 1 ? 'Modell' : 'Modelle'} bereit · ${activeModel ? `„${activeModel.name}" geladen` : 'keins geladen'} · keine Warnungen.`
|
||||
: `${configuredModels.length} ${configuredModels.length === 1 ? 'Modell' : 'Modelle'} konfiguriert · ${activeModel ? `„${activeModel.name}" geladen` : 'keines geladen'} · keine Warnungen.`
|
||||
)
|
||||
|
||||
const gpuPct = $derived(() => {
|
||||
const g = sys?.gpu
|
||||
if (g && (g.vram.total + g.gtt.total) > 0)
|
||||
return ((g.vram.used + g.gtt.used) / (g.vram.total + g.gtt.total)) * 100
|
||||
return 0
|
||||
})
|
||||
const tempWord = (t: number | null) =>
|
||||
t == null ? '' : t < 55 ? 'kühl & gesund' : t < 72 ? 'normal' : 'läuft heiß'
|
||||
|
||||
let news = $state<any[]>([])
|
||||
let preloadingModel = $state<string | null>(null)
|
||||
|
||||
onMount(async () => {
|
||||
try { news = ((await api('/api/news')).items || []).slice(0, 4) } catch {}
|
||||
})
|
||||
|
||||
async function freeMemory() {
|
||||
const ok = await confirmModal({
|
||||
if (!await confirmModal({
|
||||
title: 'Speicher freigeben?',
|
||||
body: 'Alle aktuell geladenen Modelle werden aus dem Grafikspeicher geworfen. Sie laden beim nächsten Aufruf automatisch neu — es geht nichts verloren.',
|
||||
body: 'Alle aktuell geladenen Modelle werden aus dem Grafikspeicher geworfen. Sie laden beim nächsten Aufruf automatisch neu.',
|
||||
confirmLabel: 'Speicher freigeben',
|
||||
})
|
||||
if (!ok) return
|
||||
})) return
|
||||
await api('/api/unload', { method: 'POST' })
|
||||
document.dispatchEvent(new Event('mc:refresh'))
|
||||
refresh()
|
||||
}
|
||||
|
||||
async function preload(modelName: string) {
|
||||
preloadingModel = modelName
|
||||
try { await api('/api/preload?model=' + encodeURIComponent(modelName), { method: 'POST' }) }
|
||||
catch {}
|
||||
preloadingModel = null
|
||||
refresh()
|
||||
}
|
||||
|
||||
function refresh() { document.dispatchEvent(new Event('mc:refresh')) }
|
||||
function go(view: string) {
|
||||
document.querySelector(`.nav-item[data-view="${view}"]`)?.dispatchEvent(new MouseEvent('click'))
|
||||
}
|
||||
|
||||
function capTagClass(caps: string[]) {
|
||||
if (caps?.includes('Bild')) return 'img'
|
||||
if (caps?.includes('Code')) return 'code'
|
||||
return 'text'
|
||||
}
|
||||
function capTagLabel(caps: string[]) {
|
||||
if (caps?.includes('Bild')) return 'Bild'
|
||||
if (caps?.includes('Code')) return 'Code'
|
||||
return 'Text'
|
||||
}
|
||||
function ramColor(pct: number) { return pct >= 90 ? 'red' : pct >= 75 ? '' : 'green' }
|
||||
function tempColor(t: number | null) { return t == null ? '' : t >= 75 ? 'red' : t >= 55 ? '' : 'green' }
|
||||
function gpuColor(pct: number) { return pct >= 90 ? 'red' : pct >= 70 ? '' : 'green' }
|
||||
</script>
|
||||
|
||||
<div class="pagehead">
|
||||
<!-- Hero -->
|
||||
<div class="pagehead" style="margin-bottom:10px">
|
||||
<div>
|
||||
<h1>{heroTitle}</h1>
|
||||
<h1 style="display:flex;align-items:center;gap:10px">
|
||||
{heroTitle}
|
||||
{#if swapOk && !activeModel}
|
||||
<span style="font-size:13px;font-weight:400;color:var(--mut)">— bereit für Anfragen</span>
|
||||
{/if}
|
||||
</h1>
|
||||
<div class="sub">{heroSub}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI Tiles -->
|
||||
<div class="tiles">
|
||||
<div class="tile">
|
||||
<div class="t-l">LLM-Engine</div>
|
||||
<div class="t-v {swapOk ? 'ok' : 'bad'}">{swapOk ? 'Online' : 'Offline'}</div>
|
||||
<div class="t-s">llama-swap</div>
|
||||
<div class="kpi {swapOk ? 'green' : 'red'}">
|
||||
<div class="k-h">
|
||||
<span class="k-t">LLM-Engine</span>
|
||||
<span class="k-ic">{@html icon('server')}</span>
|
||||
</div>
|
||||
<div class="tile">
|
||||
<div class="t-l">Aktives Modell</div>
|
||||
<div class="t-v">{activeModel ? activeModel.name : 'Keins'}</div>
|
||||
<div class="t-s">{activeModel ? 'im Grafikspeicher' : 'nichts geladen'}</div>
|
||||
<div class="k-v">{swapOk ? 'Online' : 'Offline'}</div>
|
||||
<div class="k-s">{s?.swap_url?.replace(/^https?:\/\//, '') ?? '—'}</div>
|
||||
</div>
|
||||
|
||||
<div class="kpi {activeModel ? 'blue' : 'muted'}">
|
||||
<div class="k-h">
|
||||
<span class="k-t">Aktives Modell</span>
|
||||
<span class="k-ic">{@html icon('cpu')}</span>
|
||||
</div>
|
||||
<div class="k-v" style="font-size:16px;word-break:break-word">{activeModel ? activeModel.name : 'Keins'}</div>
|
||||
<div class="k-s">
|
||||
{#if activeModel}
|
||||
{activeModel.meta?.ctx ? Math.round(activeModel.meta.ctx / 1024) + 'k ctx' : ''} · {activeModel.state}
|
||||
{:else}
|
||||
lädt bei erster Anfrage
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if sys}
|
||||
{@const t = sys.gpu_temp ?? sys.cpu?.temp ?? null}
|
||||
<div class="tile">
|
||||
<div class="t-l">GPU-Temperatur</div>
|
||||
<div class="t-v">{t != null ? `${Math.round(t)}°` : '–'}</div>
|
||||
<div class="t-s {t != null && t < 72 ? 'ok' : ''}">{tempWord(t)}</div>
|
||||
{@const tCol = tempColor(t)}
|
||||
<div class="kpi {tCol}">
|
||||
<div class="k-h"><span class="k-t">Temperatur (GPU)</span><span class="k-ic">{@html icon('thermo')}</span></div>
|
||||
<div class="k-v">{t != null ? `${Math.round(t)}°C` : '–'}</div>
|
||||
<div class="k-s">{t == null ? 'messe…' : t < 55 ? 'kühl' : t < 75 ? 'normal' : '⚠ heiß'}</div>
|
||||
</div>
|
||||
{@const free = sys.ram.total - sys.ram.used}
|
||||
<div class="tile">
|
||||
<div class="t-l">Freier Speicher</div>
|
||||
<div class="t-v">{Math.round(free / 1024 ** 3)}<small> GB</small></div>
|
||||
<div class="t-s">von {Math.round(sys.ram.total / 1024 ** 3)} GB</div>
|
||||
{@const freePct = 100 - sys.ram.percent}
|
||||
<div class="kpi {freePct < 15 ? 'red' : freePct < 30 ? '' : 'green'}">
|
||||
<div class="k-h"><span class="k-t">Freier Speicher</span><span class="k-ic">{@html icon('database')}</span></div>
|
||||
<div class="k-v">{Math.round(free / 1024 ** 3)}<small> GB</small></div>
|
||||
<div class="k-s">von {Math.round(sys.ram.total / 1024 ** 3)} GB · {Math.round(freePct)} % frei</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="tile"><div class="t-l">GPU-Temperatur</div><div class="t-v">–</div><div class="t-s">messe…</div></div>
|
||||
<div class="tile"><div class="t-l">Freier Speicher</div><div class="t-v">–</div><div class="t-s">messe…</div></div>
|
||||
<div class="kpi muted"><div class="k-h"><span class="k-t">Temperatur</span></div><div class="k-v">–</div><div class="k-s">messe…</div></div>
|
||||
<div class="kpi muted"><div class="k-h"><span class="k-t">Freier Speicher</span></div><div class="k-v">–</div><div class="k-s">messe…</div></div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Split: Health + Quickstart -->
|
||||
<div class="split">
|
||||
<div class="card">
|
||||
<div class="card-h"><h3>System-Gesundheit</h3></div>
|
||||
<div class="card-sub">So ausgelastet ist dein Mini-PC gerade.</div>
|
||||
<div class="card-sub">Auslastung in Echtzeit.</div>
|
||||
{#if sys}
|
||||
{@const gpu = gpuPct()}
|
||||
{#each [
|
||||
['Prozessor (CPU)', sys.cpu?.percent ?? 0],
|
||||
['Arbeitsspeicher (RAM)', sys.ram.percent],
|
||||
['Grafikspeicher (VRAM)', gpu]
|
||||
] as [label, pct]}
|
||||
['Prozessor (CPU)', sys.cpu?.percent ?? 0, ''],
|
||||
['Arbeitsspeicher (RAM)', sys.ram.percent, ramColor(sys.ram.percent)],
|
||||
['Grafikspeicher (VRAM)', gpu, gpuColor(gpu)]
|
||||
] as [label, pct, cls]}
|
||||
{@const p = Math.max(0, Math.min(100, (pct as number) || 0))}
|
||||
<div class="meter">
|
||||
<div class="meter-h"><span class="mk">{label}</span><span class="mv">{Math.round(p)} %</span></div>
|
||||
@@ -134,7 +161,7 @@
|
||||
</button>
|
||||
<button class="qa" onclick={freeMemory}>
|
||||
<span class="qa-ic amber">{@html icon('refresh')}</span>
|
||||
<span class="qa-main"><span class="qa-t">Speicher freigeben</span><span class="qa-s">Modelle entladen · fragt vorher nach</span></span>
|
||||
<span class="qa-main"><span class="qa-t">Speicher freigeben</span><span class="qa-s">Alle Modelle entladen</span></span>
|
||||
<span class="qa-arrow">{@html icon('chevron')}</span>
|
||||
</button>
|
||||
<button class="qa" onclick={() => go('server')}>
|
||||
@@ -142,38 +169,57 @@
|
||||
<span class="qa-main"><span class="qa-t">Logs ansehen</span><span class="qa-s">Live mitlesen, was läuft</span></span>
|
||||
<span class="qa-arrow">{@html icon('chevron')}</span>
|
||||
</button>
|
||||
<button class="qa" onclick={() => go('connect')}>
|
||||
<span class="qa-ic teal">{@html icon('swap')}</span>
|
||||
<span class="qa-main"><span class="qa-t">Tools verbinden</span><span class="qa-s">Zed, OpenCode, Cline einrichten</span></span>
|
||||
<span class="qa-arrow">{@html icon('chevron')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stack + News -->
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-h">
|
||||
<h3>Dein Stack</h3>
|
||||
<span class="meta">{allModels.length ? allModels.length + ' konfiguriert' : ''}</span>
|
||||
<span class="meta">{configuredModels.length ? configuredModels.length + ' konfiguriert' : ''}</span>
|
||||
</div>
|
||||
{#if allModels.length}
|
||||
{#if configuredModels.length}
|
||||
<div class="list">
|
||||
{#each allModels as m}
|
||||
{#each configuredModels as m}
|
||||
{@const isLoaded = LOADED.includes(m.state)}
|
||||
{@const downloading = m.state === 'downloading'}
|
||||
{@const on = ['running','ready','loading','starting'].includes(m.state)}
|
||||
{@const dot = (m.state === 'loading' || m.state === 'starting' || downloading) ? 'load' : on ? 'on' : ''}
|
||||
{@const dot = (m.state === 'loading' || m.state === 'starting' || downloading) ? 'load' : isLoaded ? 'on' : ''}
|
||||
{@const statusText = downloading
|
||||
? `↓ Download${m.download_progress != null ? ` ${Math.round(m.download_progress)}%` : ''}`
|
||||
: on ? (m.state === 'loading' ? 'lädt…' : 'geladen') : 'bereit'}
|
||||
<div class="li">
|
||||
? `↓ ${m.download_progress != null ? Math.round(m.download_progress) + '%' : '...'}`
|
||||
: m.state === 'loading' || m.state === 'starting' ? 'lädt…'
|
||||
: isLoaded ? 'geladen ✓' : 'bereit'}
|
||||
<div class="li" style="padding:10px 4px">
|
||||
<span class="li-dot {dot}"></span>
|
||||
<div class="li-main">
|
||||
<div class="flex items-center gap-2" style="flex-wrap:wrap">
|
||||
<span class="li-id" style="{isLoaded ? 'color:var(--accent)' : ''}">{m.name}</span>
|
||||
{#if m.meta?.caps?.includes('Bild')}<span class="tag img">Bild</span>
|
||||
{:else if m.meta?.caps?.includes('Code')}<span class="tag code">Code</span>
|
||||
{:else}<span class="tag text">Text</span>{/if}
|
||||
{#if m.ttl >= 99999}<span class="chip" style="padding:2px 7px;font-size:11px">∞ aktiv</span>{/if}
|
||||
</div>
|
||||
<div class="li-sub mono-sm">
|
||||
{m.meta?.ctx ? Math.round(m.meta.ctx / 1024) + 'k ctx' : ''}
|
||||
{m.meta?.quant ? ' · ' + m.meta.quant : ''}
|
||||
{m.meta?.size_bytes ? ' · ' + (m.meta.size_bytes / 1024 ** 3).toFixed(1) + ' GB' : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="li-id">{m.name}</span>
|
||||
{#if m.meta?.caps?.length}
|
||||
<span class="tag {capTagClass(m.meta.caps)}">{capTagLabel(m.meta.caps)}</span>
|
||||
<span class="li-meta" style="white-space:nowrap;color:{isLoaded ? 'var(--on)' : 'var(--mut)'}">{statusText}</span>
|
||||
{#if !isLoaded && !downloading && swapOk}
|
||||
<button class="ghost" style="padding:3px 9px;font-size:12px;opacity:{preloadingModel === m.name ? '.5' : '1'}"
|
||||
disabled={preloadingModel === m.name}
|
||||
onclick={() => preload(m.name)}>
|
||||
{preloadingModel === m.name ? '…' : 'Laden'}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if m.meta?.filename}
|
||||
<div class="li-sub mono-sm">{m.meta.filename}</div>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="li-meta" style="white-space:nowrap">{statusText}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -181,6 +227,7 @@
|
||||
<div class="empty-c">
|
||||
<div class="e-t">Noch keine Modelle</div>
|
||||
<div class="e-s">Hol dir unter „Cookbook" ein Modell, das auf deine Hardware passt.</div>
|
||||
<button class="primary" style="margin-top:12px" onclick={() => go('cookbook')}>Zum Cookbook →</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -197,7 +244,7 @@
|
||||
<a class="li" href={n.link} target="_blank" rel="noopener" style="text-decoration:none;color:inherit">
|
||||
<div class="li-main">
|
||||
<div style="font-size:13px;line-height:1.4">{n.title}</div>
|
||||
<div class="li-sub">{n.source}</div>
|
||||
<div class="li-sub">{n.source}{n.relevant ? ' · für dich' : ''}</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
|
||||
@@ -113,7 +113,8 @@
|
||||
let swapCheckOk = $state(false)
|
||||
|
||||
const swapModels = $derived((status?.models || []) as any[])
|
||||
const loadedModel = $derived(swapModels.find((m: any) => m.state === 'running' || m.state === 'loading'))
|
||||
const LOADED = ['running', 'ready', 'loading', 'starting']
|
||||
const loadedModel = $derived(swapModels.find((m: any) => LOADED.includes(m.state)))
|
||||
|
||||
// Job progress helpers
|
||||
function jobPct(j: any) {
|
||||
|
||||
@@ -342,6 +342,22 @@ def delete_model(req: DeleteReq):
|
||||
return {"ok": True, "alias": req.alias, "freed_bytes": freed, "note": note or "Modell entfernt."}
|
||||
|
||||
|
||||
@router.post("/preload")
|
||||
def preload(model: str):
|
||||
"""Modell vorwaermen: sendet eine Minimal-Anfrage damit llama-swap es laedt.
|
||||
max_tokens=1 haelt die Antwort kurz; der Token wird verworfen."""
|
||||
try:
|
||||
with httpx.Client(timeout=60.0) as c:
|
||||
c.post(f"{LLAMA_SWAP_URL}/v1/chat/completions", json={
|
||||
"model": model,
|
||||
"messages": [{"role": "user", "content": "!"}],
|
||||
"max_tokens": 1,
|
||||
})
|
||||
return {"ok": True}
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise HTTPException(502, f"llama-swap nicht erreichbar: {exc}")
|
||||
|
||||
|
||||
@router.post("/unload")
|
||||
def unload(model: str | None = None):
|
||||
path = f"/api/models/unload/{model}" if model else "/api/models/unload"
|
||||
|
||||
+10
-2
@@ -91,8 +91,16 @@ a{color:var(--accent);text-decoration:none}
|
||||
.dot.on{background:var(--on);box-shadow:0 0 0 0 rgba(63,185,80,.5);animation:pulse 2.2s infinite}
|
||||
.dot.off{background:var(--err)}
|
||||
@keyframes pulse{0%{box-shadow:0 0 0 0 rgba(63,185,80,.45)}70%{box-shadow:0 0 0 7px rgba(63,185,80,0)}100%{box-shadow:0 0 0 0 rgba(63,185,80,0)}}
|
||||
.top-stat{font-size:var(--text-sm);color:var(--mut)}
|
||||
.top-stat b{color:var(--tx);font-family:var(--mono);font-weight:500;margin-left:4px}
|
||||
.top-stat{font-size:var(--text-sm);color:var(--mut);display:flex;align-items:center;gap:5px}
|
||||
.top-stat b{color:var(--accent);font-family:var(--mono);font-weight:600;letter-spacing:.01em}
|
||||
/* Aktives Modell in Topbar — klarer sichtbar wenn geladen */
|
||||
#top-active-text{
|
||||
font-family:var(--mono);font-size:var(--text-sm);
|
||||
padding:4px 10px;border-radius:999px;
|
||||
border:1px solid transparent;transition:border-color .3s,background .3s;
|
||||
}
|
||||
#top-active-text b{font-weight:600}
|
||||
#top-active-text:has(b){border-color:rgba(45,212,191,.25);background:rgba(45,212,191,.07)}
|
||||
.top-clock{font-family:var(--mono);font-size:var(--text-sm);color:var(--accent)}
|
||||
|
||||
/* Security-Chip (zeigt LAN/Token-Status) */
|
||||
|
||||
Vendored
+25
-23
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user