This repository has been archived on 2026-07-22. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
mission-control/static/js/main.js
T
Hitonabi 5560c18816 v3 Phase C: Security-Härtung + UX-Feedback
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>
2026-06-21 12:41:30 +02:00

139 lines
4.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);