// 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);