fix(phase-a): Kontext-Cap, Download-State, Recipe-Edit, OS-Badge, Swap-Flash

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>
This commit is contained in:
Hitonabi
2026-06-22 21:33:03 +02:00
parent 022c42dccb
commit 82d15d82db
10 changed files with 214 additions and 45 deletions
+48 -7
View File
@@ -118,7 +118,7 @@ function mount() {
<div id="cb-new-modal" class="modal-overlay" style="display:none">
<div class="modal-card" style="max-width:640px">
<button id="cb-new-close" class="ghost" style="position:absolute;top:14px;right:14px">Schließen</button>
<h3>Eigenes Setup erstellen</h3>
<h3 id="cb-new-modal-title">Eigenes Setup erstellen</h3>
<p class="text-mut text-sm" style="margin:-4px 0 16px">Stell dir aus beliebigen Modellen ein eigenes Setup zusammen — es erscheint danach mit Hardware-Ampel hier im Cookbook.</p>
<label>Titel</label>
<input id="cb-new-title" placeholder="z.B. Mein Schreib-Stack">
@@ -270,8 +270,11 @@ function renderRecipes() {
const best = r.id === RECOMMENDED;
const tag = best ? `<div class="cb-best-tag">★ Beste Wahl für dein System</div>`
: r.user ? `<div class="cb-best-tag" style="background:var(--act);color:#fff">Dein Setup</div>` : "";
const del = r.user ? `<span class="cb-delr" data-delr="${esc(r.id)}" title="Setup löschen"
style="position:absolute;top:9px;right:12px;color:var(--err);cursor:pointer;font-size:18px;line-height:1">×</span>` : "";
const del = r.user ? `
<span class="cb-editr" data-editr="${esc(r.id)}" title="Setup bearbeiten"
style="position:absolute;top:10px;right:36px;color:var(--accent);cursor:pointer;font-size:14px;line-height:1">✎</span>
<span class="cb-delr" data-delr="${esc(r.id)}" title="Setup löschen"
style="position:absolute;top:9px;right:12px;color:var(--err);cursor:pointer;font-size:18px;line-height:1">×</span>` : "";
return `<button class="card-btn${best ? " cb-best" : ""}" data-recipe="${esc(r.id)}" style="position:relative">
${del}${tag}
<div class="flex justify-between items-center">
@@ -287,6 +290,8 @@ function renderRecipes() {
b.addEventListener("click", () => openRecipe(b.getAttribute("data-recipe"))));
$("#cb-recipes").querySelectorAll("[data-delr]").forEach(x =>
x.addEventListener("click", e => { e.stopPropagation(); deleteRecipe(x.getAttribute("data-delr")); }));
$("#cb-recipes").querySelectorAll("[data-editr]").forEach(x =>
x.addEventListener("click", e => { e.stopPropagation(); openEditRecipe(x.getAttribute("data-editr")); }));
}
// ---- Eigenes Setup erstellen/löschen ----
@@ -301,10 +306,35 @@ function newModelRow() {
</div>`;
}
let editRecipeId = null;
function openNewRecipe() {
editRecipeId = null;
$("#cb-new-title").value = "";
$("#cb-new-desc").value = "";
$("#cb-new-models").innerHTML = newModelRow();
$("#cb-new-modal-title").textContent = "Eigenes Setup erstellen";
$("#cb-new-save").textContent = "Setup speichern";
$("#cb-new-modal").style.display = "flex";
}
function openEditRecipe(id) {
const r = RECIPES.find(x => x.id === id); if (!r) return;
editRecipeId = id;
$("#cb-new-title").value = r.title;
$("#cb-new-desc").value = r.desc || "";
$("#cb-new-models").innerHTML = r.models.map(m => {
const row = document.createElement("div");
row.innerHTML = newModelRow();
const el = row.firstElementChild;
el.querySelector(".cb-nm-repo").value = m.repo;
el.querySelector(".cb-nm-role").value = m.role;
const quant = el.querySelector(".cb-nm-quant");
if (quant) [...quant.options].forEach(o => { if (o.value === m.quant) o.selected = true; });
return el.outerHTML;
}).join("");
$("#cb-new-modal-title").textContent = "Setup bearbeiten";
$("#cb-new-save").textContent = "Änderungen speichern";
$("#cb-new-modal").style.display = "flex";
}
@@ -319,13 +349,19 @@ async function saveNewRecipe() {
if (!models.length) return toast("Mindestens ein Modell (Repo) angeben.", true);
const btn = $("#cb-new-save"); btn.disabled = true; btn.textContent = "Speichere…";
try {
await api("/api/cookbook/user-recipe", { method: "POST",
body: JSON.stringify({ title, desc: $("#cb-new-desc").value.trim(), models }) });
toast("Setup gespeichert.");
const body = JSON.stringify({ title, desc: $("#cb-new-desc").value.trim(), models });
if (editRecipeId) {
await api("/api/cookbook/user-recipe/" + encodeURIComponent(editRecipeId), { method: "PUT", body });
toast("Setup aktualisiert.");
} else {
await api("/api/cookbook/user-recipe", { method: "POST", body });
toast("Setup gespeichert.");
}
editRecipeId = null;
$("#cb-new-modal").style.display = "none";
loadRecipes();
} catch (e) { toast("Fehler: " + e.message, true); }
btn.disabled = false; btn.textContent = "Setup speichern";
btn.disabled = false; btn.textContent = editRecipeId ? "Änderungen speichern" : "Setup speichern";
}
async function deleteRecipe(id) {
@@ -482,6 +518,11 @@ function showFit() {
$("#cb-m-fit-text").innerHTML = `Bedarf: <b>~${f.fit.req_gb.toFixed(1)} GB</b> · ${currentAnalysis.params_b}B · ${esc(f.quant)} · ~${Math.round(f.fit.tps)} Tok/s`;
$("#cb-m-fit-badge").innerHTML = `<span class="fit-badge ${fitCls(f.fit.level)}">${esc(f.fit.text)}</span>`;
const opt = f.optimal_ctx, recEl = $("#cb-m-ctx-rec");
if (opt) {
// Kontext-Input auto auf optimal setzen, wenn noch auf Default (8192)
const ctxInput = $("#cb-m-ctx");
if (ctxInput && (ctxInput.value === "8192" || ctxInput.value === "")) ctxInput.value = opt;
}
if (recEl) recEl.innerHTML = opt
? `Empfohlener Kontext für deine Hardware: <b>~${Math.round(opt / 1024)}k</b> — <a href="#" onclick="event.preventDefault();window.cbSetCtx(${opt})">übernehmen</a>` : "";
const btn = $("#cb-m-download");