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:
co-authored by
Claude Opus 5.5
parent
83941847ab
commit
bc1106ddc8
@@ -20,6 +20,7 @@ export function Auftraege({ alsPanel = false }: { alsPanel?: boolean }) {
|
||||
<JobKarte
|
||||
key={j.id}
|
||||
job={j}
|
||||
ebene={alsPanel ? 3 : 4}
|
||||
abbrechenGesperrt={abbrechen.isPending}
|
||||
onAbbrechen={(job) => abbrechen.mutate({ id: job.id, titel: jobTitel(job) })}
|
||||
/>
|
||||
|
||||
@@ -23,13 +23,16 @@ export function Bestaetigen({ offen, titel, text, knopf, neinKnopf = "Abbrechen"
|
||||
}) {
|
||||
return (
|
||||
<Dialog open={offen} onOpenChange={(o) => !o && onNein()}>
|
||||
<DialogContent className="border-linie bg-panel sm:max-w-lg">
|
||||
{/* Nie höher als der Bildschirm: Auf kleinen Handys (320 × 568) lagen die Knöpfe sonst außerhalb
|
||||
und ließen sich nicht erreichen. Der Text scrollt, die Knöpfe bleiben unten stehen — bündig,
|
||||
darum ohne Bodenpolster (sticky rechnet das Polster ein, darunter schien sonst Text durch). */}
|
||||
<DialogContent className="max-h-[calc(100dvh-2rem)] overflow-y-auto border-linie bg-panel pb-0 sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="schild pr-10 text-xl break-words">{titel}</DialogTitle>
|
||||
<DialogDescription className="text-[15px] leading-relaxed text-text-2">{text}</DialogDescription>
|
||||
</DialogHeader>
|
||||
{children}
|
||||
<DialogFooter className="gap-2 sm:gap-2">
|
||||
<DialogFooter className="sticky bottom-0 mb-0 gap-2 bg-erhaben sm:gap-2">
|
||||
<Button variant="outline" onClick={onNein}>
|
||||
{neinKnopf}
|
||||
</Button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react"
|
||||
import { sichtbareJobs } from "@/lib/wartung"
|
||||
import { jobTitel, sichtbareJobs } from "@/lib/wartung"
|
||||
import type { Job } from "@/lib/typen"
|
||||
import { JobKarte } from "./JobKarte"
|
||||
|
||||
@@ -44,10 +44,15 @@ describe("JobKarte", () => {
|
||||
expect(screen.queryByRole("button", { name: "Abbrechen" })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("bietet bei Updates kein Abbrechen an und zeigt bei Fehlern die letzte Meldung", () => {
|
||||
it("bietet bei Updates und der Suche kein Abbrechen an und zeigt bei Fehlern die letzte Meldung", () => {
|
||||
const { rerender } = render(
|
||||
<JobKarte job={job({ label: "Hermes-Agent-Update", group: "maintenance" })} onAbbrechen={() => {}} />,
|
||||
)
|
||||
expect(screen.getByText("Update Hermes")).toBeInTheDocument()
|
||||
expect(screen.queryByRole("button", { name: "Abbrechen" })).not.toBeInTheDocument()
|
||||
|
||||
// Die Suche läuft als Shell-Kette wie die Updates: kein Abbrechen, das womöglich nichts stoppt.
|
||||
rerender(<JobKarte job={job({ label: "Nach Updates suchen" })} onAbbrechen={() => {}} />)
|
||||
expect(screen.queryByRole("button", { name: "Abbrechen" })).not.toBeInTheDocument()
|
||||
|
||||
rerender(
|
||||
@@ -63,6 +68,18 @@ describe("JobKarte", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("jobTitel", () => {
|
||||
it("nennt die Bausteine wie die Oberfläche und lässt Unbekanntes stehen", () => {
|
||||
expect(jobTitel({ label: "Engine-Update (llama.cpp Vulkan)" })).toBe("Update Motor (llama.cpp)")
|
||||
expect(jobTitel({ label: "Router-Update (llama-swap)" })).toBe("Update llama-swap")
|
||||
expect(jobTitel({ label: "OS-Update (apt)" })).toBe("Update Betriebssystem")
|
||||
expect(jobTitel({ label: "Alle aktualisieren (Engine (llama.cpp) → Router (llama-swap) → Hermes-Agent → OS (apt))" }))
|
||||
.toBe("Alle aktualisieren (Motor → llama-swap → Hermes → Betriebssystem)")
|
||||
expect(jobTitel({ label: "download unsloth/Qwen3.8-27B-GGUF" })).toBe("Download unsloth/Qwen3.8-27B-GGUF")
|
||||
expect(jobTitel({ label: "Nach Updates suchen" })).toBe("Nach Updates suchen")
|
||||
})
|
||||
})
|
||||
|
||||
describe("sichtbareJobs", () => {
|
||||
it("zeigt alle laufenden Aufträge jeder Gruppe und die drei zuletzt beendeten", () => {
|
||||
const liste = sichtbareJobs(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { cn } from "cn"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { laeuftNoch } from "@/lib/anzeige"
|
||||
@@ -22,14 +22,30 @@ const BALKEN: Record<JobZustand, string> = {
|
||||
|
||||
const zeitVon = (sekunden: number) => flugplanZeit(new Date(sekunden * 1000).toISOString())
|
||||
|
||||
/**
|
||||
* Die Uhrzeit in Sekunden, alle 30 s neu, solange `aktiv`. Ohne sie bliebe „Läuft seit 5 min“ stehen,
|
||||
* wenn ein Auftrag eine Weile nichts schreibt: Die Karte zeichnet sonst nur neu, wenn sich der Auftrag ändert.
|
||||
*/
|
||||
function useJetzt(aktiv: boolean) {
|
||||
const [jetzt, setJetzt] = useState(() => Date.now() / 1000)
|
||||
useEffect(() => {
|
||||
if (!aktiv) return
|
||||
const t = window.setInterval(() => setJetzt(Date.now() / 1000), 30_000)
|
||||
return () => window.clearInterval(t)
|
||||
}, [aktiv])
|
||||
return jetzt
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein Hintergrund-Auftrag als Karte: Titel und Zustand, bei Downloads der Fortschritt, bei Fehlern
|
||||
* die letzte Meldung, auf Wunsch das Protokoll. Abbrechen nur, wo es wirklich stoppt (`abbrechbar`).
|
||||
* `ebene` ist die Überschriftenebene (unter einem Panel-Titel h3, unter einer Zwischenüberschrift h4).
|
||||
*/
|
||||
export function JobKarte({ job, onAbbrechen, abbrechenGesperrt }: {
|
||||
export function JobKarte({ job, onAbbrechen, abbrechenGesperrt, ebene = 4 }: {
|
||||
job: Job
|
||||
onAbbrechen?: (job: Job) => void
|
||||
abbrechenGesperrt?: boolean
|
||||
ebene?: 3 | 4
|
||||
}) {
|
||||
const [frage, setFrage] = useState(false)
|
||||
const [protokoll, setProtokoll] = useState(false)
|
||||
@@ -38,13 +54,15 @@ export function JobKarte({ job, onAbbrechen, abbrechenGesperrt }: {
|
||||
const fortschritt = fortschrittText(job)
|
||||
const meldung = letzteMeldung(job)
|
||||
const laeuft = laeuftNoch(job)
|
||||
const jetzt = useJetzt(laeuft)
|
||||
const darfAbbrechen = !!onAbbrechen && abbrechbar(job)
|
||||
const protokollZeilen = (job.log ?? []).slice(-12)
|
||||
const Titel = ebene === 3 ? "h3" : "h4"
|
||||
|
||||
return (
|
||||
<article aria-label={`Auftrag: ${titel}`} className="flex flex-col gap-2.5 rounded-xl border border-linie bg-erhaben/50 p-3.5">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<h4 className="m-0 min-w-0 text-[15px] leading-snug font-semibold break-words">{titel}</h4>
|
||||
<Titel className="m-0 min-w-0 text-[15px] leading-snug font-semibold break-words">{titel}</Titel>
|
||||
<Chip art={zustand.art}>{zustand.text}</Chip>
|
||||
</div>
|
||||
|
||||
@@ -66,7 +84,7 @@ export function JobKarte({ job, onAbbrechen, abbrechenGesperrt }: {
|
||||
|
||||
<p className="ziffern m-0 text-sm text-text-2">
|
||||
{laeuft
|
||||
? fortschritt ?? laufText(job)
|
||||
? fortschritt ?? laufText(job, jetzt)
|
||||
: `${zustand.text} · ${zeitVon(job.finished_at ?? job.started_at)}`}
|
||||
</p>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { fireEvent, render, screen, waitFor, within } from "@testing-library/rea
|
||||
import { hinweisRueckfrage } from "@/lib/anzeige"
|
||||
import { post } from "@/lib/api"
|
||||
import type { Hinweis } from "@/lib/typen"
|
||||
import { rollenErsatzText } from "@/lib/wartung"
|
||||
import { installierenUnterbrechung, rollenErsatzText } from "@/lib/wartung"
|
||||
import { Bestaetigen } from "./Bestaetigen"
|
||||
import { Checkliste } from "./Checkliste"
|
||||
|
||||
@@ -63,11 +63,18 @@ describe("Bestaetigen", () => {
|
||||
})
|
||||
|
||||
describe("rollenErsatzText", () => {
|
||||
it("sagt deutlich, welches Modell die Rolle verliert", () => {
|
||||
expect(rollenErsatzText("org/Neu-GGUF", "Coder", { name: "Qwen3.8-27B-Heretic", zwilling: true })).toBe(
|
||||
it("sagt deutlich, welches Modell die Rolle verliert und was es behält", () => {
|
||||
expect(rollenErsatzText("org/Neu-GGUF", "Coder", { name: "Qwen3.8-27B-Heretic", zwilling: true, weitere: ["heavy"] })).toBe(
|
||||
"Achtung: Nach dem Download übernimmt org/Neu-GGUF die Rolle „Coder“ und ersetzt damit Qwen3.8-27B-Heretic. " +
|
||||
"Das bisherige Modell bleibt auf der Platte. Bilder liest vorerst weiter sein Bild-Zwilling, also noch das bisherige Modell.",
|
||||
"Das bisherige Modell bleibt auf der Platte. Die Rolle „heavy“ behält es. " +
|
||||
"Bilder liest vorerst weiter sein Bild-Zwilling, also noch das bisherige Modell.",
|
||||
)
|
||||
expect(rollenErsatzText("org/Neu-GGUF", "Coder", undefined)).toBe("Nach dem Download übernimmt org/Neu-GGUF die Rolle „Coder“.")
|
||||
})
|
||||
|
||||
it("warnt vor der Unterbrechung durch das Neuladen von llama-swap", () => {
|
||||
expect(installierenUnterbrechung(false)).toContain("bricht ab")
|
||||
expect(installierenUnterbrechung(false)).not.toContain("noch einmal")
|
||||
expect(installierenUnterbrechung(true)).toContain("Mit Rolle passiert das nach dem Download noch einmal.")
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user