Mission Control v2 – Schritt 1: SoC-Refactor + Design 2.0

Architektur auf Separation of Concerns umgestellt – ohne Build-Schritt,
ohne neues Framework, ohne DB (KISS bleibt). Endpoint-URLs unveraendert,
daher 1:1-kompatibel zum bisherigen Stand.

Backend (Top-Level-Helfer + ein Router je Bereich):
- app.py auf duennen Einstieg reduziert (FastAPI + include_router + static)
- config/auth/jobengine/llamaswap als getrennte Helfer-Module
- Endpoints in routers/{models,jobs,maintenance}.py

Frontend (native ES-Module statt Single-File):
- index.html = Huelle: Sidebar-Nav, Topbar, Alert-Banner, Hash-Routing
- css/{base,components}.css – Tokens + Komponenten
- js/core/{api,ui,nav}.js + js/panels/{overview,models,maintenance,jobs}.js + main.js
- Panel-Vertrag: { id, mount?(), onStatus?(s), onJobs?(jobs) }
- Optik an docs/mission-control-overview.png angelehnt (Hero, KPI-Kacheln,
  Listen, Aktivitaets-Stream, getoente Karten)

Doku: CLAUDE.md + README auf die neue Struktur aktualisiert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-20 20:46:45 +02:00
parent e46380d99f
commit 364939466f
23 changed files with 1218 additions and 486 deletions
+24
View File
@@ -0,0 +1,24 @@
// api.js — zentraler Fetch-Wrapper + Token-Handling.
// Einziges Modul, das localStorage nutzt (nur fuers Token-Feld — laut Konvention ok).
let TOKEN = localStorage.getItem("mc_token") || "";
export function getToken() { return TOKEN; }
export function setToken(t) {
TOKEN = (t || "").trim();
localStorage.setItem("mc_token", TOKEN);
}
export function hdr() {
const h = { "Content-Type": "application/json" };
if (TOKEN) h["X-MC-Token"] = TOKEN;
return h;
}
export async function api(path, opts = {}) {
const r = await fetch(path, { headers: hdr(), ...opts });
const data = await r.json().catch(() => ({}));
if (!r.ok) throw new Error(data.error || ("HTTP " + r.status));
return data;
}
+28
View File
@@ -0,0 +1,28 @@
// nav.js — minimaler Hash-basierter View-Switch fuer die Sidebar.
// Zeigt genau eine .view[data-view] und markiert das aktive Nav-Item.
import { $$ } from "./ui.js";
export function initNav(defaultView = "overview") {
const items = $$(".nav-item[data-view]:not(.disabled)");
const views = $$(".view[data-view]");
const valid = new Set(views.map(v => v.dataset.view));
function show(view) {
if (!valid.has(view)) view = defaultView;
views.forEach(v => (v.hidden = v.dataset.view !== view));
items.forEach(i => i.classList.toggle("active", i.dataset.view === view));
if (location.hash !== "#/" + view) location.hash = "#/" + view;
}
items.forEach(i =>
i.addEventListener("click", e => {
e.preventDefault();
show(i.dataset.view);
})
);
window.addEventListener("hashchange", () => show(location.hash.replace("#/", "")));
show(location.hash.replace("#/", "") || defaultView);
return { show };
}
+59
View File
@@ -0,0 +1,59 @@
// ui.js — kleine DOM-Helfer, Toast, Badge, Escaping + Inline-Icon-Set.
// Bewusst kein Icon-Font / kein CDN: SVGs als Strings, faerben via currentColor.
export const $ = (s, r = document) => r.querySelector(s);
export const $$ = (s, r = document) => [...r.querySelectorAll(s)];
export function esc(s) {
return String(s ?? "").replace(/[&<>]/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" }[c]));
}
let _tt;
export function toast(msg, err = false) {
const t = $("#toast");
if (!t) return;
t.textContent = msg;
t.className = "toast show" + (err ? " err" : "");
clearTimeout(_tt);
_tt = setTimeout(() => (t.className = "toast"), 3200);
}
// Modell-Status -> Badge-HTML
export function badge(state) {
if (state === "running" || state === "ready") return '<span class="badge b-run">geladen</span>';
if (state === "loading" || state === "starting") return '<span class="badge b-load">lädt…</span>';
return '<span class="badge b-idle">bereit</span>';
}
// relative Zeit aus Unix-Sekunden (z.B. "2m", "13h")
export function ago(ts) {
if (!ts) return "";
const s = Math.max(0, Math.floor(Date.now() / 1000 - ts));
if (s < 60) return s + "s";
if (s < 3600) return Math.floor(s / 60) + "m";
if (s < 86400) return Math.floor(s / 3600) + "h";
return Math.floor(s / 86400) + "d";
}
const _svg = (p) =>
`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" ` +
`stroke-linecap="round" stroke-linejoin="round">${p}</svg>`;
// Icon-Set (Stroke-Style, an die Referenz angelehnt)
export const ICON = {
logo: _svg('<circle cx="12" cy="12" r="3.2"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3M5 5l2 2M17 17l2 2M19 5l-2 2M7 17l-2 2"/>'),
grid: _svg('<rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/>'),
cpu: _svg('<rect x="6" y="6" width="12" height="12" rx="2"/><path d="M9 1v3M15 1v3M9 20v3M15 20v3M1 9h3M1 15h3M20 9h3M20 15h3"/>'),
pulse: _svg('<path d="M3 12h4l2 6 4-14 2 8h6"/>'),
server: _svg('<rect x="3" y="4" width="18" height="7" rx="2"/><rect x="3" y="13" width="18" height="7" rx="2"/><path d="M7 7.5h.01M7 16.5h.01"/>'),
book: _svg('<path d="M4 5a2 2 0 0 1 2-2h13v16H6a2 2 0 0 0-2 2z"/><path d="M19 19H6a2 2 0 0 0-2 2"/>'),
help: _svg('<circle cx="12" cy="12" r="9"/><path d="M9.5 9.2a2.5 2.5 0 0 1 4.8 1c0 1.7-2.3 2-2.3 3.4"/><path d="M12 17h.01"/>'),
settings: _svg('<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.6 1.6 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.6 1.6 0 0 0-2.7 1.1V21a2 2 0 1 1-4 0v-.1A1.6 1.6 0 0 0 6.6 19l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.6 1.6 0 0 0-1.1-2.7H2a2 2 0 1 1 0-4h.1A1.6 1.6 0 0 0 4 6.6l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1A1.6 1.6 0 0 0 9 4.6V4a2 2 0 1 1 4 0v.1A1.6 1.6 0 0 0 17.4 6l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.6 1.6 0 0 0 1.1 2.7H21a2 2 0 1 1 0 4h-.1a1.6 1.6 0 0 0-1.5 1z"/>'),
swap: _svg('<path d="M7 4 3 8l4 4"/><path d="M3 8h12a4 4 0 0 1 0 8h-1"/><path d="m17 20 4-4-4-4"/>'),
monitor: _svg('<rect x="3" y="4" width="18" height="12" rx="2"/><path d="M8 20h8M12 16v4"/>'),
layers: _svg('<path d="m12 2 9 5-9 5-9-5z"/><path d="m3 12 9 5 9-5M3 17l9 5 9-5"/>'),
alert: _svg('<path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/><path d="M12 9v4M12 17h.01"/>'),
gauge: _svg('<path d="M12 14 16 9"/><circle cx="12" cy="13" r="9"/><path d="M12 4v2M21 13h-2M5 13H3"/>'),
};
export function icon(name) { return ICON[name] || ""; }