From bc1106ddc8e5b43c482d456b949a5e504c5b4dc3 Mon Sep 17 00:00:00 2001
From: Hitonabi
Date: Thu, 24 Sep 2026 17:44:40 +0200
Subject: [PATCH] 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
---
frontend/src/components/cockpit/Auftraege.tsx | 1 +
.../src/components/cockpit/Bestaetigen.tsx | 7 +-
.../src/components/cockpit/JobKarte.test.tsx | 21 ++++-
frontend/src/components/cockpit/JobKarte.tsx | 26 +++++-
.../components/cockpit/Rueckfrage.test.tsx | 15 +++-
frontend/src/lib/abfragen.ts | 42 ++++++---
frontend/src/lib/wartung.ts | 86 ++++++++++++++-----
frontend/src/views/ModellSuche.tsx | 12 ++-
frontend/src/views/Modelle.tsx | 11 ++-
frontend/src/views/Updates.tsx | 29 ++++---
10 files changed, 188 insertions(+), 62 deletions(-)
diff --git a/frontend/src/components/cockpit/Auftraege.tsx b/frontend/src/components/cockpit/Auftraege.tsx
index 2eaa696..7fb74e3 100644
--- a/frontend/src/components/cockpit/Auftraege.tsx
+++ b/frontend/src/components/cockpit/Auftraege.tsx
@@ -20,6 +20,7 @@ export function Auftraege({ alsPanel = false }: { alsPanel?: boolean }) {
abbrechen.mutate({ id: job.id, titel: jobTitel(job) })}
/>
diff --git a/frontend/src/components/cockpit/Bestaetigen.tsx b/frontend/src/components/cockpit/Bestaetigen.tsx
index 230ec01..2b7014a 100644
--- a/frontend/src/components/cockpit/Bestaetigen.tsx
+++ b/frontend/src/components/cockpit/Bestaetigen.tsx
@@ -23,13 +23,16 @@ export function Bestaetigen({ offen, titel, text, knopf, neinKnopf = "Abbrechen"
}) {
return (
)}
+
{installierenUnterbrechung(!!rolle)}
diff --git a/frontend/src/views/Modelle.tsx b/frontend/src/views/Modelle.tsx
index 40aec5f..a706399 100644
--- a/frontend/src/views/Modelle.tsx
+++ b/frontend/src/views/Modelle.tsx
@@ -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() {
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,
+ }}
/>
>
)
diff --git a/frontend/src/views/Updates.tsx b/frontend/src/views/Updates.tsx
index 8487c1d..8afc0ba 100644
--- a/frontend/src/views/Updates.tsx
+++ b/frontend/src/views/Updates.tsx
@@ -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
+ /** 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() {
Was sich ändertAktion
- {BAUSTEINE.map((b) => (
+ {BAUSTEINE.map((b, i) => (
updateStarten.mutate("pruefen")}
disabled={updateStarten.isPending || suchtGerade}
>
- Nach Neuem suchen
+ {suchtGerade ? "Sucht …" : "Nach Neuem suchen"}
-