// main.js — App-Boot: Panels mounten, Nav starten, Topbar/Alert pflegen, Polling fahren. // Panel-Vertrag: { id, mount?(), onStatus?(s), onJobs?(jobs) }. import { api, getToken, setToken, getHfToken, setHfToken } from "./core/api.js"; import { $ } from "./core/ui.js"; import { initNav } from "./core/nav.js"; import overview from "./panels/overview.js"; import models from "./panels/models.js"; import server from "./panels/server.js"; import jobs from "./panels/jobs.js"; import cookbook from "./panels/cookbook.js"; import connect from "./panels/connect.js"; import news from "./panels/news.js"; import guides from "./panels/guides.js"; const panels = [overview, models, server, jobs, cookbook, connect, news, guides]; let lastJobs = []; let lastSystem = null; let prevModelStates = {}; // ---- Topbar / Alert aus dem Status ableiten ---- function applyStatus(s) { const dot = $("#swdot"), label = $("#swlabel"), alert = $("#alert"); if (!s) { dot.className = "dot off"; label.textContent = "Backend nicht erreichbar"; $("#top-active-text").textContent = "Backend offline"; showAlert("Backend nicht erreichbar – läuft uvicorn?", false); } else { const host = s.swap_url.replace(/^https?:\/\//, ""); dot.className = "dot " + (s.swap_ok ? "on" : "off"); label.textContent = (s.swap_ok ? "LLM-Engine: Online – " : "LLM-Engine: Offline – ") + host; const active = (s.models || []).find(m => m.state === "running"); if (active) { $("#top-active-text").innerHTML = `Geladen: ${active.name}`; } else { $("#top-active-text").innerHTML = "Kein Modell im VRAM"; } if (s.swap_ok) hideAlert(); else showAlert(`LLM-Engine nicht erreichbar unter ${host} – läuft der llama-swap Dienst?`, true); } setSecChip(s); for (const p of panels) p.onStatus?.(s); } // Security-Chip: ehrlich anzeigen, ob ein Zugangs-Token aktiv ist. function setSecChip(s) { const chip = $("#sec-chip"), tx = $("#sec-chip-tx"); if (!chip || !tx) return; const secured = !!s?.secured; chip.classList.toggle("ok", secured); tx.textContent = secured ? "Geschützt · Heimnetz" : "Nur im Heimnetz (offen)"; } function applyJobs(jobs) { lastJobs = jobs || []; for (const p of panels) p.onJobs?.(lastJobs); } function applySystem(sys) { lastSystem = sys; for (const p of panels) p.onSystem?.(sys); } function showAlert(html, warn) { const a = $("#alert"); a.className = "alert" + (warn ? " warn" : ""); a.innerHTML = `${html}`; a.style.display = "flex"; } function hideAlert() { $("#alert").style.display = "none"; } // ---- Toolbar: ausstehende Updates anzeigen ---- const goView = v => document.querySelector(`.nav-item[data-view="${v}"]`)?.click(); function badge(text, cls, view, title) { const s = document.createElement("span"); s.className = "upd-badge" + (cls ? " " + cls : ""); s.textContent = text; if (title) s.title = title; s.onclick = () => goView(view); return s; } async function pollUpdates() { try { const u = await api("/api/updates"); const el = $("#update-badges"); if (!el) return; const age = u.apt_cache_age_h; const stale = age != null && age > 24; const ageNote = age != null ? ` · apt-Cache: vor ${age}h` : ""; el.innerHTML = ""; el.appendChild(badge( u.os > 0 ? `OS: ${u.os} Updates` : (stale ? "OS: ?" : "OS: aktuell"), u.os > 0 ? "warn" : stale ? "warn" : "ok", "server", u.os > 0 ? `${u.os} ausstehende Pakete${ageNote}` : (stale ? `apt-Cache veraltet (${age}h) — OS-Update prüfen` : `Keine ausstehenden Pakete${ageNote}`) )); el.appendChild(badge(u.engine > 0 ? "Engine: Update" : "Engine: aktuell", u.engine > 0 ? "warn" : "ok", "server")); el.appendChild(badge(u.models > 0 ? `Modelle: ${u.models}` : "Modelle: aktuell", u.models > 0 ? "warn" : "ok", "news")); } catch { /* still */ } } // ---- Swap-Erkennung: kurze visuelle Hervorhebung wenn Modell wechselt ---- function flashSwap() { const el = $("#top-active-text"); if (el) { el.classList.add("swap-flash"); setTimeout(() => el.classList.remove("swap-flash"), 900); } } // ---- Polling ---- async function pollStatus() { try { const s = await api("/api/status"); // Zustandswechsel erkennen → Swap-Flash auslösen const newStates = {}; for (const m of (s?.models || [])) newStates[m.name] = m.state; for (const [name, state] of Object.entries(newStates)) { const prev = prevModelStates[name]; if (prev && prev !== state && (state === "running" || state === "loading")) { flashSwap(); break; } } prevModelStates = newStates; applyStatus(s); } catch { applyStatus(null); } } async function pollJobs() { try { applyJobs(await api("/api/jobs")); } catch { /* still */ } } function connectSystemStream() { const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const wsUrl = `${protocol}//${window.location.host}/api/system/stream`; const ws = new WebSocket(wsUrl); ws.onmessage = (e) => { try { const data = JSON.parse(e.data); applySystem(data); } catch (err) {} }; ws.onclose = () => { // Bei Verbindungsabbruch nach 3 Sekunden erneut versuchen setTimeout(connectSystemStream, 3000); }; ws.onerror = () => ws.close(); } // ---- Boot ---- function bootToken() { const i = $("#token"); if (i) { i.value = getToken(); i.addEventListener("change", e => { setToken(e.target.value); pollStatus(); }); } const hf = $("#hf-token"); if (hf) { hf.value = getHfToken(); hf.addEventListener("change", e => setHfToken(e.target.value)); } const sbtn = $("#nav-settings"); const smod = $("#settings-modal"); const scls = $("#sm-close"); if (sbtn && smod && scls) { sbtn.addEventListener("click", () => smod.style.display = "flex"); scls.addEventListener("click", () => smod.style.display = "none"); } } function tickClock() { const c = $("#clock"); if (c) c.textContent = new Date().toTimeString().slice(0, 5); } for (const p of panels) p.mount?.(); initNav("overview"); bootToken(); tickClock(); document.addEventListener("mc:refresh", pollStatus); pollStatus(); pollJobs(); connectSystemStream(); setInterval(tickClock, 1000); setInterval(pollStatus, 3000); setInterval(pollJobs, 1500); pollUpdates(); setInterval(pollUpdates, 60000);