82d15d82db
A1: 32768-Kontext-Cap entfernt (install-recipe, install-model, register) →
max_ctx_for() liefert nun bis zu 128k auf Strix Halo; behebt "context
size exceeded" bei externen Tools.
A2: Download-State jetzt im Status-Endpoint sichtbar: Modelle zeigen
"↓ Download X%" statt "bereit" während Job läuft (Backend + Frontend).
A3: PUT /api/cookbook/user-recipe/{id} + Edit-Button (✎) für eigene Setups.
Download-Modal setzt Kontext-Input automatisch auf optimal.
A4: /api/updates liefert apt_cache_age_h; Badge zeigt Tooltip + ⚠ wenn >24h.
A5: Swap-Flash: Topbar-Text pulst kurz teal wenn Modell den State wechselt.
A6: LLM-Engine-Update fragt jetzt per confirmModal nach (Konsistenz).
A7: Event-Delegation statt per-render addEventListener in models.js.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
196 lines
6.3 KiB
JavaScript
196 lines
6.3 KiB
JavaScript
// 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: <b style="color:var(--teal)">${active.name}</b>`;
|
||
} else {
|
||
$("#top-active-text").innerHTML = "Kein Modell im VRAM";
|
||
}
|
||
|
||
if (s.swap_ok) hideAlert();
|
||
else showAlert(`LLM-Engine nicht erreichbar unter <b>${host}</b> – 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 = `<span class="a-dot"></span><span>${html}</span>`;
|
||
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);
|