durchsicht: Rueckfragen passen aufs kleine Handy, ehrlichere Texte, Abbrechen nur wo es stoppt

Befunde der Schlussdurchsicht (Attrappe des Backends, 320 x 568 bis 1440 px):
- Rueckfragen: Bei 320 x 568 lagen "Installieren"/"Abbrechen" unter dem Bildschirmrand
  und liessen sich nicht erreichen (Dialog 791 px hoch, kein Scrollen). Jetzt hoechstens
  Bildschirmhoehe, der Text scrollt, die Knoepfe bleiben unten stehen. Gilt fuer alle.
- Installieren: Die Rueckfrage sagt jetzt, dass llama-swap beim Eintragen sofort neu laedt
  (--watch-config): laufende Antworten von Lucy/OpenChamber brechen ab, mit Rolle nach dem
  Download noch einmal. Der Ersatz-Hinweis nennt, dass das bisherige Modell "heavy" behaelt.
- Abbrechen nur noch bei Downloads (eigener Prozess). Updates und die Suche laufen als
  Shell-Kette, ein Abbruch stoppt dort nicht sicher alles.
- Auftragstitel in den Worten der Oberflaeche ("Update Motor (llama.cpp)" statt
  "Engine-Update (llama.cpp Vulkan)"); "Laeuft seit ... min" zaehlt weiter, auch wenn der
  Auftrag nichts schreibt; Ueberschriftenebene passend zum Panel.
- Updates: "Alles jetzt aktualisieren" richtet sich wie die Zeilen nach den frischen
  Details (vorher nur nach dem bis zu 10 min alten Start-Stand); "Nach Neuem suchen"
  sperrt, solange eine Suche laeuft (zwei apt-get update stolpern ueber die Sperre);
  Rueckfragetext sagt genau, was gesichert wird.
- Nach Auftragsende nur nachladen, was betroffen ist (Download: Modelle/Platte; sonst
  Versionen/Verlauf/Sicherungen) - die Update-Pruefung fragt GitHub, apt und git.
- Radar "Uebernehmen"/"Verwerfen" und "Endgueltig loeschen": Ja-Knopf gesperrt, solange
  die Aktion laeuft, und ein zweiter Klick waehrend des Schliessens loest nichts mehr aus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-24 17:44:40 +02:00
co-authored by Claude Opus 5.5
parent 83941847ab
commit bc1106ddc8
10 changed files with 188 additions and 62 deletions
+29 -13
View File
@@ -3,7 +3,7 @@
// Takte: Steht der Live-Strom, meldet er Änderungen selbst — die Abfragen sind dann nur
// Sicherheitsnetz und laufen fünfmal seltener.
import { type QueryClient, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { type QueryClient, useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query"
import { api, post } from "./api"
import { fehlerText, laeuftNoch } from "./anzeige"
import { melden } from "./meldungen"
@@ -15,14 +15,24 @@ import { gbText } from "./zeit"
const takt = (ms: number) => () => (stromVerbunden() ? ms * 5 : ms)
/** Was ein beendeter Auftrag verändert haben kann: Versionen, Modelle, Sicherungen, Platte. */
const NACH_AUFTRAG = ["start", "update-details", "updates-verlauf", "models", "sicherungen", "aufraeumen"]
/** Ist seit der letzten Abfrage ein Auftrag fertig geworden, lesen die betroffenen Ansichten neu. */
/**
* Ist seit der letzten Abfrage ein Auftrag fertig geworden, lesen die betroffenen Ansichten neu:
* nach einem Download die Modelle und die Platte, nach Updates und der Suche die Versionen, der
* Verlauf und die Sicherungen. Gezielt, weil die Update-Prüfung teuer ist (GitHub, apt, git fetch).
*/
function auftragsEndeMelden(qc: QueryClient, neu: Job[]) {
const alt = qc.getQueryData<{ jobs: Job[] }>(["jobs"])?.jobs ?? []
const ebenFertig = neu.some((j) => !laeuftNoch(j) && alt.some((a) => a.id === j.id && laeuftNoch(a)))
if (ebenFertig) for (const k of NACH_AUFTRAG) qc.invalidateQueries({ queryKey: [k] })
const ebenFertig = neu.filter((j) => !laeuftNoch(j) && alt.some((a) => a.id === j.id && laeuftNoch(a)))
if (ebenFertig.length === 0) return
const schluessel = new Set(["start"])
for (const j of ebenFertig) {
// Wie wartung.jobDownload — hier ausgeschrieben, damit wartung.ts nicht ins Start-Bündel wandert.
const download = j.group === "download" || /^download\s/i.test(j.label)
for (const k of download ? ["models", "aufraeumen"] : ["update-details", "updates-verlauf", "sicherungen"]) {
schluessel.add(k)
}
}
for (const k of schluessel) qc.invalidateQueries({ queryKey: [k] })
}
export const useStart = () =>
@@ -66,12 +76,18 @@ export const useSicherungen = () =>
queryFn: () => api<{ available: boolean; backups: Sicherung[] }>("/api/zeitmaschine"),
})
export const useUpdateDetails = (art: string | null) =>
useQuery({
queryKey: ["update-details", art],
queryFn: () => api<UpdateDetails>(`/api/maintenance/update-details?kind=${art}`),
enabled: !!art,
staleTime: 600_000,
/**
* Was die Updates der Bausteine bringen würden — je Baustein eine eigene Abfrage, damit ein langsamer
* (Hermes: git fetch bis 25 s) die anderen nicht aufhält. Die Prüfung fragt GitHub, apt und git,
* darum gilt ein Ergebnis zehn Minuten.
*/
export const useUpdateDetails = (arten: readonly string[]) =>
useQueries({
queries: arten.map((art) => ({
queryKey: ["update-details", art],
queryFn: () => api<UpdateDetails>(`/api/maintenance/update-details?kind=${encodeURIComponent(art)}`),
staleTime: 600_000,
})),
})
export interface UpdateLauf {
+65 -21
View File
@@ -46,22 +46,45 @@ export function pruefFehlerText(fehler: string): string {
// --- Aufträge (services/jobengine.py) ---------------------------------------------------
/**
* Darf man den Auftrag abbrechen? Updates nicht: Der Abbruch beendet nur die äußere Shell, apt
* und die Update-Skripte liefen weiter, und die Karte zeigte fälschlich „Abgebrochen“. Scheitert
* ein Update, rollen Motor und llama-swap selbst zurück.
*/
export const abbrechbar = (j: Pick<Job, "state" | "group">) => laeuftNoch(j) && j.group !== "maintenance"
/** Die Beschriftung eines Auftrags auf Deutsch („download org/repo“ heißt im Backend noch englisch). */
export function jobTitel(j: Pick<Job, "label">): string {
const m = /^download\s+(.+)$/i.exec(j.label)
return m ? `Download ${m[1]}` : j.label
}
/** Ist der Auftrag ein Modell-Download? (Bis das Backend eine Gruppe setzt, verrät es nur die Beschriftung.) */
export const jobDownload = (j: Pick<Job, "label" | "group">) => 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<Job, "label" | "group">) => 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<Job, "state" | "label" | "group">) => 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<Job, "label">): 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"
@@ -113,20 +136,41 @@ export function sichtbareJobs(jobs: Job[], jetzt = Date.now() / 1000): Job[] {
// --- 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 der Rollen-Alias um (services/llamaswap.set_role) — das bisherige Modell bleibt auf der Platte
* und sein Bild-Zwilling zeigt weiter auf dessen Gewichte.
* 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: { name: string; zwilling: boolean } | undefined,
): string {
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." +
`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." : "")
)
}