Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b76adc96e | |||
| e3be7fbfb5 |
+29
-1
@@ -43,6 +43,11 @@ class ChatReq(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
class UpdateReq(BaseModel):
|
||||
alias: str
|
||||
ctx: int
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Endpoints
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -61,11 +66,13 @@ def status():
|
||||
|
||||
size_bytes = None
|
||||
quant = ""
|
||||
filename = ""
|
||||
m_path = re.search(r'-(?:m|-model)\s+([^\s]+)', cmd)
|
||||
if m_path:
|
||||
path = m_path.group(1).replace("'", "").replace('"', '')
|
||||
if os.path.exists(path):
|
||||
size_bytes = os.path.getsize(path)
|
||||
filename = os.path.basename(path)
|
||||
q_match = re.search(r'(Q\d_[A-Z0-9_]+|IQ\d_[A-Z0-9_]+|fp16|bf16)\.gguf', path, flags=re.IGNORECASE)
|
||||
if q_match:
|
||||
quant = q_match.group(1).upper()
|
||||
@@ -86,7 +93,8 @@ def status():
|
||||
"ctx": ctx,
|
||||
"size_bytes": size_bytes,
|
||||
"quant": quant,
|
||||
"caps": caps
|
||||
"caps": caps,
|
||||
"filename": filename
|
||||
}
|
||||
}
|
||||
swap_ok = True
|
||||
@@ -141,6 +149,26 @@ def register(req: RegisterReq):
|
||||
"note": "In config.yaml geschrieben. llama-swap mit -watch-config laedt automatisch neu."}
|
||||
|
||||
|
||||
@router.post("/update_model")
|
||||
def update_model(req: UpdateReq):
|
||||
cfg = read_config()
|
||||
if req.alias not in cfg.get("models", {}):
|
||||
raise HTTPException(404, "Modell nicht gefunden")
|
||||
|
||||
spec = cfg["models"][req.alias]
|
||||
cmd = str(spec.get("cmd", ""))
|
||||
|
||||
# Replace or add context size
|
||||
if re.search(r'-(?:c|-ctx-size)\s+\d+', cmd):
|
||||
cmd = re.sub(r'-(?:c|-ctx-size)\s+\d+', f'-c {req.ctx}', cmd)
|
||||
else:
|
||||
cmd = cmd.strip() + f" -c {req.ctx}\n"
|
||||
|
||||
cfg["models"][req.alias]["cmd"] = LiteralScalarString(cmd)
|
||||
write_config(cfg)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.post("/unload")
|
||||
def unload(model: str | None = None):
|
||||
path = f"/api/models/unload/{model}" if model else "/api/models/unload"
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ a{color:var(--act);text-decoration:none}
|
||||
.tokin:focus{outline:none;border-color:var(--act)}
|
||||
|
||||
/* ---- Content-Bereich ---- */
|
||||
.content{padding:22px 26px 64px;max-width:1500px;width:100%}
|
||||
.content{padding:22px 26px 64px;max-width:1300px;margin:0 auto;width:100%}
|
||||
.view[hidden]{display:none}
|
||||
.view{display:flex;flex-direction:column;gap:18px}
|
||||
|
||||
|
||||
@@ -140,3 +140,20 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
opacity:0;transition:.25s;pointer-events:none;max-width:90vw;z-index:50}
|
||||
.toast.show{opacity:1}
|
||||
.toast.err{border-color:var(--err);color:#ffb4ae}
|
||||
|
||||
/* ---- Accordion (Guides) ---- */
|
||||
.guide-acc { border-bottom: 1px solid var(--line); }
|
||||
.guide-acc:last-child { border-bottom: none; }
|
||||
.guide-acc summary {
|
||||
padding: 16px 20px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.guide-acc summary::-webkit-details-marker { display: none; }
|
||||
.guide-acc summary::after { content: "▼"; font-size: 10px; color: var(--mut); transition: transform 0.2s; }
|
||||
.guide-acc[open] summary::after { transform: rotate(180deg); }
|
||||
.guide-acc .acc-body { padding: 0 20px 20px 20px; }
|
||||
|
||||
+23
-8
@@ -22,7 +22,7 @@
|
||||
<a class="nav-item" data-view="guides" title="Guides" data-ic="help"></a>
|
||||
</nav>
|
||||
<div class="side-foot">
|
||||
<span class="nav-item disabled" title="Einstellungen (bald)" data-ic="settings"></span>
|
||||
<span class="nav-item" title="Einstellungen" data-ic="settings" id="nav-settings"></span>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -31,10 +31,7 @@
|
||||
<header class="topbar">
|
||||
<span class="status-pill"><span id="swdot" class="dot"></span><span id="swlabel">verbinde…</span></span>
|
||||
<span class="spacer"></span>
|
||||
<span class="top-stat">Modelle<b id="top-models">–</b></span>
|
||||
<span class="top-stat">Jobs<b id="top-jobs">0</b></span>
|
||||
<span class="top-clock" id="clock">--:--</span>
|
||||
<input id="token" class="tokin" placeholder="Token" autocomplete="off">
|
||||
<span class="top-stat" id="top-active-text" style="color:var(--teal)">Kein Modell geladen</span>
|
||||
</header>
|
||||
|
||||
<!-- Alert-Banner (wird per JS ein-/ausgeblendet) -->
|
||||
@@ -45,14 +42,18 @@
|
||||
|
||||
<section class="view" data-view="overview">
|
||||
<div id="hero"></div>
|
||||
<div class="grid kpis" id="kpis"></div>
|
||||
<div class="grid grid-2">
|
||||
<div class="card" id="health"></div>
|
||||
|
||||
<div class="grid grid-3" id="ov-quick" style="margin-top: 24px"></div>
|
||||
|
||||
<div class="grid grid-2" style="margin-top: 24px; align-items: start;">
|
||||
<div class="card" id="ov-models"></div>
|
||||
<div class="card" id="ov-recent-jobs"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="view" data-view="activity" hidden>
|
||||
<div class="grid kpis" id="act-kpis"></div>
|
||||
<div class="card" id="act-sys"></div>
|
||||
<div class="card" id="v-activity"></div>
|
||||
</section>
|
||||
|
||||
@@ -79,6 +80,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Modal -->
|
||||
<div id="settings-modal" style="display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.8); z-index:100; align-items:center; justify-content:center;">
|
||||
<div class="card" style="width:100%; max-width:400px; position:relative">
|
||||
<button id="sm-close" class="ghost" style="position:absolute; top:12px; right:12px;">Schließen</button>
|
||||
<h2 style="margin-top:0">Einstellungen</h2>
|
||||
|
||||
<div style="margin-top:24px">
|
||||
<label>API Token (Authentifizierung)</label>
|
||||
<input id="token" class="tokin" placeholder="Optionales API Token..." autocomplete="off">
|
||||
<div class="meta" style="font-size:12px; margin-top:4px;">Wird für die WebSockets und API Calls genutzt, falls der Server geschützt ist.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toast" class="toast"></div>
|
||||
|
||||
<!-- Icons in die Nav/Logo einsetzen, bevor das Haupt-Modul laedt -->
|
||||
|
||||
@@ -54,6 +54,10 @@ export const ICON = {
|
||||
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"/>'),
|
||||
search: _svg('<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>'),
|
||||
compass: _svg('<circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/>'),
|
||||
code: _svg('<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>'),
|
||||
eye: _svg('<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>'),
|
||||
};
|
||||
|
||||
export function icon(name) { return ICON[name] || ""; }
|
||||
|
||||
+25
-9
@@ -24,22 +24,28 @@ function applyStatus(s) {
|
||||
if (!s) {
|
||||
dot.className = "dot off";
|
||||
label.textContent = "Backend nicht erreichbar";
|
||||
$("#top-models").textContent = "–";
|
||||
showAlert("Backend nicht erreichbar — läuft uvicorn?", false);
|
||||
$("#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 ? "llama-swap online · " : "llama-swap offline · ") + host;
|
||||
$("#top-models").textContent = (s.models || []).length;
|
||||
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(`llama-swap nicht erreichbar unter <b>${host}</b> — läuft der Dienst?`, true);
|
||||
else showAlert(`LLM-Engine nicht erreichbar unter <b>${host}</b> – läuft der llama-swap Dienst?`, true);
|
||||
}
|
||||
for (const p of panels) p.onStatus?.(s);
|
||||
}
|
||||
|
||||
function applyJobs(jobs) {
|
||||
lastJobs = jobs || [];
|
||||
$("#top-jobs").textContent = lastJobs.filter(j => j.state === "running" || j.state === "queued").length;
|
||||
for (const p of panels) p.onJobs?.(lastJobs);
|
||||
}
|
||||
|
||||
@@ -74,8 +80,18 @@ async function pollSystem() {
|
||||
// ---- Boot ----
|
||||
function bootToken() {
|
||||
const i = $("#token");
|
||||
i.value = getToken();
|
||||
i.addEventListener("change", e => { setToken(e.target.value); pollStatus(); });
|
||||
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() {
|
||||
$("#clock").textContent = new Date().toTimeString().slice(0, 5);
|
||||
@@ -93,4 +109,4 @@ pollSystem();
|
||||
setInterval(tickClock, 1000);
|
||||
setInterval(pollStatus, 3000);
|
||||
setInterval(pollJobs, 1500);
|
||||
setInterval(pollSystem, 3000);
|
||||
setInterval(pollSystem, 2000);
|
||||
|
||||
+248
-148
@@ -1,17 +1,15 @@
|
||||
import { api } from "../core/api.js";
|
||||
import { $ } from "../core/ui.js";
|
||||
import { $, esc, icon, toast } from "../core/ui.js";
|
||||
import { track } from "./jobs.js";
|
||||
|
||||
const COOKBOOK_MODELS = [
|
||||
const CURATED_MODELS = [
|
||||
{
|
||||
id: "qwen-coder-32b",
|
||||
name: "Qwen 2.5 Coder 32B",
|
||||
repo: "unsloth/Qwen2.5-Coder-32B-Instruct-GGUF",
|
||||
file: "Qwen2.5-Coder-32B-Instruct-Q4_K_M.gguf",
|
||||
desc: "Top-Tier lokales Coder-Modell. Braucht viel Speicher.",
|
||||
params_b: 32,
|
||||
quant: "Q4_K_M",
|
||||
ctx: 32768,
|
||||
alias: "coder"
|
||||
desc: "Top-Tier lokales Coder-Modell. Braucht ca. 24GB VRAM.",
|
||||
params_b: 32, quant: "Q4_K_M", ctx: 32768, alias: "coder"
|
||||
},
|
||||
{
|
||||
id: "qwen-coder-7b",
|
||||
@@ -19,10 +17,7 @@ const COOKBOOK_MODELS = [
|
||||
repo: "unsloth/Qwen2.5-Coder-7B-Instruct-GGUF",
|
||||
file: "Qwen2.5-Coder-7B-Instruct-Q4_K_M.gguf",
|
||||
desc: "Schneller Coder. Perfekte Balance aus Speed und Qualität.",
|
||||
params_b: 7,
|
||||
quant: "Q4_K_M",
|
||||
ctx: 32768,
|
||||
alias: "coder-fast"
|
||||
params_b: 7, quant: "Q4_K_M", ctx: 32768, alias: "coder-fast"
|
||||
},
|
||||
{
|
||||
id: "llama3-vision-11b",
|
||||
@@ -30,10 +25,7 @@ const COOKBOOK_MODELS = [
|
||||
repo: "unsloth/Llama-3.2-11B-Vision-Instruct-GGUF",
|
||||
file: "Llama-3.2-11B-Vision-Instruct-Q4_K_M.gguf",
|
||||
desc: "Modell für Bilderkennung und multimodale Tasks.",
|
||||
params_b: 11,
|
||||
quant: "Q4_K_M",
|
||||
ctx: 8192,
|
||||
alias: "vision"
|
||||
params_b: 11, quant: "Q4_K_M", ctx: 8192, alias: "vision"
|
||||
},
|
||||
{
|
||||
id: "qwen-general-7b",
|
||||
@@ -41,185 +33,293 @@ const COOKBOOK_MODELS = [
|
||||
repo: "unsloth/Qwen2.5-7B-Instruct-GGUF",
|
||||
file: "Qwen2.5-7B-Instruct-Q4_K_M.gguf",
|
||||
desc: "Hervorragendes Generalist/Scout Modell.",
|
||||
params_b: 7,
|
||||
quant: "Q4_K_M",
|
||||
ctx: 8192,
|
||||
alias: "scout"
|
||||
params_b: 7, quant: "Q4_K_M", ctx: 8192, alias: "scout"
|
||||
}
|
||||
];
|
||||
|
||||
// Odysseus Hardware Fit Logic
|
||||
function estimateMemoryGB(params_b, quant, ctx) {
|
||||
// Q4_K_M is roughly 0.6 bytes per param
|
||||
const bpp = 0.6;
|
||||
const weights = params_b * bpp;
|
||||
// Context roughly: 1GB per 8K for 7B
|
||||
const context = (ctx / 8192) * (params_b / 7) * 0.8;
|
||||
return weights + context;
|
||||
}
|
||||
|
||||
function getFit(m, sys) {
|
||||
const req = estimateMemoryGB(m.params_b, m.quant, m.ctx);
|
||||
// Falls keine sys-Daten da sind (Backend mock), nimm Standardwerte an
|
||||
const vram_bytes = sys?.gpu?.vram?.total || 0;
|
||||
const vram = vram_bytes / (1024 ** 3);
|
||||
const ram_bytes = sys?.ram?.total || 0;
|
||||
const ram_used = sys?.ram?.used || 0;
|
||||
const ram = (ram_bytes) / (1024 ** 3);
|
||||
const ram = ram_bytes / (1024 ** 3);
|
||||
const freeRam = (ram_bytes - ram_used) / (1024 ** 3);
|
||||
|
||||
// Wenn gar keine echten Metriken kommen (Windows Dummy Backend), immer "Fits" für Demo
|
||||
if (vram === 0 && ram === 0) return { level: "perfect", class: "green", text: "Fits (Mock)", req };
|
||||
|
||||
if (vram > 0 && req <= vram) return { level: "perfect", class: "green", text: "Fits VRAM", req };
|
||||
if (req <= (vram + freeRam)) return { level: "good", class: "yellow", text: "RAM Offload", req };
|
||||
return { level: "too_tight", class: "red", text: "OOM (Zu groß)", req };
|
||||
if (vram === 0 && ram === 0) return { level: "perfect", class: "b-run", text: "Fits (Mock)", req };
|
||||
if (vram > 0 && req <= vram) return { level: "perfect", class: "b-run", text: "Fits VRAM", req };
|
||||
if (req <= (vram + freeRam)) return { level: "good", class: "b-load", text: "RAM Offload", req };
|
||||
return { level: "too_tight", class: "b-err", text: "OOM (Zu groß)", req };
|
||||
}
|
||||
|
||||
let lastSys = null;
|
||||
let dlPoll = null;
|
||||
let currentResults = [];
|
||||
|
||||
function mount() {
|
||||
render();
|
||||
}
|
||||
|
||||
function unmount() {
|
||||
if (dlPoll) clearInterval(dlPoll);
|
||||
}
|
||||
|
||||
function onSystem(sys) {
|
||||
lastSys = sys;
|
||||
renderGrid();
|
||||
}
|
||||
|
||||
export default { mount, unmount, onSystem };
|
||||
|
||||
function render() {
|
||||
const c = document.querySelector(".view[data-view='cookbook']");
|
||||
const c = $(".view[data-view='cookbook']");
|
||||
c.innerHTML = `
|
||||
<div class="card" style="padding-bottom:32px">
|
||||
<div style="max-width:600px; margin:0 auto; text-align:center;">
|
||||
<h2 style="margin-top:20px; margin-bottom:8px">HuggingFace Suche</h2>
|
||||
<p class="meta" style="margin-bottom:24px">Finde GGUF-Modelle direkt auf HuggingFace und lade sie in Mission Control herunter.</p>
|
||||
|
||||
<div style="display:flex; gap:12px; position:relative">
|
||||
<input id="cb-search" class="tokin" style="flex:1; padding:12px 18px; font-size:16px; border-radius:12px;" placeholder="Nach Modellen suchen (z.B. Llama 3)...">
|
||||
<button class="primary" id="cb-btn-search" style="border-radius:12px; padding:0 24px;">Suchen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-h" style="margin-top:24px"><h3 id="cb-section-title">Kuratierte Empfehlungen</h3></div>
|
||||
<div class="grid grid-3" id="cb-grid"></div>
|
||||
|
||||
<div class="card" style="margin-top: 24px;">
|
||||
<div class="card-h"><h3>Custom Download</h3></div>
|
||||
<div style="display:flex; gap:12px; margin-bottom:12px;">
|
||||
<input id="cb-repo" class="tokin" placeholder="HuggingFace Repo (z.B. unsloth/Qwen2.5-Coder-7B-Instruct-GGUF)" style="flex:2">
|
||||
<input id="cb-file" class="tokin" placeholder="Dateipfad (z.B. Qwen2.5-Coder-7B-Instruct-Q4_K_M.gguf)" style="flex:1">
|
||||
<button class="btn" id="cb-btn-dl">Herunterladen</button>
|
||||
</div>
|
||||
<div id="cb-dl-prog" style="display:none; color:var(--text-dim); font-size:12px; margin-bottom:12px;"></div>
|
||||
<div id="cb-register-box" style="display:none; padding-top:12px; border-top:1px solid var(--border);">
|
||||
<div style="margin-bottom:8px;">Modell heruntergeladen! Jetzt einpflegen:</div>
|
||||
<div style="display:flex; gap:12px;">
|
||||
<input id="cb-reg-alias" class="tokin" placeholder="Alias (z.B. coder)" style="flex:1">
|
||||
<input id="cb-reg-path" class="tokin" placeholder="Modell-Pfad" style="flex:2" readonly>
|
||||
<input id="cb-reg-ctx" class="tokin" placeholder="Ctx (z.B. 8192)" type="number" style="width:100px;">
|
||||
<button class="btn" id="cb-btn-reg">Einpflegen</button>
|
||||
<!-- Modal für HuggingFace Modell Details -->
|
||||
<div id="cb-modal" style="display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.8); z-index:100; align-items:center; justify-content:center;">
|
||||
<div class="card" style="width:100%; max-width:600px; max-height:80vh; overflow-y:auto; position:relative">
|
||||
<button id="cb-modal-close" class="ghost" style="position:absolute; top:12px; right:12px;">Schließen</button>
|
||||
<h2 id="cb-m-title" style="margin-top:0">Modell</h2>
|
||||
<p class="meta" id="cb-m-repo">repo/name</p>
|
||||
|
||||
<div style="margin-top:24px">
|
||||
<label>Wähle eine Quantisierung (GGUF-Datei)</label>
|
||||
<select id="cb-m-files" class="tokin" style="width:100%; margin-top:8px; padding:12px"></select>
|
||||
<div id="cb-m-loading" class="meta" style="margin-top:8px; display:none">Lade Dateien...</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:24px; display:flex; gap:12px">
|
||||
<div style="flex:1">
|
||||
<label>Alias (Rolle)</label>
|
||||
<input id="cb-m-alias" class="tokin" style="width:100%; margin-top:8px" placeholder="z.B. coder">
|
||||
</div>
|
||||
<div style="flex:1">
|
||||
<label>Context-Size</label>
|
||||
<input id="cb-m-ctx" class="tokin" style="width:100%; margin-top:8px" value="8192" type="number">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="cb-m-fit-container" style="margin-top:24px; padding:16px; background:var(--bg); border:1px solid var(--line); border-radius:8px; display:flex; justify-content:space-between; align-items:center;">
|
||||
<div>
|
||||
<div style="font-weight:600; font-size:14px; margin-bottom:4px;">Ressourcen-Check</div>
|
||||
<div class="meta" id="cb-m-fit-text">Berechne...</div>
|
||||
</div>
|
||||
<div id="cb-m-fit-badge"></div>
|
||||
</div>
|
||||
|
||||
<button class="primary" id="cb-m-download" style="width:100%; margin-top:24px; padding:12px">Herunterladen & Einpflegen</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById("cb-btn-dl").onclick = () => {
|
||||
startDownload(
|
||||
document.getElementById("cb-repo").value,
|
||||
document.getElementById("cb-file").value,
|
||||
"", 8192
|
||||
);
|
||||
};
|
||||
$("#cb-btn-search").addEventListener("click", doSearch);
|
||||
$("#cb-search").addEventListener("keydown", e => { if (e.key === "Enter") doSearch(); });
|
||||
$("#cb-modal-close").addEventListener("click", () => $("#cb-modal").style.display = "none");
|
||||
$("#cb-m-download").addEventListener("click", doDownload);
|
||||
|
||||
document.getElementById("cb-btn-reg").onclick = async () => {
|
||||
const alias = document.getElementById("cb-reg-alias").value;
|
||||
const path = document.getElementById("cb-reg-path").value;
|
||||
const ctx = document.getElementById("cb-reg-ctx").value;
|
||||
if (!alias || !path) return;
|
||||
try {
|
||||
await api("/api/models/register", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ alias, model_path: path, ctx_len: parseInt(ctx)||4096 })
|
||||
});
|
||||
alert("Erfolgreich eingepflegt!");
|
||||
document.getElementById("cb-register-box").style.display = "none";
|
||||
} catch(e) {
|
||||
alert("Fehler: " + e);
|
||||
}
|
||||
};
|
||||
$("#cb-m-files").addEventListener("change", updateLiveFit);
|
||||
$("#cb-m-ctx").addEventListener("input", updateLiveFit);
|
||||
|
||||
renderGrid();
|
||||
renderCurated();
|
||||
}
|
||||
|
||||
function renderGrid() {
|
||||
const grid = document.getElementById("cb-grid");
|
||||
function extractParamsB(name) {
|
||||
const moe = name.match(/(\d+)x(\d+(?:\.\d+)?)[bB]/i);
|
||||
if (moe) return parseInt(moe[1]) * parseFloat(moe[2]);
|
||||
const m = name.match(/(\d+(?:\.\d+)?)[bB](?![a-zA-Z])/i);
|
||||
if (m) return parseFloat(m[1]);
|
||||
return 7; // Fallback
|
||||
}
|
||||
|
||||
function extractQuant(filename) {
|
||||
const m = filename.match(/(Q\d_[A-Z0-9_]+|IQ\d_[A-Z0-9_]+|FP16|BF16)/i);
|
||||
return m ? m[1].toUpperCase() : "Q4_K_M";
|
||||
}
|
||||
|
||||
function updateLiveFit() {
|
||||
const repo = $("#cb-m-repo").textContent;
|
||||
const file = $("#cb-m-files").value;
|
||||
const ctx = parseInt($("#cb-m-ctx").value) || 8192;
|
||||
|
||||
if (!repo || !file) {
|
||||
$("#cb-m-fit-container").style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
const params_b = extractParamsB(repo);
|
||||
const quant = extractQuant(file);
|
||||
|
||||
const m = { params_b, quant, ctx };
|
||||
const fit = getFit(m, lastSys);
|
||||
|
||||
$("#cb-m-fit-container").style.display = "flex";
|
||||
$("#cb-m-fit-text").innerHTML = `Geschätzter Bedarf: <b>~${fit.req.toFixed(1)} GB RAM/VRAM</b> <br><small class="meta">${params_b}B Params · ${quant}</small>`;
|
||||
$("#cb-m-fit-badge").innerHTML = `<span class="badge ${fit.class}">${fit.text}</span>`;
|
||||
}
|
||||
|
||||
async function doSearch() {
|
||||
const q = $("#cb-search").value.trim();
|
||||
if (!q) return renderCurated();
|
||||
|
||||
const btn = $("#cb-btn-search");
|
||||
btn.disabled = true; btn.textContent = "Lade...";
|
||||
$("#cb-section-title").textContent = "Suchergebnisse für: " + esc(q);
|
||||
$("#cb-grid").innerHTML = `<div class="meta" style="grid-column:1/-1; text-align:center; padding:40px">Suche auf HuggingFace...</div>`;
|
||||
|
||||
try {
|
||||
const url = `https://huggingface.co/api/models?search=${encodeURIComponent(q)}&filter=gguf&sort=downloads&direction=-1&limit=12`;
|
||||
const r = await fetch(url);
|
||||
const data = await r.json();
|
||||
currentResults = data;
|
||||
renderResults(data);
|
||||
} catch (e) {
|
||||
$("#cb-grid").innerHTML = `<div class="alert err" style="grid-column:1/-1">${esc(e.message)}</div>`;
|
||||
}
|
||||
btn.disabled = false; btn.textContent = "Suchen";
|
||||
}
|
||||
|
||||
function renderResults(results) {
|
||||
const grid = $("#cb-grid");
|
||||
if (!results || results.length === 0) {
|
||||
grid.innerHTML = `<div class="meta" style="grid-column:1/-1; text-align:center; padding:40px">Keine GGUF-Modelle gefunden.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = results.map((m, i) => `
|
||||
<div class="card" style="display:flex; flex-direction:column; cursor:pointer" onclick="window.openModelModal(${i})">
|
||||
<h3 style="margin:0; font-size:16px; word-break:break-all">${esc(m.id.split('/').pop())}</h3>
|
||||
<div class="meta" style="font-size:12px; margin-top:4px">${esc(m.author)}</div>
|
||||
<div style="flex:1; margin-top:16px;"></div>
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-top:16px">
|
||||
<span class="badge b-idle" style="font-size:11px">GGUF</span>
|
||||
<span class="meta" style="font-size:12px">⬇ ${(m.downloads||0).toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join("");
|
||||
}
|
||||
|
||||
// Global hook for inline onclick
|
||||
window.openModelModal = async (index) => {
|
||||
const m = currentResults[index];
|
||||
if (!m) return;
|
||||
$("#cb-modal").style.display = "flex";
|
||||
$("#cb-m-title").textContent = m.id.split('/').pop();
|
||||
$("#cb-m-repo").textContent = m.id;
|
||||
$("#cb-m-files").innerHTML = "";
|
||||
$("#cb-m-alias").value = m.id.split('/').pop().toLowerCase().replace(/[^a-z0-9]/g, "-");
|
||||
|
||||
$("#cb-m-files").style.display = "none";
|
||||
$("#cb-m-loading").style.display = "block";
|
||||
|
||||
try {
|
||||
const url = `https://huggingface.co/api/models/${m.id}/tree/main`;
|
||||
const r = await fetch(url);
|
||||
const tree = await r.json();
|
||||
const files = tree.filter(f => f.path.endsWith('.gguf')).map(f => f.path);
|
||||
|
||||
$("#cb-m-loading").style.display = "none";
|
||||
$("#cb-m-files").style.display = "block";
|
||||
|
||||
if (files.length === 0) {
|
||||
$("#cb-m-files").innerHTML = "<option value=''>Keine GGUF-Dateien im Hauptverzeichnis gefunden.</option>";
|
||||
$("#cb-m-download").disabled = true;
|
||||
$("#cb-m-fit-container").style.display = "none";
|
||||
} else {
|
||||
$("#cb-m-files").innerHTML = files.map(f => `<option value="${esc(f)}">${esc(f)}</option>`).join("");
|
||||
$("#cb-m-download").disabled = false;
|
||||
updateLiveFit();
|
||||
}
|
||||
} catch(e) {
|
||||
$("#cb-m-loading").textContent = "Fehler beim Laden der Dateien.";
|
||||
}
|
||||
};
|
||||
|
||||
async function doDownload() {
|
||||
const repo = $("#cb-m-repo").textContent;
|
||||
const file = $("#cb-m-files").value;
|
||||
const alias = $("#cb-m-alias").value.trim();
|
||||
const ctx = parseInt($("#cb-m-ctx").value) || 8192;
|
||||
|
||||
if (!repo || !file || !alias) return toast("Bitte alle Felder ausfüllen.", true);
|
||||
|
||||
$("#cb-m-download").disabled = true;
|
||||
$("#cb-m-download").textContent = "Starte...";
|
||||
|
||||
try {
|
||||
const res = await api("/api/download", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ repo, file })
|
||||
});
|
||||
|
||||
// Register directly, jobengine will download it, MC will pick it up when done
|
||||
await api("/api/register", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ alias, model_path: res.expected_path, ctx })
|
||||
});
|
||||
|
||||
toast("Download gestartet! Siehe Aktivitäten.");
|
||||
$("#cb-modal").style.display = "none";
|
||||
track(res.job_id); // open in jobs
|
||||
// Switch to activity tab
|
||||
document.querySelector(".nav-item[data-view='activity']").click();
|
||||
|
||||
} catch (e) {
|
||||
toast("Fehler: " + e.message, true);
|
||||
}
|
||||
|
||||
$("#cb-m-download").disabled = false;
|
||||
$("#cb-m-download").textContent = "Herunterladen & Einpflegen";
|
||||
}
|
||||
|
||||
function renderCurated() {
|
||||
$("#cb-section-title").textContent = "Kuratierte Empfehlungen";
|
||||
const grid = $("#cb-grid");
|
||||
if (!grid) return;
|
||||
grid.innerHTML = COOKBOOK_MODELS.map(m => {
|
||||
grid.innerHTML = CURATED_MODELS.map((m, i) => {
|
||||
const fit = getFit(m, lastSys);
|
||||
return `
|
||||
<div class="card" style="display:flex; flex-direction:column;">
|
||||
<div class="card" style="display:flex; flex-direction:column; cursor:pointer" onclick="window.openCuratedModal(${i})">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<h3 style="margin:0">${m.name}</h3>
|
||||
<span style="font-size:11px; padding:2px 6px; border-radius:4px; background:${fit.class === 'green' ? 'rgba(0,255,0,0.1)' : fit.class === 'yellow' ? 'rgba(255,255,0,0.1)' : 'rgba(255,0,0,0.1)'}; color:${fit.class === 'green' ? '#4ade80' : fit.class === 'yellow' ? '#fde047' : '#f87171'};">
|
||||
${fit.text}
|
||||
</span>
|
||||
<h3 style="margin:0; font-size:16px">${esc(m.name)}</h3>
|
||||
<span class="badge ${fit.class}">${fit.text}</span>
|
||||
</div>
|
||||
<div style="font-size:12px; color:var(--text-dim); margin-top:8px; flex:1;">
|
||||
${m.desc}<br><br>
|
||||
<b>Größe:</b> ~${fit.req.toFixed(1)} GB (inkl. Context)<br>
|
||||
<b>Quant:</b> ${m.quant}
|
||||
<div style="font-size:13px; color:var(--mut); margin-top:12px; flex:1; line-height:1.5;">
|
||||
${m.desc}
|
||||
</div>
|
||||
<div style="display:flex; justify-content:space-between; margin-top:16px; font-size:12px" class="meta">
|
||||
<span>~${fit.req.toFixed(1)} GB RAM</span>
|
||||
<span>${m.quant}</span>
|
||||
</div>
|
||||
<button class="btn cb-btn-preset" data-id="${m.id}" style="margin-top:16px; width:100%;">Herunterladen</button>
|
||||
</div>
|
||||
`;
|
||||
}).join("");
|
||||
|
||||
document.querySelectorAll(".cb-btn-preset").forEach(b => {
|
||||
b.onclick = () => {
|
||||
const m = COOKBOOK_MODELS.find(x => x.id === b.dataset.id);
|
||||
if(m) {
|
||||
document.getElementById("cb-repo").value = m.repo;
|
||||
document.getElementById("cb-file").value = m.file;
|
||||
startDownload(m.repo, m.file, m.alias, m.ctx);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function startDownload(repo, file, autoAlias, autoCtx) {
|
||||
if(!repo || !file) return;
|
||||
document.getElementById("cb-dl-prog").style.display = "block";
|
||||
document.getElementById("cb-dl-prog").innerText = "Starte Download...";
|
||||
document.getElementById("cb-register-box").style.display = "none";
|
||||
window.openCuratedModal = (index) => {
|
||||
const m = CURATED_MODELS[index];
|
||||
if (!m) return;
|
||||
$("#cb-modal").style.display = "flex";
|
||||
$("#cb-m-title").textContent = m.name;
|
||||
$("#cb-m-repo").textContent = m.repo;
|
||||
$("#cb-m-files").innerHTML = `<option value="${esc(m.file)}">${esc(m.file)}</option>`;
|
||||
$("#cb-m-files").style.display = "block";
|
||||
$("#cb-m-loading").style.display = "none";
|
||||
$("#cb-m-alias").value = m.alias;
|
||||
$("#cb-m-ctx").value = m.ctx;
|
||||
$("#cb-m-download").disabled = false;
|
||||
updateLiveFit();
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await api("/api/models/download", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ repo_id: repo, filename: file })
|
||||
});
|
||||
if (res.job_id) {
|
||||
if(dlPoll) clearInterval(dlPoll);
|
||||
dlPoll = setInterval(() => checkProg(res.job_id, autoAlias, autoCtx), 2000);
|
||||
}
|
||||
} catch(e) {
|
||||
document.getElementById("cb-dl-prog").innerText = "Fehler: " + e;
|
||||
}
|
||||
function onSystem(sys) {
|
||||
lastSys = sys;
|
||||
if (!$("#cb-search").value.trim()) renderCurated();
|
||||
}
|
||||
|
||||
async function checkProg(jobId, autoAlias, autoCtx) {
|
||||
try {
|
||||
const res = await api("/api/jobs");
|
||||
const j = res.find(x => x.id === jobId);
|
||||
if (!j) {
|
||||
clearInterval(dlPoll);
|
||||
document.getElementById("cb-dl-prog").innerText = "Job verschwunden.";
|
||||
return;
|
||||
}
|
||||
document.getElementById("cb-dl-prog").innerText = j.status + (j.error ? (" - " + j.error) : "");
|
||||
if (j.status === "done") {
|
||||
clearInterval(dlPoll);
|
||||
document.getElementById("cb-register-box").style.display = "block";
|
||||
document.getElementById("cb-reg-path").value = j.result || "";
|
||||
if(autoAlias) document.getElementById("cb-reg-alias").value = autoAlias;
|
||||
if(autoCtx) document.getElementById("cb-reg-ctx").value = autoCtx;
|
||||
} else if (j.status === "failed") {
|
||||
clearInterval(dlPoll);
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
function unmount() {}
|
||||
|
||||
export default { id: "cookbook", mount, unmount, onSystem };
|
||||
|
||||
+61
-43
@@ -7,38 +7,48 @@ function render() {
|
||||
if (!c) return;
|
||||
|
||||
c.innerHTML = `
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-h"><h3>Cline / Cursor</h3></div>
|
||||
<p>Nutze llama-swap als lokalen OpenAI-kompatiblen Provider in Cursor oder Cline.</p>
|
||||
<label>API Provider</label>
|
||||
<input class="tokin mono-sm" readonly value="OpenAI Compatible">
|
||||
<label>Base URL</label>
|
||||
<input class="tokin mono-sm" readonly value="${currentUrl}/v1">
|
||||
<label>API Key</label>
|
||||
<input class="tokin mono-sm" readonly value="Dein_Mission_Control_Token_oder_leer">
|
||||
</div>
|
||||
<div class="card-h"><h3>Guides & Integrationen</h3></div>
|
||||
<div class="card" style="padding:0; overflow:hidden;">
|
||||
|
||||
<div class="card">
|
||||
<div class="card-h"><h3>OpenWebUI</h3></div>
|
||||
<p>Füge llama-swap in OpenWebUI als externe OpenAI-Verbindung hinzu.</p>
|
||||
<label>Settings -> Connections -> OpenAI API</label>
|
||||
<input class="tokin mono-sm" readonly value="${currentUrl}/v1">
|
||||
<label>API Key</label>
|
||||
<input class="tokin mono-sm" readonly value="dummy-key">
|
||||
<p style="margin-top:12px; font-size:12px; color:var(--text-dim);">
|
||||
OpenWebUI zieht sich danach automatisch alle geladenen Modelle.
|
||||
</p>
|
||||
</div>
|
||||
<details class="guide-acc">
|
||||
<summary>Cline / Cursor (Juni 2026)</summary>
|
||||
<div class="acc-body">
|
||||
<p>Nutze deine lokalen Modelle kostenlos in der Cursor IDE und Cline.</p>
|
||||
<label>Cursor: Settings -> Models -> Add Custom Model</label>
|
||||
<input class="tokin mono-sm" readonly value="coder">
|
||||
<label>Base URL (OpenAI API)</label>
|
||||
<input class="tokin mono-sm" readonly value="${currentUrl}/v1">
|
||||
<label>API Key</label>
|
||||
<input class="tokin mono-sm" readonly value="MissionControl-Token-oder-leer">
|
||||
<p style="margin-top:12px; font-size:12px; color:var(--mut);">
|
||||
In Cline: Wähle "OpenAI Compatible" als Provider.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="card" style="grid-column: span 2;">
|
||||
<div class="card-h"><h3>Python / LangChain (OpenAI SDK)</h3></div>
|
||||
<p>Nutze das offizielle <code>openai</code> Package, um mit llama-swap zu sprechen.</p>
|
||||
<pre style="background: var(--bg); padding: 12px; border-radius: 8px; border: 1px solid var(--border); overflow-x: auto; color: var(--text); font-family: monospace; font-size: 13px;"><code>from openai import OpenAI
|
||||
<details class="guide-acc">
|
||||
<summary>OpenWebUI (Pipes & Connections)</summary>
|
||||
<div class="acc-body">
|
||||
<p>Verbinde llama-swap nativ in OpenWebUI.</p>
|
||||
<label>Settings -> Admin Settings -> Connections</label>
|
||||
<input class="tokin mono-sm" readonly value="${currentUrl}/v1">
|
||||
<label>API Key</label>
|
||||
<input class="tokin mono-sm" readonly value="dummy-key">
|
||||
<p style="margin-top:12px; font-size:12px; color:var(--mut);">
|
||||
OpenWebUI erkennt nun vollautomatisch, wenn ein Modell ausgetauscht wird.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="guide-acc">
|
||||
<summary>Python / LangChain (OpenAI SDK)</summary>
|
||||
<div class="acc-body">
|
||||
<p>Nutze das offizielle <code>openai</code> Package, um mit llama-swap zu sprechen.</p>
|
||||
<pre style="background: var(--bg); padding: 12px; border-radius: 8px; border: 1px solid var(--line); overflow-x: auto; color: var(--tx); font-family: monospace; font-size: 13px;"><code>from openai import OpenAI
|
||||
|
||||
client = OpenAI(
|
||||
base_url="${currentUrl}/v1",
|
||||
api_key="Dein_Token" # Optional, falls in Mission Control konfiguriert
|
||||
api_key="Dein_Token" # Optional
|
||||
)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
@@ -47,23 +57,31 @@ response = client.chat.completions.create(
|
||||
)
|
||||
|
||||
print(response.choices[0].message.content)</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="guide-acc">
|
||||
<summary>n8n (AI Agent Nodes)</summary>
|
||||
<div class="acc-body">
|
||||
<p>Nutze den "OpenAI Chat Model" Node in n8n Advanced AI.</p>
|
||||
<label>Credentials -> OpenAI API -> Custom URL</label>
|
||||
<input class="tokin mono-sm" readonly value="${currentUrl}/v1">
|
||||
<p style="margin-top:12px; font-size:12px; color:var(--mut);">
|
||||
Einfach den Node verbinden, "coder" als Modell-ID (Expression) eintippen und loslegen.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="guide-acc">
|
||||
<summary>Begrifflichkeiten (Glossar)</summary>
|
||||
<div class="acc-body">
|
||||
<p><b>LLM-Engine (llama-swap):</b> Der Server im Hintergrund, der die Sprachmodelle (LLMs) lädt und OpenAI-kompatible Schnittstellen bereitstellt.</p>
|
||||
<p><b>VRAM:</b> Der Grafikkarten-Speicher. KI-Modelle sind extrem groß und benötigen viel VRAM, um schnell zu laufen.</p>
|
||||
<p><b>Quantisierung (z.B. Q4_K_M):</b> Ein Verfahren, das die Genauigkeit der Modellgewichte leicht reduziert (von 16-Bit auf 4-Bit), damit sie in den VRAM passen, ohne signifikant dümmer zu werden.</p>
|
||||
<p><b>Context-Window:</b> Wie viel Text ("Tokens") sich das Modell gleichzeitig "merken" kann. Ein Buchstabe ist ~0.3 Tokens. Mehr Kontext braucht drastisch mehr VRAM.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="card" style="grid-column: span 2;">
|
||||
<div class="card-h"><h3>n8n / Zapier / HTTP Node</h3></div>
|
||||
<p>Führe direkte HTTP POST Requests gegen den Endpunkt aus.</p>
|
||||
<label>Methode & URL</label>
|
||||
<input class="tokin mono-sm" readonly value="POST ${currentUrl}/v1/chat/completions">
|
||||
<label>Headers</label>
|
||||
<input class="tokin mono-sm" readonly value="Content-Type: application/json">
|
||||
<label>Body (JSON)</label>
|
||||
<pre style="background: var(--bg); padding: 12px; border-radius: 8px; border: 1px solid var(--border); overflow-x: auto; color: var(--text); font-family: monospace; font-size: 13px;"><code>{
|
||||
"model": "coder",
|
||||
"messages": [
|
||||
{ "role": "user", "content": "Erkläre mir Quantisierung." }
|
||||
]
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
+89
-14
@@ -1,20 +1,20 @@
|
||||
// jobs.js — Aktivitaets-Stream ("Incident Stream"): Hintergrund-Jobs mit Live-Log.
|
||||
// Exportiert track(id), damit andere Panels einen frisch gestarteten Job auto-aufklappen.
|
||||
// jobs.js (Aktivität) — System-Metriken & Hintergrund-Jobs
|
||||
|
||||
import { $, esc } from "../core/ui.js";
|
||||
import { $, esc, icon } from "../core/ui.js";
|
||||
|
||||
const tracked = new Set();
|
||||
let JOBS = [];
|
||||
let SYS = null;
|
||||
|
||||
export function track(id) {
|
||||
tracked.add(id);
|
||||
render();
|
||||
renderJobs();
|
||||
}
|
||||
|
||||
function statusBadge(state) {
|
||||
if (state === "done") return '<span class="badge b-run">fertig</span>';
|
||||
if (state === "failed") return '<span class="badge b-err">fehler</span>';
|
||||
return '<span class="badge b-load">läuft…</span>';
|
||||
return '<span class="badge b-load">lädt…</span>';
|
||||
}
|
||||
function dotClass(state) {
|
||||
if (state === "done") return "on";
|
||||
@@ -22,26 +22,95 @@ function dotClass(state) {
|
||||
return "load";
|
||||
}
|
||||
|
||||
function kpi(cls, title, ic, value, sub) {
|
||||
return `<div class="kpi ${cls}">
|
||||
<div class="k-h"><span class="k-t">${title}</span><span class="k-ic">${icon(ic)}</span></div>
|
||||
<div class="k-v">${value}</div>
|
||||
<div class="k-s">${sub}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function kvRow(k, v, cls = "") {
|
||||
return `<div class="kv-row"><span class="kv-k">${k}</span><span class="kv-v ${cls}">${v}</span></div>`;
|
||||
}
|
||||
|
||||
// Chart-Daten (letzte 60 Sekunden, je 3s Polling = 20 Punkte)
|
||||
const hist = { cpu: [], ram: [], gpu: [] };
|
||||
const MAX_HIST = 20;
|
||||
|
||||
function renderSys() {
|
||||
if (!SYS) return;
|
||||
const sysV = `${SYS.cpu.percent.toFixed(0)}<small>% CPU</small>`;
|
||||
const sysS = `${SYS.ram.percent.toFixed(0)}% RAM, ${SYS.gpu_temp ? SYS.gpu_temp.toFixed(0)+'°C' : SYS.cpu.temp ? SYS.cpu.temp.toFixed(0)+'°C' : '–'}`;
|
||||
|
||||
// History aktualisieren
|
||||
hist.cpu.push(SYS.cpu.percent);
|
||||
hist.ram.push(SYS.ram.percent);
|
||||
let gpuP = 0;
|
||||
if (SYS.gpu && SYS.gpu.vram.total) {
|
||||
gpuP = ((SYS.gpu.vram.used + SYS.gpu.gtt.used) / (SYS.gpu.vram.total + SYS.gpu.gtt.total)) * 100;
|
||||
}
|
||||
hist.gpu.push(gpuP);
|
||||
if (hist.cpu.length > MAX_HIST) hist.cpu.shift();
|
||||
if (hist.ram.length > MAX_HIST) hist.ram.shift();
|
||||
if (hist.gpu.length > MAX_HIST) hist.gpu.shift();
|
||||
|
||||
// Mini-Sparklines generieren
|
||||
const makeBars = (arr, color) => {
|
||||
return '<div style="display:flex; align-items:flex-end; gap:2px; height:40px; margin-top:12px;">' +
|
||||
arr.map(v => `<div style="width:4px; background:var(--${color}); opacity:0.6; height:${Math.max(2, v)}%; border-radius:2px;"></div>`).join("") +
|
||||
'</div>';
|
||||
};
|
||||
|
||||
$("#act-kpis").innerHTML =
|
||||
kpi("blue", "CPU Last", "gauge", sysV, sysS) +
|
||||
kpi("purple", "RAM", "monitor", `${SYS.ram.percent.toFixed(0)}<small>%</small>`, "Arbeitsspeicher") +
|
||||
kpi("green", "GPU VRAM", "layers", `${gpuP.toFixed(0)}<small>%</small>`, "Grafikspeicher");
|
||||
|
||||
const gb = b => (b / 1024 / 1024 / 1024).toFixed(1);
|
||||
const ramStr = `${gb(SYS.ram.used)} GB / ${gb(SYS.ram.total)} GB`;
|
||||
const gpuStr = (SYS.gpu && SYS.gpu.vram.total) ? `${gb(SYS.gpu.vram.used + SYS.gpu.gtt.used)} GB / ${gb(SYS.gpu.vram.total + SYS.gpu.gtt.total)} GB` : "–";
|
||||
const diskStr = `${SYS.disk.percent.toFixed(0)}% belegt`;
|
||||
|
||||
$("#act-sys").innerHTML = `
|
||||
<div class="card-h"><h3>System-Metriken (Bosgame)</h3></div>
|
||||
<div class="kv" style="margin-bottom: 24px;">
|
||||
${kvRow("Arbeitsspeicher (RAM)", ramStr)}
|
||||
${kvRow("Grafikspeicher (VRAM+GTT)", gpuStr)}
|
||||
${kvRow("Speicherplatz (Disk)", diskStr)}
|
||||
${kvRow("Temperatur (GPU / CPU)", `${SYS.gpu_temp?.toFixed(1) || '–'}°C / ${SYS.cpu.temp?.toFixed(1) || '–'}°C`)}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-3">
|
||||
<div><div class="meta">CPU Historie</div>${makeBars(hist.cpu, "act")}</div>
|
||||
<div><div class="meta">RAM Historie</div>${makeBars(hist.ram, "purple")}</div>
|
||||
<div><div class="meta">VRAM Historie</div>${makeBars(hist.gpu, "on")}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function mount() {
|
||||
$("#v-activity").innerHTML = `
|
||||
<div class="card-h"><h3>Aktivität</h3><span class="meta" id="job-count"></span></div>
|
||||
<div class="card-h"><h3>Hintergrund-Aktivitäten</h3><span class="meta" id="job-count"></span></div>
|
||||
<div id="jobs"></div>
|
||||
<div id="jobs-empty" class="empty-c">
|
||||
<div class="e-t">Noch nichts losgemacht.</div>
|
||||
<div class="e-t">Keine laufenden Jobs.</div>
|
||||
<div class="e-s">Downloads, Updates & Co. erscheinen hier mit Live-Log.</div>
|
||||
</div>`;
|
||||
|
||||
// Klicks auf Job-Kopf -> auf/zuklappen (Event-Delegation)
|
||||
$("#jobs").addEventListener("click", e => {
|
||||
// Klicks auf Job-Kopf -> auf/zuklappen
|
||||
$("#v-activity").addEventListener("click", e => {
|
||||
const h = e.target.closest(".job-h");
|
||||
if (!h) return;
|
||||
const id = h.getAttribute("data-id");
|
||||
tracked.has(id) ? tracked.delete(id) : tracked.add(id);
|
||||
render();
|
||||
renderJobs();
|
||||
});
|
||||
|
||||
if (SYS) renderSys();
|
||||
}
|
||||
|
||||
function render() {
|
||||
function renderJobs() {
|
||||
const c = $("#jobs");
|
||||
if (!c) return;
|
||||
$("#jobs-empty").style.display = JOBS.length ? "none" : "flex";
|
||||
@@ -50,7 +119,7 @@ function render() {
|
||||
|
||||
c.innerHTML = JOBS.map(j => {
|
||||
const open = tracked.has(j.id);
|
||||
const log = open ? `<div class="log">${esc((j.log || []).join("\n"))}</div>` : "";
|
||||
const log = open ? `<div class="log">${esc((j.log || []).join("\\n"))}</div>` : "";
|
||||
return `<div class="job">
|
||||
<div class="job-h" data-id="${esc(j.id)}">
|
||||
<span class="li-dot ${dotClass(j.state)}"></span>
|
||||
@@ -61,7 +130,13 @@ function render() {
|
||||
|
||||
function onJobs(jobs) {
|
||||
JOBS = jobs || [];
|
||||
render();
|
||||
renderJobs();
|
||||
}
|
||||
|
||||
export default { id: "jobs", mount, onJobs };
|
||||
function onSystem(sys) {
|
||||
SYS = sys;
|
||||
const c = $("#act-sys");
|
||||
if (c) renderSys();
|
||||
}
|
||||
|
||||
export default { id: "jobs", mount, onJobs, onSystem };
|
||||
|
||||
+72
-14
@@ -1,7 +1,9 @@
|
||||
// models.js — "Modelle"-Ansicht: Download + Einpflegen, Schnelltest-Chat, Modell-Tabelle.
|
||||
// models.js — "Modelle"-Ansicht: Schnelltest-Chat, Modell-Tabelle & Konfiguration.
|
||||
|
||||
import { api } from "../core/api.js";
|
||||
import { $, badge, esc, toast } from "../core/ui.js";
|
||||
import { $, badge, esc, toast, icon } from "../core/ui.js";
|
||||
|
||||
let ALL_MODELS = [];
|
||||
|
||||
function refreshSoon() { document.dispatchEvent(new Event("mc:refresh")); }
|
||||
|
||||
@@ -13,40 +15,63 @@ function mount() {
|
||||
<select id="chat-model"></select>
|
||||
<label>Nachricht</label>
|
||||
<textarea id="chat-msg" placeholder="Schreib was, um ein Modell zu wecken…"></textarea>
|
||||
<button class="primary" id="chat-btn">Senden</button>
|
||||
<button class="primary" id="chat-btn" title="Sendet eine Chat-Anfrage an das Modell (weckt es auf, falls es schläft)">Senden</button>
|
||||
<div id="chat-reply" class="reply" style="display:none"></div>`;
|
||||
|
||||
$("#m-table").innerHTML = `
|
||||
<div class="card-h"><h3>Modelle & Ports</h3><span class="meta" id="m-count"></span></div>
|
||||
<div class="hint" style="margin-bottom: 16px;">
|
||||
💡 <b>Modelle werden automatisch geladen</b>, sobald eine Chat-Anfrage an sie gestellt wird. Du musst sie nicht manuell starten.
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr><th>Modell</th><th>Fähigkeiten</th><th>Details</th><th>Status</th><th>Port</th><th style="text-align:right">Aktion</th></tr></thead>
|
||||
<thead><tr><th>Modell</th><th>Fähigkeiten</th><th>Details</th><th>Status</th><th>Port</th><th style="text-align:right">Aktionen</th></tr></thead>
|
||||
<tbody id="models"></tbody>
|
||||
</table>
|
||||
<div id="models-empty" class="empty" style="display:none">Noch keine Modelle konfiguriert — zieh dir oben eins rein. 👇</div>`;
|
||||
<div id="models-empty" class="empty" style="display:none">Noch keine Modelle konfiguriert — zieh dir oben eins rein. 👇</div>
|
||||
|
||||
<!-- Config Modal -->
|
||||
<div id="cfg-modal" style="display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.8); z-index:100; align-items:center; justify-content:center;">
|
||||
<div class="card" style="width:100%; max-width:400px; position:relative">
|
||||
<button id="cfg-close" class="ghost" style="position:absolute; top:12px; right:12px;">Schließen</button>
|
||||
<h2 style="margin-top:0">Modell konfigurieren</h2>
|
||||
<p class="meta" id="cfg-model-name"></p>
|
||||
|
||||
<div style="margin-top:24px">
|
||||
<label>Context-Size (Tokens)</label>
|
||||
<input id="cfg-ctx" type="number" class="tokin" value="8192">
|
||||
<div class="meta" style="font-size:12px; margin-top:4px;">Höhere Werte erlauben längere Dokumente, brauchen aber mehr VRAM.</div>
|
||||
</div>
|
||||
|
||||
<button class="primary" id="cfg-save" style="width:100%; margin-top:24px; padding:12px">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$("#chat-btn").addEventListener("click", sendChat);
|
||||
$("#cfg-close").addEventListener("click", () => $("#cfg-modal").style.display = "none");
|
||||
$("#cfg-save").addEventListener("click", saveConfig);
|
||||
}
|
||||
|
||||
function onStatus(s) {
|
||||
const models = s?.models || [];
|
||||
ALL_MODELS = s?.models || [];
|
||||
const tb = $("#models");
|
||||
if (!tb) return;
|
||||
tb.innerHTML = "";
|
||||
$("#models-empty").style.display = models.length ? "none" : "block";
|
||||
$("#m-count").textContent = models.length ? models.length + " konfiguriert" : "";
|
||||
$("#models-empty").style.display = ALL_MODELS.length ? "none" : "block";
|
||||
$("#m-count").textContent = ALL_MODELS.length ? ALL_MODELS.length + " konfiguriert" : "";
|
||||
|
||||
const sel = $("#chat-model");
|
||||
const cur = sel.value;
|
||||
sel.innerHTML = "";
|
||||
for (const m of models) {
|
||||
for (const m of ALL_MODELS) {
|
||||
const tr = document.createElement("tr");
|
||||
|
||||
let capsHtml = "–";
|
||||
if (m.meta && m.meta.caps) {
|
||||
capsHtml = m.meta.caps.map(c => {
|
||||
if (c === "Text") return `<span class="meta" title="Text">T</span>`;
|
||||
if (c === "Code") return `<strong style="color:var(--blue)" title="Code">{ }</strong>`;
|
||||
if (c === "Bild") return `<strong style="color:var(--purple)" title="Vision">👁</strong>`;
|
||||
if (c === "Text") return `<span class="meta" title="Text" style="display:inline-block;width:18px">${icon("compass")}</span>`;
|
||||
if (c === "Code") return `<strong style="color:var(--blue);display:inline-block;width:18px" title="Code">{ }</strong>`;
|
||||
if (c === "Bild") return `<strong style="color:var(--purple);display:inline-block;width:18px" title="Vision">👁</strong>`;
|
||||
return "";
|
||||
}).join(" ");
|
||||
}
|
||||
@@ -61,19 +86,52 @@ function onStatus(s) {
|
||||
|
||||
const perfHtml = m.state === "running" ? `<br><small class="meta" style="font-size:0.75em">n/a t/s</small>` : "";
|
||||
|
||||
tr.innerHTML = `<td class="mid" style="font-weight:500">${esc(m.name)}</td>
|
||||
const filenameHtml = m.meta?.filename ? `<br><small class="meta" style="font-size:0.8em">${esc(m.meta.filename)}</small>` : "";
|
||||
|
||||
tr.innerHTML = `<td class="mid" style="font-weight:500">${esc(m.name)}${filenameHtml}</td>
|
||||
<td>${capsHtml}</td>
|
||||
<td>${detailsHtml}</td>
|
||||
<td>${badge(m.state)}${perfHtml}</td>
|
||||
<td class="port">${m.port ?? "auto"}</td>
|
||||
<td style="text-align:right"><button class="ghost" data-unload="${esc(m.name)}">Entladen</button></td>`;
|
||||
<td style="text-align:right">
|
||||
<button class="ghost" data-cfg="${esc(m.name)}" title="Context-Size anpassen">Konfigurieren</button>
|
||||
<button class="ghost" data-unload="${esc(m.name)}" title="Beendet das Modell sofort und gibt den VRAM wieder frei">Aus VRAM entfernen</button>
|
||||
</td>`;
|
||||
tb.appendChild(tr);
|
||||
sel.insertAdjacentHTML("beforeend", `<option>${esc(m.name)}</option>`);
|
||||
}
|
||||
if (cur) sel.value = cur;
|
||||
|
||||
tb.querySelectorAll("[data-unload]").forEach(b =>
|
||||
b.addEventListener("click", () => unloadOne(b.getAttribute("data-unload")))
|
||||
);
|
||||
tb.querySelectorAll("[data-cfg]").forEach(b =>
|
||||
b.addEventListener("click", () => openConfig(b.getAttribute("data-cfg")))
|
||||
);
|
||||
}
|
||||
|
||||
function openConfig(alias) {
|
||||
const m = ALL_MODELS.find(x => x.name === alias);
|
||||
if (!m) return;
|
||||
$("#cfg-model-name").textContent = m.name;
|
||||
$("#cfg-ctx").value = m.meta?.ctx || 8192;
|
||||
$("#cfg-modal").style.display = "flex";
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
const alias = $("#cfg-model-name").textContent;
|
||||
const ctx = parseInt($("#cfg-ctx").value) || 8192;
|
||||
|
||||
$("#cfg-save").disabled = true;
|
||||
try {
|
||||
await api("/api/update_model", { method: "POST", body: JSON.stringify({ alias, ctx }) });
|
||||
toast("Gespeichert! Änderungen werden beim nächsten Modell-Start aktiv.");
|
||||
$("#cfg-modal").style.display = "none";
|
||||
refreshSoon();
|
||||
} catch (e) {
|
||||
toast(e.message, true);
|
||||
}
|
||||
$("#cfg-save").disabled = false;
|
||||
}
|
||||
|
||||
async function unloadOne(m) {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// overview.js — Dashboard-Kopf: Hero + Mini-Stats, KPI-Reihe, Stack-Gesundheit,
|
||||
// kompakte Modell-Liste ("Session Router"). Speist sich aus /api/status + /api/jobs.
|
||||
// overview.js — Dashboard: Quick Actions, Modelle & Recent Jobs
|
||||
|
||||
import { $, icon, esc } from "../core/ui.js";
|
||||
import { $, esc, icon } from "../core/ui.js";
|
||||
|
||||
let S = null; // letzter Status
|
||||
let J = []; // letzte Job-Liste
|
||||
let SYS = null; // letzte System-Auslastung
|
||||
let SYS = null;
|
||||
|
||||
const RUNNING = new Set(["running", "ready", "loading", "starting"]);
|
||||
|
||||
@@ -14,85 +13,46 @@ function counts() {
|
||||
return {
|
||||
total: models.length,
|
||||
running: models.filter(m => RUNNING.has(m.state)).length,
|
||||
jobsRun: J.filter(j => j.state === "running" || j.state === "queued").length,
|
||||
jobsErr: J.filter(j => j.state === "failed").length,
|
||||
swap: !!S?.swap_ok,
|
||||
};
|
||||
}
|
||||
|
||||
function mini(label, val, tone = "") {
|
||||
const v = tone ? `<b style="${tone === "bad" ? "color:var(--err)" : ""}">${val}</b>` : val;
|
||||
return `<div class="mini"><div class="l">${label}</div><div class="v">${v}</div></div>`;
|
||||
}
|
||||
|
||||
function renderHero() {
|
||||
const c = counts();
|
||||
$("#hero").innerHTML = `<div class="hero">
|
||||
<div>
|
||||
<div class="eyebrow">Übersicht</div>
|
||||
<div class="eyebrow">Dashboard</div>
|
||||
<h1>Mission Control</h1>
|
||||
<p>Steuerzentrale für deinen lokalen llama-swap-Stack — Modelle, Downloads,
|
||||
Wartung und Schnelltest an einem Ort.</p>
|
||||
</div>
|
||||
<div class="hero-stats">
|
||||
${mini("Modelle", c.total)}
|
||||
${mini("Aktiv", c.running, "on")}
|
||||
${mini("Jobs", c.jobsRun)}
|
||||
${mini("Fehler", c.jobsErr, c.jobsErr ? "bad" : "")}
|
||||
<p>Steuerzentrale für deinen lokalen llama-swap-Stack. Hier verwaltest du Modelle, Downloads und Server-Wartung.</p>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function kpi(cls, title, ic, value, sub) {
|
||||
return `<div class="kpi ${cls}">
|
||||
<div class="k-h"><span class="k-t">${title}</span><span class="k-ic">${icon(ic)}</span></div>
|
||||
<div class="k-v">${value}</div>
|
||||
<div class="k-s">${sub}</div>
|
||||
</div>`;
|
||||
}
|
||||
function renderQuickActions() {
|
||||
// 3 Kacheln (Cookbook, Server-Status, Aktivität/Guides)
|
||||
$("#ov-quick").innerHTML = `
|
||||
<div class="card" style="cursor:pointer; display:flex; flex-direction:column; gap:12px; transition:border-color 0.2s" onclick="document.querySelector('.nav-item[data-view=\\'cookbook\\']').click()" onmouseover="this.style.borderColor='var(--act)'" onmouseout="this.style.borderColor='var(--line)'">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<h3 style="margin:0; font-size:16px;">Modell finden</h3>
|
||||
<span style="color:var(--act)">${icon("book")}</span>
|
||||
</div>
|
||||
<p style="margin:0; font-size:13px; color:var(--mut);">Durchsuche HuggingFace nach neuen Modellen im Cookbook.</p>
|
||||
</div>
|
||||
|
||||
function renderKpis() {
|
||||
const c = counts();
|
||||
const sysV = SYS ? `${SYS.cpu.percent.toFixed(0)}<small>% CPU</small>` : "n/a";
|
||||
const sysS = SYS ? `${SYS.ram.percent.toFixed(0)}% RAM, ${SYS.gpu_temp ? SYS.gpu_temp.toFixed(0)+'°C' : SYS.cpu.temp ? SYS.cpu.temp.toFixed(0)+'°C' : '–'}` : "bald · Live-Auslastung";
|
||||
<div class="card" style="cursor:pointer; display:flex; flex-direction:column; gap:12px; transition:border-color 0.2s" onclick="document.querySelector('.nav-item[data-view=\\'activity\\']').click()" onmouseover="this.style.borderColor='var(--act)'" onmouseout="this.style.borderColor='var(--line)'">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<h3 style="margin:0; font-size:16px;">Live Metriken</h3>
|
||||
<span style="color:var(--act)">${icon("pulse")}</span>
|
||||
</div>
|
||||
<p style="margin:0; font-size:13px; color:var(--mut);">${SYS ? `System läuft (RAM: ${SYS.ram.percent.toFixed(0)}%, CPU: ${SYS.cpu.percent.toFixed(0)}%)` : 'Lade Metriken...'}</p>
|
||||
</div>
|
||||
|
||||
$("#kpis").innerHTML =
|
||||
kpi(c.swap ? "green" : "red", "llama-swap", "swap",
|
||||
c.swap ? "Online" : "Offline", "Transport-Status") +
|
||||
kpi("blue", "Modelle", "monitor",
|
||||
`${c.running}<small>/${c.total}</small>`, "aktiv / gesamt") +
|
||||
kpi("purple", "Jobs", "layers", c.jobsRun, "laufend") +
|
||||
kpi(c.jobsErr ? "red" : "muted", "Fehler", "alert", c.jobsErr, "in der Aktivität") +
|
||||
kpi(SYS ? "blue" : "muted", "System-Last", "gauge", sysV, sysS);
|
||||
}
|
||||
|
||||
function kvRow(k, v, cls = "") {
|
||||
return `<div class="kv-row"><span class="kv-k">${k}</span><span class="kv-v ${cls}">${v}</span></div>`;
|
||||
}
|
||||
|
||||
function renderHealth() {
|
||||
const c = counts();
|
||||
|
||||
let sysRow = kvRow("Auslastung (RAM/GPU/Disk)", "folgt", "na");
|
||||
if (SYS) {
|
||||
const gb = b => (b / 1024 / 1024 / 1024).toFixed(1);
|
||||
const ram = `${gb(SYS.ram.used)}GB / ${gb(SYS.ram.total)}GB`;
|
||||
const gpu = (SYS.gpu && SYS.gpu.vram.total) ? `${gb(SYS.gpu.vram.used + SYS.gpu.gtt.used)}GB / ${gb(SYS.gpu.vram.total + SYS.gpu.gtt.total)}GB` : "–";
|
||||
const disk = `${SYS.disk.percent.toFixed(0)}%`;
|
||||
sysRow = kvRow("Auslastung (RAM/GPU/Disk)", `${ram} | ${gpu} | ${disk}`);
|
||||
}
|
||||
|
||||
$("#health").innerHTML = `
|
||||
<div class="card-h"><h3>Stack-Gesundheit</h3>
|
||||
<span class="meta ${c.swap ? "ok" : ""}">${c.swap ? "Connected" : "Offline"}</span></div>
|
||||
<div class="kv">
|
||||
${kvRow("llama-swap", c.swap ? "Connected" : "Offline", c.swap ? "ok" : "bad")}
|
||||
${kvRow("Modelle (gesamt)", c.total)}
|
||||
${kvRow("Aktiv", c.running, c.running ? "ok" : "")}
|
||||
${kvRow("Jobs (laufend)", c.jobsRun)}
|
||||
${kvRow("Fehler", c.jobsErr, c.jobsErr ? "bad" : "")}
|
||||
${sysRow}
|
||||
</div>`;
|
||||
<div class="card" style="cursor:pointer; display:flex; flex-direction:column; gap:12px; transition:border-color 0.2s" onclick="document.querySelector('.nav-item[data-view=\\'server\\']').click()" onmouseover="this.style.borderColor='var(--act)'" onmouseout="this.style.borderColor='var(--line)'">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<h3 style="margin:0; font-size:16px;">Wartung</h3>
|
||||
<span style="color:var(--act)">${icon("server")}</span>
|
||||
</div>
|
||||
<p style="margin:0; font-size:13px; color:var(--mut);">Server neustarten, VRAM leeren oder Engine aktualisieren.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function modelRow(m) {
|
||||
@@ -109,14 +69,15 @@ function modelRow(m) {
|
||||
}).join("");
|
||||
}
|
||||
|
||||
const filename = m.meta?.filename ? `<div class="li-sub" style="font-family:var(--mono); color:var(--mut);">${esc(m.meta.filename)}</div>` : '';
|
||||
|
||||
return `<div class="li">
|
||||
<span class="li-dot ${dot}"></span>
|
||||
<div class="li-main">
|
||||
<div class="li-id" style="font-weight:500">${esc(m.name)}${caps}</div>
|
||||
<div class="li-sub">TTL ${m.ttl ?? "—"}${typeof m.ttl === "number" ? "s" : ""}</div>
|
||||
${filename}
|
||||
</div>
|
||||
<div class="li-right">
|
||||
<div class="li-meta">${m.port ?? "auto"}</div>
|
||||
<div class="li-time">${state}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -125,18 +86,50 @@ function modelRow(m) {
|
||||
function renderModels() {
|
||||
const models = S?.models || [];
|
||||
$("#ov-models").innerHTML = `
|
||||
<div class="card-h"><h3>Modelle</h3><span class="meta">${models.length || ""}</span></div>
|
||||
<div class="card-h"><h3>Aktuelle Modelle im Stack</h3><span class="meta">${models.length || ""}</span></div>
|
||||
${models.length
|
||||
? `<div class="list">${models.map(modelRow).join("")}</div>`
|
||||
: `<div class="empty-c"><div class="e-t">Keine Modelle konfiguriert</div>
|
||||
<div class="e-s">Hol dir unter „Modelle" eins von HuggingFace.</div></div>`}`;
|
||||
<div class="e-s">Hol dir unter „Cookbook“ eins von HuggingFace.</div></div>`}`;
|
||||
}
|
||||
|
||||
function renderAll() { renderHero(); renderKpis(); renderHealth(); renderModels(); }
|
||||
function renderRecentJobs() {
|
||||
const latest = J.slice(0, 4);
|
||||
|
||||
const statusBadge = (s) => {
|
||||
if (s === "done") return '<span class="badge b-run" style="font-size:10px">fertig</span>';
|
||||
if (s === "failed") return '<span class="badge b-err" style="font-size:10px">fehler</span>';
|
||||
return '<span class="badge b-load" style="font-size:10px">lädt…</span>';
|
||||
};
|
||||
|
||||
$("#ov-recent-jobs").innerHTML = `
|
||||
<div class="card-h"><h3>Letzte Aktivitäten</h3><span class="meta" style="cursor:pointer" onclick="document.querySelector('.nav-item[data-view=\\'activity\\']').click()">Alle ansehen →</span></div>
|
||||
${latest.length
|
||||
? `<div class="list">
|
||||
${latest.map(j => `
|
||||
<div class="li" style="padding:10px 4px">
|
||||
<div class="li-main">
|
||||
<div class="li-id" style="font-size:12.5px">${esc(j.label)}</div>
|
||||
</div>
|
||||
<div class="li-right">${statusBadge(j.state)}</div>
|
||||
</div>
|
||||
`).join("")}
|
||||
</div>`
|
||||
: `<div class="empty-c"><div class="e-t">Keine Aktivitäten</div><div class="e-s">Alles läuft ruhig.</div></div>`
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function renderAll() {
|
||||
renderHero();
|
||||
renderQuickActions();
|
||||
renderModels();
|
||||
renderRecentJobs();
|
||||
}
|
||||
|
||||
function mount() { renderAll(); }
|
||||
function onStatus(s) { S = s; renderAll(); }
|
||||
function onJobs(jobs) { J = jobs || []; renderHero(); renderKpis(); renderHealth(); }
|
||||
function onSystem(sys) { SYS = sys; renderKpis(); renderHealth(); }
|
||||
function onStatus(s) { S = s; renderModels(); }
|
||||
function onJobs(jobs) { J = jobs || []; renderRecentJobs(); }
|
||||
function onSystem(sys) { SYS = sys; renderQuickActions(); }
|
||||
|
||||
export default { id: "overview", mount, onStatus, onJobs, onSystem };
|
||||
|
||||
@@ -7,13 +7,11 @@ function refreshSoon() { document.dispatchEvent(new Event("mc:refresh")); }
|
||||
function mount() {
|
||||
$("#wartung").innerHTML = `
|
||||
<div class="card-h"><h3>Dienste & Applikation</h3></div>
|
||||
<div class="btn-row" style="margin-bottom:20px">
|
||||
<button id="w-restart-swap">llama-swap neustarten</button>
|
||||
<button id="w-restart-mc">Mission Control neustarten</button>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button id="w-update">Container aktualisieren</button>
|
||||
<button id="w-unload" class="ghost">Modelle entladen</button>
|
||||
<button id="w-restart-swap" title="Startet die LLM-Engine neu. Dauer ca. 5 Sekunden.">llama-swap neustarten</button>
|
||||
<button id="w-restart-mc" title="Startet Mission Control (das Dashboard) neu.">Mission Control neustarten</button>
|
||||
<button id="w-update" title="Zieht den neuesten Code via Git und aktualisiert das Dashboard.">Mission Control System-Update</button>
|
||||
<button id="w-unload" class="ghost" title="Wirft alle derzeit in VRAM geladenen Modelle sofort raus.">Aus dem VRAM entfernen</button>
|
||||
</div>
|
||||
<div class="hint" style="margin-top:12px; margin-bottom:32px">
|
||||
Dienste starten via passwortlosem Sudo neu.
|
||||
@@ -21,8 +19,8 @@ function mount() {
|
||||
|
||||
<div class="card-h"><h3>Betriebssystem (Bosgame)</h3></div>
|
||||
<div class="btn-row">
|
||||
<button id="w-os-update">OS-Updates installieren (apt update)</button>
|
||||
<button id="w-reboot" class="danger">Server Reboot</button>
|
||||
<button id="w-os-update" title="Führt apt update & upgrade aus, um das Betriebssystem (Bosgame) zu aktualisieren.">OS-Updates installieren (apt update)</button>
|
||||
<button id="w-reboot" class="danger" title="Startet den gesamten Server physikalisch neu. Alles ist kurz offline.">Server Reboot</button>
|
||||
</div>
|
||||
<div class="hint" style="margin-top:12px; margin-bottom:32px">
|
||||
Für tiefe Eingriffe fragt das Dashboard einmalig das sudo-Passwort ab.
|
||||
|
||||
Reference in New Issue
Block a user