Welle 2 · Zeitleiste je Gerät: Protokoll nach Gerät, Updates von → nach, Hinweise mit ihrem Gerät

/homelab/protokoll#ct-104 zeigt die Zeitleiste eines Geräts (Auswahl oben, Link „Zeitleiste“ je Gerät in der
Geräte-Tabelle); die Punkte eines Tages hängen an einer Linie. Das Protokoll filtert dafür im Backend (?ziel=…),
Update-Läufe heißen „Update eingespielt · Gitea 1.27.2 → 1.27.3“, und Hinweise finden ihr Gerät über ihre Quelle
(der Wächter schreibt sie seit heute in seinen Verlauf) — auch Zertifikate, Docker und Sicherungen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-25 18:06:41 +02:00
co-authored by Claude Opus 5.5
parent 1c66daad0b
commit 83bb227969
7 changed files with 84 additions and 29 deletions
@@ -77,7 +77,11 @@ export function Gesundheit({ ziele, messwerte }: { ziele: ZielStand[]; messwerte
<Link to="/homelab/monitoring" hash={z.id} className="text-[15px] font-semibold break-words hover:text-lab-text">
{z.name}
</Link>
{artText(z) !== z.name && <span className="text-[13px] text-text-3">{artText(z)}</span>}
<span className="flex flex-wrap gap-x-2 text-[13px] text-text-3">
{artText(z) !== z.name && <span>{artText(z)}</span>}
{/* Zeitleiste je Gerät (seit 25.09.2026). */}
<Link to="/homelab/protokoll" hash={z.id} className="text-lab-text hover:underline">Zeitleiste</Link>
</span>
</span>
</div>
<span role="cell" className={cn("flex items-center gap-1.5 text-[13px]", z.erreichbar === false ? "text-rot-text" : "text-text-3")}>
+6 -3
View File
@@ -479,10 +479,13 @@ export const useSammellaufQuittieren = () =>
neuLaden: ["homelab-sammellauf"],
})
export const useHomelabProtokoll = (aktiv: boolean, berichte: boolean) =>
/** Das Protokoll des Homelab-Teils; mit ziel nur ein Gerät (Zeitleiste je Gerät, seit 25.09.2026). */
export const useHomelabProtokoll = (aktiv: boolean, berichte: boolean, ziel: string | null = null) =>
useQuery({
queryKey: ["homelab-protokoll", berichte],
queryFn: () => api<{ eintraege: ProtokollEintrag[] }>(`/api/homelab/protokoll?grenze=300&berichte=${berichte ? 1 : 0}`),
queryKey: ["homelab-protokoll", berichte, ziel],
queryFn: () => api<{ eintraege: ProtokollEintrag[] }>(
`/api/homelab/protokoll?grenze=300&berichte=${berichte ? 1 : 0}${ziel ? `&ziel=${encodeURIComponent(ziel)}` : ""}`,
),
enabled: aktiv,
refetchInterval: takt(30_000),
retry: false,
+28 -9
View File
@@ -1,4 +1,5 @@
import { useState } from "react"
import { useNavigate, useRouterState } from "@tanstack/react-router"
import { Bell, ChevronRight, Cog, History, RefreshCw, Search, TriangleAlert, type LucideIcon } from "lucide-react"
import { cn } from "cn"
import { Zustandsfeld } from "@/components/cockpit/Chip"
@@ -7,7 +8,9 @@ import { useHomelab, useHomelabProtokoll, useInstanz, usePartner } from "@/lib/a
import type { ProtokollEintrag } from "@/lib/typen"
// Was der Homelab-Teil getan hat: Aufträge an den Ausführer (Snapshot, Update, Suchen …), Update-Läufe,
// Hinweise des Wächters, gesendete Meldungen und die nächtliche Pflege — neueste zuerst, nach Tagen.
// Hinweise des Wächters, gesendete Meldungen und die nächtliche Pflege — neueste zuerst, nach Tagen. Seit 25.09.2026
// (Ausbauplan Welle 2, Punkt 12) auch je Gerät: /homelab/protokoll#ct-104 ist die Zeitleiste von Gitea — Updates
// von → nach, Snapshots, Rückwege, Neustarts und Hinweise auf einer Linie.
type Art = ProtokollEintrag["art"]
@@ -55,8 +58,10 @@ function Eintrag({ e, geraet }: { e: ProtokollEintrag; geraet: string | null })
return (
<li className="grid grid-cols-[64px_18px_minmax(0,1fr)] gap-x-3 border-t border-white/[0.06] py-2.5 first:border-t-0">
<span className="ziffern pt-0.5 text-xs text-text-3">{uhr.format(new Date(e.zeit * 1000))}</span>
<span className="flex flex-col items-center pt-1">
<span aria-hidden className={cn("size-2.5 rounded-full", s.punkt)} />
{/* Die Linie verbindet die Punkte eines Tages zur Zeitleiste. */}
<span className="relative flex flex-col items-center pt-1">
<span aria-hidden className="absolute -top-2.5 -bottom-2.5 w-px bg-white/10" />
<span aria-hidden className={cn("relative size-2.5 rounded-full ring-4 ring-[var(--panel)]", s.punkt)} />
</span>
<div className="flex min-w-0 flex-col gap-0.5">
<span className="flex flex-wrap items-center gap-x-2 text-[15px]">
@@ -91,8 +96,12 @@ export function HomelabProtokollSeite() {
const aktiv = instanz.data?.rolle === "homelab" || !!(partner.data?.eingerichtet && partner.data.erreichbar)
const [art, setArt] = useState<Art | "alle">("alle")
const [berichte, setBerichte] = useState(false)
const protokoll = useHomelabProtokoll(aktiv, berichte)
const namen = new Map((useHomelab(aktiv).data?.ziele ?? []).map((z) => [z.id, z.name]))
const navigate = useNavigate()
const hash = useRouterState({ select: (s) => s.location.hash })
const ziele = useHomelab(aktiv).data?.ziele ?? []
const namen = new Map(ziele.map((z) => [z.id, z.name]))
const geraet = hash && /^(pve|(ct|vm)-\d+)$/.test(hash) ? hash : null
const protokoll = useHomelabProtokoll(aktiv, berichte, geraet)
if (instanz.isPending || partner.isPending) return <Zustandsfeld text="Wird gelesen …" />
if (!aktiv) return <Zustandsfeld fehler text="Der Homelab-Teil antwortet gerade nicht." />
@@ -108,11 +117,21 @@ export function HomelabProtokollSeite() {
}
return (
<Panel titel="Protokoll">
<Panel titel={geraet ? `Zeitleiste · ${namen.get(geraet) ?? geraet}` : "Protokoll"}>
<p className="-mt-2 text-[15px] text-text-2">
Was der Homelab-Teil getan hat: Aufträge an den Ausführer auf dem Proxmox-Host, Update-Läufe, Snapshots,
Hinweise, gesendete Meldungen und die nächtliche Paketsuche.
{geraet
? "Alles, was an diesem Gerät geschah: Updates von → nach, Snapshots, Rückwege, Neustarts und Hinweise."
: "Was der Homelab-Teil getan hat: Aufträge an den Ausführer auf dem Proxmox-Host, Update-Läufe, Snapshots, " +
"Hinweise, gesendete Meldungen und die nächtliche Paketsuche."}
</p>
<label className="flex max-w-sm flex-col gap-1 text-sm text-text-2">
Gerät
<select value={geraet ?? ""} onChange={(ev) => void navigate({ to: "/homelab/protokoll", hash: ev.target.value || undefined })}
className="h-10 rounded-xl border border-white/12 bg-white/[0.04] px-3 text-[15px] text-foreground outline-none focus:border-lab/60">
<option value="">Alle Geräte</option>
{ziele.map((z) => <option key={z.id} value={z.id}>{z.name}</option>)}
</select>
</label>
<div className="flex flex-wrap items-center gap-2" role="group" aria-label="Filter">
{ARTEN.map((a) => (
<button key={a.id} type="button" onClick={() => setArt(a.id)} aria-pressed={art === a.id}
@@ -142,7 +161,7 @@ export function HomelabProtokollSeite() {
<ol className="m-0 flex list-none flex-col p-0">
{t.eintraege.map((e, i) => {
// Das Gerät als Schild — außer, der Titel nennt es schon.
const name = e.ziel ? namen.get(e.ziel) ?? e.ziel : null
const name = e.ziel && !geraet ? namen.get(e.ziel) ?? e.ziel : null
return <Eintrag key={`${e.zeit}-${e.titel}-${i}`} e={e} geraet={name && !e.titel.includes(name) ? name : null} />
})}
</ol>