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
+8 -4
View File
@@ -5,18 +5,21 @@ import { Button } from "@/components/ui/button"
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"
import { useInstallieren } from "@/lib/abfragen"
import { api } from "@/lib/api"
import { rollenErsatzText } from "@/lib/wartung"
import { installierenUnterbrechung, type RollenInhaber, rollenErsatzText } from "@/lib/wartung"
import { gbText, zahl } from "@/lib/zeit"
interface Treffer { repo: string; downloads: number; likes: number }
/** /api/hf/quants liefert heute nur die Namen. Kommen Größen dazu, zeigen Liste und Rückfrage sie an. */
interface Quant { quant?: string; name?: string; size_bytes?: number; total_bytes?: number }
/** Rollen, die sich beim Installieren gleich vergeben lassen (Rollen-Alias → Name in der Oberfläche). */
/**
* Rollen, die sich beim Installieren gleich vergeben lassen (Rollen-Alias → Name in der Oberfläche).
* Das Hirn steht hier (wie bisher) nicht zur Wahl; Hirn-Kandidaten testet das Radar nachts, bevor man sie übernimmt.
*/
const ROLLEN: Record<string, string> = { coder: "Coder" }
/** Wer eine Rolle heute innehat: Modellname und ob ein Bild-Zwilling daran hängt. */
export type RollenStand = Record<string, { name: string; zwilling: boolean } | undefined>
/** Wer die Rollen heute innehat (Schlüssel = Rollen-Alias). */
export type RollenStand = Record<string, RollenInhaber | undefined>
/** Modelle selbst auf Hugging Face suchen und installieren — zusätzlich zum Radar. */
export function ModellSuche({ offen, onSchliessen, rollenJetzt }: {
@@ -188,6 +191,7 @@ export function ModellSuche({ offen, onSchliessen, rollenJetzt }: {
{ersatz}
</p>
)}
<p className="m-0 text-[15px] leading-relaxed text-text-2">{installierenUnterbrechung(!!rolle)}</p>
</Bestaetigen>
</SheetContent>
</Sheet>
+10 -1
View File
@@ -214,8 +214,10 @@ function Kandidat({ k, test, heute }: { k: RadarKandidat; test: RadarTest | unde
? `${k.name} wird Lucys Hirn${heute ? ` und löst ${heute} ab` : ""}. NerdQuiz und OpenChamber nutzen ab dann ebenfalls das neue Modell. Hermes startet dafür kurz neu, Lucy ist etwa eine Minute nicht erreichbar. Das alte Modell bleibt auf der Platte.`
: `${k.name} wird der Coder${heute ? ` und löst ${heute} ab` : ""}. OpenChamber plant und baut ab dann damit. Das alte Modell bleibt auf der Platte.`}
knopf="Übernehmen"
gesperrt={aktion.isPending}
onNein={() => setFrage(null)}
onJa={() => {
if (frage !== "uebernehmen") return // zweiter Klick eines Doppelklicks, der Dialog schließt schon
aktion.mutate({ id: k.id, aktion: "uebernehmen", name: k.name })
setFrage(null)
}}
@@ -226,8 +228,10 @@ function Kandidat({ k, test, heute }: { k: RadarKandidat; test: RadarTest | unde
titel={`${k.name} verwerfen?`}
text="Die heruntergeladenen Dateien werden gelöscht, und das Radar testet dieses Modell nicht noch einmal."
knopf="Verwerfen"
gesperrt={aktion.isPending}
onNein={() => setFrage(null)}
onJa={() => {
if (frage !== "verwerfen") return
aktion.mutate({ id: k.id, aktion: "verwerfen", name: k.name })
setFrage(null)
}}
@@ -353,6 +357,7 @@ function AufraeumenPanel() {
titel={`${frage?.name ?? ""} löschen?`}
text={`${frage?.hinweis ?? ""} Es werden ${frage?.groesse_gb.toLocaleString("de-DE") ?? ""} GB frei. Das lässt sich nicht rückgängig machen — zurück ginge es nur mit einem neuen Download.`}
knopf="Endgültig löschen"
gesperrt={loeschen.isPending}
onNein={() => setFrage(null)}
onJa={() => {
if (frage) loeschen.mutate(frage)
@@ -432,7 +437,11 @@ export function ModelleSeite() {
<ModellSuche
offen={suche}
onSchliessen={() => setSuche(false)}
rollenJetzt={{ coder: coder ? { name: coder.name, zwilling: !!coderZwilling } : undefined }}
rollenJetzt={{
coder: coder
? { name: coder.name, zwilling: !!coderZwilling, weitere: coder.aliases.filter((a) => a.toLowerCase() !== "coder") }
: undefined,
}}
/>
</>
)
+18 -11
View File
@@ -1,4 +1,5 @@
import { useState } from "react"
import type { UseQueryResult } from "@tanstack/react-query"
import { ChevronRight } from "lucide-react"
import { cn } from "cn"
import { Auftraege } from "@/components/cockpit/Auftraege"
@@ -11,7 +12,7 @@ import {
useUpdateStarten, useUpdateVerlauf, useZurueckspielen,
} from "@/lib/abfragen"
import { laeuftNoch } from "@/lib/anzeige"
import { detailsHabenNeues, pruefFehlerText, versionen } from "@/lib/wartung"
import { detailsHabenNeues, jobSuche, pruefFehlerText, versionen } from "@/lib/wartung"
import type { Festgehalten, Sicherung, UpdateDetails } from "@/lib/typen"
import { flugplanZeit } from "@/lib/zeit"
@@ -110,9 +111,12 @@ function zusammenfassung(summary: string | undefined): string | null {
return s
}
function BausteinZeile({ id, name, neuLautStart, fest, gesperrt, onStart, onFreigeben }: {
function BausteinZeile({ id, name, details, hatUpdate, neuLautStart, fest, gesperrt, onStart, onFreigeben }: {
id: string
name: string
details: UseQueryResult<UpdateDetails, Error>
/** Gibt es Neues? Frische Details schlagen den gecachten Stand der Startseite (siehe UpdatesSeite). */
hatUpdate: boolean
/** Was /api/start über diesen Baustein sagt („3 Pakete“, „neuer Build“), undefined = nichts Neues. */
neuLautStart: string | undefined
fest: Festgehalten | undefined
@@ -120,13 +124,10 @@ function BausteinZeile({ id, name, neuLautStart, fest, gesperrt, onStart, onFrei
onStart: () => void
onFreigeben: () => void
}) {
const details = useUpdateDetails(id)
const d = details.data
const laedt = details.isPending
const fehler = details.isError ? details.error.message : d?.error
const v = d ? versionen(id, d) : null
// Frische Details schlagen den gecachten Stand der Startseite; wissen sie nichts, gilt der Start.
const hatUpdate = detailsHabenNeues(id, d) ?? neuLautStart != null
const neu = hatUpdate ? v?.neu ?? neuLautStart ?? null : null
const summary = zusammenfassung(d?.summary)
@@ -269,12 +270,16 @@ export function UpdatesSeite() {
const neuLautStart = new Map((start.data?.updates.bausteine ?? []).map((b) => [b.id, b.neu]))
const festgehalten = new Map((start.data?.updates.festgehalten ?? []).map((f) => [f.baustein, f]))
// /api/start kennt nur einen bis zu zehn Minuten alten Stand. Die Details prüfen frisch — sagen
// sie etwas, gilt das (für die Zeilen UND für „Alles jetzt aktualisieren“), sonst der Start.
const details = useUpdateDetails(BAUSTEINE.map((b) => b.id))
const hatNeues = BAUSTEINE.map((b, i) => detailsHabenNeues(b.id, details[i].data) ?? neuLautStart.has(b.id))
const alleJobs = jobs.data?.jobs ?? []
// Nur ein Update gleichzeitig (Riegel im Backend) — auch der Sonntags-Lauf zählt.
const updateLaeuft =
alleJobs.some((j) => j.group === "maintenance" && laeuftNoch(j)) || (start.data?.zustand.update_laeuft ?? false)
// Die Suche hat im Backend (noch) keine Gruppe; sobald sie "pruefen" heißt, sperrt der Knopf auch hier.
const suchtGerade = alleJobs.some((j) => j.group === "pruefen" && laeuftNoch(j))
// Zwei Suchen gleichzeitig stolpern über die apt-Sperre — solange eine läuft, bleibt der Knopf zu.
const suchtGerade = alleJobs.some((j) => jobSuche(j) && laeuftNoch(j))
const gesperrt = updateLaeuft || updateStarten.isPending
const zurueckMoeglich = sicherungen.data?.available ?? false
@@ -296,11 +301,13 @@ export function UpdatesSeite() {
<span role="columnheader">Was sich ändert</span>
<span role="columnheader" className="sr-only">Aktion</span>
</div>
{BAUSTEINE.map((b) => (
{BAUSTEINE.map((b, i) => (
<BausteinZeile
key={b.id}
id={b.id}
name={b.name}
details={details[i]}
hatUpdate={hatNeues[i]}
neuLautStart={neuLautStart.get(b.id)}
fest={festgehalten.get(b.id)}
gesperrt={gesperrt}
@@ -371,9 +378,9 @@ export function UpdatesSeite() {
onClick={() => updateStarten.mutate("pruefen")}
disabled={updateStarten.isPending || suchtGerade}
>
Nach Neuem suchen
{suchtGerade ? "Sucht …" : "Nach Neuem suchen"}
</Button>
<Button onClick={() => setFrage({ art: "alle" })} disabled={gesperrt || neuLautStart.size === 0}>
<Button onClick={() => setFrage({ art: "alle" })} disabled={gesperrt || !hatNeues.some(Boolean)}>
Alles jetzt aktualisieren
</Button>
</div>
@@ -413,7 +420,7 @@ export function UpdatesSeite() {
<Bestaetigen
offen={frage?.art === "alle"}
titel="Alles jetzt aktualisieren?"
text="Motor, llama-swap, Hermes und Betriebssystem werden nacheinander aktualisiert, soweit es Neues gibt. Vorher wird gesichert, danach geprüft. Lucy ist dabei ein paar Minuten nicht erreichbar."
text="Motor, llama-swap, Hermes und Betriebssystem werden nacheinander aktualisiert, soweit es Neues gibt, und danach geprüft. Motor und llama-swap holen bei Problemen ihren alten Stand von selbst zurück, vor Hermes wird gesichert. Lucy ist dabei ein paar Minuten nicht erreichbar."
knopf="Aktualisieren"
gesperrt={gesperrt}
onNein={() => setFrage(null)}