5560c18816
Security: - Passwort-Leak dicht: os-update/reboot fuettern das sudo-Passwort ueber stdin (jobengine: neues stdin_data + sanitisierte Log-Zeile log_cmd). Verifiziert: Secret taucht nicht mehr im Job-Log/ps auf. - /api/status liefert "secured" -> Topbar-Security-Chip zeigt ehrlich, ob Token aktiv. - WS-Token-Auth funktioniert (Query-Param, war bereits in auth.py). - Bind bleibt 0.0.0.0 (kein Aussperren), kein Token erzwungen. UX-Feedback: - Schnellstart-Bug gefixt (Titel/Text brechen um, .qa-main column). - Cookbook: Sortier-Dropdown (Downloads/Likes/Aktualisiert/Trend) wie HuggingFace. - Server & Wartung: Buttons -> selbsterklaerende Aktionszeilen mit Beschreibung; Reboot nur mit rotem Icon (Vollrot-Bug behoben). - Live-Konsole: neue Option "System (ganzer Server)" -> journalctl ueber alle Units. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
139 lines
4.1 KiB
JavaScript
139 lines
4.1 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 } 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 guides from "./panels/guides.js";
|
||
|
||
const panels = [overview, models, server, jobs, cookbook, guides];
|
||
|
||
let lastJobs = [];
|
||
let lastSystem = null;
|
||
|
||
// ---- 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"; }
|
||
|
||
// ---- Polling ----
|
||
async function pollStatus() {
|
||
try { applyStatus(await api("/api/status")); }
|
||
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 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);
|