// Anzeige-Logik der Wartungs-Ansichten (Updates, Aufträge, Installieren). Eigene Datei, weil // anzeige.ts im Start-Bündel liegt — was nur die nachgeladenen Seiten brauchen, gehört nicht dorthin. import { laeuftNoch } from "./anzeige" import type { Job, UpdateDetails } from "./typen" import { gbText } from "./zeit" // --- Updates ------------------------------------------------------------------------ /** „Läuft“ und „Neu“ aus den Update-Details lesbar machen (die Details sind schon geladen). */ export function versionen(id: string, d: UpdateDetails): { laeuft: string; neu: string | null } { if (id === "os") return { laeuft: "Ubuntu", neu: d.count ? `${d.count} ${d.count === 1 ? "Paket" : "Pakete"}` : null } // Hermes kommt aus git: „Neu“ ist die Zahl der Änderungen seit dem eingespielten Stand. if (id === "hermes") { return { laeuft: d.installed_version ? `v${d.installed_version}` : "–", neu: d.behind ? `+${d.behind.toLocaleString("de-DE")} Änderungen` : null, } } const pre = id === "engine" ? "b" : "v" const laeuft = d.installed_build != null ? `${pre}${d.installed_build}` : "–" const neuer = d.latest_build != null && d.installed_build != null && d.latest_build > d.installed_build return { laeuft, neu: neuer ? `${pre}${d.latest_build}` : null } } /** Sagen die Details, dass es Neues gibt? null = weiß man nicht (Prüfung gescheitert oder unvollständig). */ export function detailsHabenNeues(id: string, d: UpdateDetails | undefined): boolean | null { if (!d || d.error) return null if (id === "os") return typeof d.count === "number" ? d.count > 0 : null if (id === "hermes") return typeof d.behind === "number" ? d.behind > 0 : null if (d.latest_build == null || d.installed_build == null) return null return d.latest_build > d.installed_build } /** Eine gescheiterte Update-Prüfung in Worten („Konnte nicht prüfen: …“), ohne Befehlszeilen-Kauderwelsch. */ export function pruefFehlerText(fehler: string): string { const f = fehler.trim() if (/timed out|timeout|zeitüberschreitung/i.test(f)) { return "Konnte nicht prüfen: Die Gegenstelle hat nicht rechtzeitig geantwortet. Später klappt es meist wieder." } if (/name resolution|name or service not known|network is unreachable|connection (refused|reset)|nicht erreichbar/i.test(f)) { return "Konnte nicht prüfen: Die Box kommt gerade nicht ins Internet." } return `Konnte nicht prüfen: ${f.length > 160 ? `${f.slice(0, 157)}…` : f}` } // --- Aufträge (services/jobengine.py) --------------------------------------------------- /** Ist der Auftrag ein Modell-Download? (Bis das Backend eine Gruppe setzt, verrät es nur die Beschriftung.) */ export const jobDownload = (j: Pick) => j.group === "download" || /^download\s/i.test(j.label) /** Ist der Auftrag die Suche nach Updates (apt-get update)? Auch sie hat im Backend noch keine Gruppe. */ export const jobSuche = (j: Pick) => j.group === "pruefen" || j.label === "Nach Updates suchen" /** * Darf man den Auftrag abbrechen? Nur Downloads: Sie laufen als eigener Prozess (`hf download`), den * der Abbruch wirklich beendet. Updates und die Suche laufen als Shell-Kette (`bash -c …`), und ob * ein Abbruch dort alles stoppt, ist nicht sicher — bei Updates liefen apt und die Update-Skripte * weiter, während die Karte „Abgebrochen“ zeigte. Scheitert ein Update, rollen Motor und llama-swap * selbst zurück. */ export const abbrechbar = (j: Pick) => laeuftNoch(j) && jobDownload(j) // Die Beschriftungen setzt das Backend (services/maintenance.py, routers/models.py) mit technischen // Namen; die Oberfläche sagt Motor, llama-swap, Hermes und Betriebssystem. const TITEL: [RegExp, string][] = [ [/^download\s+(.+)$/i, "Download $1"], [/^OS-Update \(apt\)$/, "Update Betriebssystem"], [/^Engine-Update \(llama\.cpp Vulkan\)$/, "Update Motor (llama.cpp)"], [/^Router-Update \(llama-swap\)$/, "Update llama-swap"], [/^Hermes-Agent-Update$/, "Update Hermes"], ] const TEILE: [string, string][] = [ ["Engine (llama.cpp)", "Motor"], ["Router (llama-swap)", "llama-swap"], ["Hermes-Agent", "Hermes"], ["OS (apt)", "Betriebssystem"], ] /** Die Beschriftung eines Auftrags in den Worten der Oberfläche. Unbekanntes bleibt, wie es ist. */ export function jobTitel(j: Pick): string { for (const [muster, ersatz] of TITEL) if (muster.test(j.label)) return j.label.replace(muster, ersatz) // „Alle aktualisieren (Engine (llama.cpp) → Router (llama-swap) → Hermes-Agent → OS (apt))“ if (j.label.startsWith("Alle aktualisieren")) return TEILE.reduce((t, [alt, neu]) => t.split(alt).join(neu), j.label) return j.label } /** „noch etwa 6 min“ aus der geschätzten Restzeit in Sekunden. */ export function restzeit(s: number): string { if (s < 60) return "noch unter einer Minute" const min = Math.round(s / 60) if (min < 60) return `noch etwa ${min} min` const h = Math.floor(min / 60) const rest = min % 60 return rest ? `noch etwa ${h} h ${rest} min` : `noch etwa ${h} h` } /** „Läuft seit 12 min“ / „Wartet seit ein paar Sekunden“ für Aufträge ohne Fortschrittsanzeige. */ export function laufText(j: Pick, jetzt = Date.now() / 1000): string { const s = Math.max(0, jetzt - j.started_at) const min = Math.floor(s / 60) const dauer = s < 60 ? "ein paar Sekunden" : min < 60 ? `${min} min` : `${Math.floor(min / 60)} h ${min % 60} min` return `${j.state === "queued" ? "Wartet" : "Läuft"} seit ${dauer}` } /** „42 % · 12,3 GB von 29,1 GB · noch etwa 6 min“ — oder null, wenn der Auftrag keinen Fortschritt kennt. */ export function fortschrittText(j: Job): string | null { if (j.progress == null) return null const teile = [`${Math.round(j.progress)} %`] if (j.total_bytes) teile.push(`${gbText(j.done_bytes ?? 0, 1)} von ${gbText(j.total_bytes, 1)}`) if (j.eta_s != null && laeuftNoch(j)) teile.push(restzeit(j.eta_s)) return teile.join(" · ") } /** Letzte aussagekräftige Zeile des Protokolls (nicht die Befehlszeile „$ …“, die der Auftrag zuerst schreibt). */ export function letzteMeldung(j: Pick): string | null { for (let i = (j.log?.length ?? 0) - 1; i >= 0; i--) { const z = j.log[i].trim() if (z && !z.startsWith("$ ")) return z } return null } /** * Welche Aufträge die Oberfläche zeigt: alle laufenden und wartenden (egal aus welcher Gruppe), * dazu die drei zuletzt beendeten der letzten 24 Stunden — damit man sieht, wie es ausging. */ export function sichtbareJobs(jobs: Job[], jetzt = Date.now() / 1000): Job[] { const laufend = jobs.filter(laeuftNoch).sort((a, b) => a.started_at - b.started_at) const beendet = jobs .filter((j) => !laeuftNoch(j) && !j.quittiert && jetzt - (j.finished_at ?? j.started_at) < 86_400) .sort((a, b) => (b.finished_at ?? b.started_at) - (a.finished_at ?? a.started_at)) .slice(0, 3) return [...laufend, ...beendet] } // --- Installieren ------------------------------------------------------------------- /** Wer eine Rolle heute innehat — für den Hinweis vor dem Installieren. */ export interface RollenInhaber { name: string /** Hängt ein Bild-Zwilling an seinen Gewichten (vision, coder-bild)? */ zwilling: boolean /** Rollen, die das Modell außerdem trägt (der Coder auch „heavy“) — die behält es. */ weitere: string[] } /** * Hinweis vor dem Installieren mit Rolle: Wen ersetzt das neue Modell? Nach dem Download wandert * nur dieser eine Rollen-Alias um (services/llamaswap.set_role_alias): Das bisherige Modell bleibt * auf der Platte, behält seine übrigen Rollen, und sein Bild-Zwilling zeigt weiter auf dessen Gewichte. */ export function rollenErsatzText(neu: string, rolle: string, bisher: RollenInhaber | undefined): string { if (!bisher) return `Nach dem Download übernimmt ${neu} die Rolle „${rolle}“.` const behaelt = bisher.weitere.length ? ` Die Rolle${bisher.weitere.length === 1 ? "" : "n"} „${bisher.weitere.join("“, „")}“ behält es.` : "" return ( `Achtung: Nach dem Download übernimmt ${neu} die Rolle „${rolle}“ und ersetzt damit ${bisher.name}. ` + `Das bisherige Modell bleibt auf der Platte.${behaelt}` + (bisher.zwilling ? " Bilder liest vorerst weiter sein Bild-Zwilling, also noch das bisherige Modell." : "") ) } /** * Was das Eintragen in llama-swap unterwegs anrichtet: llama-swap läuft mit `--watch-config` und lädt * bei jeder Änderung sofort neu (services/warmer.py, deploy/coder-vollkontext.sh) — laufende Antworten * brechen ab, geladene Modelle fliegen raus. Mit Rolle schreibt das Backend nach dem Download noch einmal. */ export function installierenUnterbrechung(mitRolle: boolean): string { return ( "Kurze Unterbrechung: Beim Eintragen lädt llama-swap sofort neu. Was Lucy oder OpenChamber gerade " + "schreiben, bricht ab, und die Modelle werden neu geladen." + (mitRolle ? " Mit Rolle passiert das nach dem Download noch einmal." : "") ) }