- /api/instanz: Rolle der ausliefernden Instanz, ohne Netzabfragen - lib/instanz.ts: /api/homelab/... an die Homelab-Instanz, alles andere an die Box, ueber /api/partner/..., wenn die andere Instanz die Seite ausliefert - neue Seite Homelab: Stand der zweiten Instanz (noch nicht eingerichtet / verbunden / antwortet nicht) und was dazukommt - Kopfzeile, Titel und Web-Manifest mit neuem Namen; fuenfter Menuepunkt, 320 px geprueft Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
265 lines
10 KiB
TypeScript
265 lines
10 KiB
TypeScript
// Alle Abfragen und Aktionen der Oberfläche an einem Ort (TanStack Query).
|
|
//
|
|
// Takte: Steht der Live-Strom, meldet er Änderungen selbst — die Abfragen sind dann nur
|
|
// Sicherheitsnetz und laufen fünfmal seltener.
|
|
|
|
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"
|
|
import { stromVerbunden } from "./strom"
|
|
import type {
|
|
AktionsErgebnis, AufraeumKandidat, Instanz, Job, Modell, Nutzung, PartnerStatus, Radar, Sicherung, Start,
|
|
UpdateDetails,
|
|
} from "./typen"
|
|
import { gbText } from "./zeit"
|
|
|
|
const takt = (ms: number) => () => (stromVerbunden() ? ms * 5 : ms)
|
|
|
|
/**
|
|
* 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.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 = () =>
|
|
useQuery({
|
|
queryKey: ["start"],
|
|
queryFn: () => api<Start>("/api/start"),
|
|
// Sucht MC2 gerade noch nach Updates (nach einem Neustart), bald wieder nachsehen.
|
|
refetchInterval: (q) => (q.state.data && !q.state.data.updates.gelesen ? 4000 : takt(20_000)()),
|
|
})
|
|
|
|
export const useModelle = () =>
|
|
useQuery({
|
|
queryKey: ["models"],
|
|
queryFn: () => api<{ models: Modell[]; running: string[] }>("/api/models"),
|
|
refetchInterval: takt(30_000),
|
|
})
|
|
|
|
export const useNutzung = () =>
|
|
useQuery({ queryKey: ["nutzung"], queryFn: () => api<Nutzung>("/api/modelle/nutzung"), refetchInterval: 600_000 })
|
|
|
|
export const useRadar = () =>
|
|
useQuery({ queryKey: ["radar"], queryFn: () => api<Radar>("/api/radar"), retry: false, refetchInterval: 300_000 })
|
|
|
|
export const useJobs = () => {
|
|
const qc = useQueryClient()
|
|
return useQuery({
|
|
queryKey: ["jobs"],
|
|
queryFn: async () => {
|
|
const daten = await api<{ jobs: Job[] }>("/api/jobs")
|
|
auftragsEndeMelden(qc, daten.jobs)
|
|
return daten
|
|
},
|
|
// Läuft etwas, wollen wir den Fortschritt sehen; sonst reicht der Strom.
|
|
refetchInterval: (q) => (q.state.data?.jobs.some(laeuftNoch) ? 2000 : takt(30_000)()),
|
|
})
|
|
}
|
|
|
|
export const useSicherungen = () =>
|
|
useQuery({
|
|
queryKey: ["sicherungen"],
|
|
queryFn: () => api<{ available: boolean; backups: Sicherung[] }>("/api/zeitmaschine"),
|
|
})
|
|
|
|
/**
|
|
* 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 {
|
|
start: string
|
|
ende: string
|
|
anlass: string
|
|
stufe: "rot" | "gelb" | "gruen" | "info" | "grau"
|
|
titel: string
|
|
zeilen: { baustein: string; ergebnis: string; stufe: UpdateLauf["stufe"]; text: string }[]
|
|
}
|
|
|
|
export const useUpdateVerlauf = () =>
|
|
useQuery({ queryKey: ["updates-verlauf"], queryFn: () => api<{ laeufe: UpdateLauf[] }>("/api/updates/verlauf") })
|
|
|
|
export const useProtokoll = (dienst: string | null) =>
|
|
useQuery({
|
|
queryKey: ["protokoll", dienst],
|
|
queryFn: () => api<{ ok: boolean; text?: string; err?: string }>(
|
|
`/api/maintenance/logs?service=${encodeURIComponent(dienst ?? "")}&lines=200`),
|
|
enabled: !!dienst,
|
|
})
|
|
|
|
export interface Dienst {
|
|
name: string
|
|
unit: string
|
|
ok: boolean
|
|
scope: string
|
|
/** Bewusst abgeschaltet (systemctl disable) — kein Fehler, nur geweckt wird per Knopf. */
|
|
schlaeft?: boolean
|
|
}
|
|
|
|
export const useDienste = () =>
|
|
useQuery({ queryKey: ["dienste"], queryFn: () => api<{ services: Dienst[] }>("/api/system/services") })
|
|
|
|
// --- Aktionen ----------------------------------------------------------------
|
|
|
|
function useAktion<V, E extends AktionsErgebnis | Record<string, unknown>>(
|
|
fn: (v: V) => Promise<E>,
|
|
opt: {
|
|
erfolg: (v: V, e: E) => string
|
|
/** Eigener Text, wenn die Box mit ok: false antwortet (sonst der Grund aus der Antwort). */
|
|
fehler?: (v: V, e: AktionsErgebnis) => string
|
|
neuLaden: string[]
|
|
},
|
|
) {
|
|
const qc = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: fn,
|
|
onSuccess: (e, v) => {
|
|
const ergebnis = e as AktionsErgebnis
|
|
if (ergebnis && ergebnis.ok === false) {
|
|
melden("fehler", opt.fehler?.(v, ergebnis) ?? fehlerText(ergebnis))
|
|
} else {
|
|
melden("erfolg", opt.erfolg(v, e))
|
|
}
|
|
for (const k of opt.neuLaden) qc.invalidateQueries({ queryKey: [k] })
|
|
},
|
|
onError: (f: Error) => melden("fehler", f.message),
|
|
})
|
|
}
|
|
|
|
export const useHinweisAktion = () =>
|
|
useAktion(
|
|
({ hinweis, aktion }: { hinweis: string; aktion: string; label: string }) =>
|
|
post<AktionsErgebnis>(`/api/hinweise/${encodeURIComponent(hinweis)}/aktion/${aktion}`),
|
|
{
|
|
erfolg: (v, e) =>
|
|
v.aktion === "freigeben" && e.text ? e.text : `${v.label}: erledigt. Der Wächter prüft in einer Minute nach.`,
|
|
neuLaden: ["start"],
|
|
},
|
|
)
|
|
|
|
const UPDATE_PFADE: Record<string, string> = {
|
|
alle: "/api/maintenance/update-all",
|
|
pruefen: "/api/maintenance/check-updates",
|
|
os: "/api/maintenance/os-update",
|
|
engine: "/api/maintenance/engine-update",
|
|
swap: "/api/maintenance/swap-update",
|
|
hermes: "/api/maintenance/hermes-update",
|
|
}
|
|
|
|
export const useUpdateStarten = () =>
|
|
useAktion((art: string) => post<AktionsErgebnis>(UPDATE_PFADE[art]), {
|
|
erfolg: (art) =>
|
|
art === "pruefen"
|
|
? "Die Suche nach Neuem läuft. Den Fortschritt siehst du unten bei den Aufträgen."
|
|
: "Das Update läuft. Den Fortschritt siehst du unten bei den Aufträgen.",
|
|
neuLaden: ["jobs", "start"],
|
|
})
|
|
|
|
/** Einen laufenden Auftrag abbrechen (nur Downloads und die Suche, siehe `abbrechbar`). */
|
|
export const useJobAbbrechen = () =>
|
|
useAktion(({ id }: { id: string; titel: string }) => post<AktionsErgebnis>(`/api/jobs/${encodeURIComponent(id)}/cancel`), {
|
|
erfolg: (v) => `${v.titel}: wird abgebrochen.`,
|
|
fehler: (v) => `${v.titel} lief schon nicht mehr.`,
|
|
neuLaden: ["jobs"],
|
|
})
|
|
|
|
/** Ein Modell von Hugging Face holen (Hintergrund-Auftrag, mit optionaler Rolle nach dem Download). */
|
|
export const useInstallieren = () =>
|
|
useAktion(
|
|
({ repo, quant, rolle }: { repo: string; quant: string; rolle: string | null }) =>
|
|
post<AktionsErgebnis & { total_bytes?: number }>("/api/models/install", { repo, quant, role: rolle }),
|
|
{
|
|
erfolg: (v, e) =>
|
|
`${v.repo} (${v.quant}) wird heruntergeladen${e.total_bytes ? `, ${gbText(e.total_bytes, 1)}` : ""}. ` +
|
|
"Den Fortschritt siehst du unter Aufträge.",
|
|
neuLaden: ["jobs", "models"],
|
|
},
|
|
)
|
|
|
|
export const useAufraeumen = () =>
|
|
useQuery({
|
|
queryKey: ["aufraeumen"],
|
|
queryFn: () => api<{ kandidaten: AufraeumKandidat[] }>("/api/modelle/aufraeumen"),
|
|
staleTime: 60_000,
|
|
})
|
|
|
|
export const useAufraeumenLoeschen = () =>
|
|
useAktion(
|
|
(k: AufraeumKandidat) => post<AktionsErgebnis>("/api/modelle/aufraeumen/loeschen", { art: k.art, name: k.name }),
|
|
{ erfolg: (_, e) => (e as AktionsErgebnis).text ?? "Gelöscht.", neuLaden: ["aufraeumen", "models", "start"] },
|
|
)
|
|
|
|
export const useFreigeben = () =>
|
|
useAktion(
|
|
(baustein: string) => post<AktionsErgebnis>(`/api/updates/festgehalten/${encodeURIComponent(baustein)}/freigeben`),
|
|
{ erfolg: (_, e) => (e as AktionsErgebnis).text ?? "Freigegeben.", neuLaden: ["start"] },
|
|
)
|
|
|
|
export const useJetztSichern = () =>
|
|
useAktion(() => post<AktionsErgebnis & { size_mb?: number }>("/api/system/backup"), {
|
|
erfolg: (_, e) =>
|
|
e.size_mb != null ? `Sicherung erstellt (${e.size_mb.toLocaleString("de-DE")} MB).` : "Sicherung erstellt.",
|
|
fehler: (_, e) => `Die Sicherung ging nicht: ${fehlerText(e)}`,
|
|
neuLaden: ["sicherungen", "start"],
|
|
})
|
|
|
|
export const useZurueckspielen = () =>
|
|
useAktion((datei: string) => post<AktionsErgebnis>("/api/zeitmaschine/restore", { file: datei }), {
|
|
erfolg: () => "Zurückspielen läuft. Die Dienste starten gleich neu.",
|
|
neuLaden: ["sicherungen"],
|
|
})
|
|
|
|
export const useModellLaden = () =>
|
|
useAktion((name: string) => post<AktionsErgebnis>(`/api/models/${encodeURIComponent(name)}/load`), {
|
|
erfolg: (name) => `${name} wird geladen.`,
|
|
neuLaden: ["models", "start"],
|
|
})
|
|
|
|
export const useRadarAktion = () =>
|
|
useAktion(
|
|
({ id, aktion }: { id: string; aktion: "uebernehmen" | "verwerfen"; name: string }) =>
|
|
post<AktionsErgebnis>(`/api/radar/${encodeURIComponent(id)}/${aktion}`),
|
|
{
|
|
erfolg: (v) => (v.aktion === "uebernehmen" ? `${v.name} übernommen.` : `${v.name} verworfen.`),
|
|
neuLaden: ["radar", "models", "start"],
|
|
},
|
|
)
|
|
|
|
export const useRadarSuche = () =>
|
|
useAktion(() => post<AktionsErgebnis>("/api/radar/suche"), {
|
|
erfolg: () => "Radar sucht nach neuen Modellen.",
|
|
neuLaden: ["radar"],
|
|
})
|
|
|
|
/** Wer liefert die Seite aus, und gibt es die zweite Instanz? (ändert sich nur mit einem Deploy) */
|
|
export const useInstanz = () =>
|
|
useQuery({ queryKey: ["instanz"], queryFn: () => api<Instanz>("/api/instanz"), staleTime: Infinity })
|
|
|
|
/** Lebenszeichen der anderen Instanz — die Box prüft den Homelab-Teil und umgekehrt. */
|
|
export const usePartner = () =>
|
|
useQuery({ queryKey: ["partner"], queryFn: () => api<PartnerStatus>("/api/partner"), refetchInterval: 30_000 })
|