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:
co-authored by
Claude Opus 5.5
parent
1c66daad0b
commit
83bb227969
@@ -162,9 +162,10 @@ def sammellauf_quittieren(sl_id: str) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/protokoll")
|
@router.get("/protokoll")
|
||||||
def protokoll_lesen(grenze: int = 200, berichte: bool = False) -> dict:
|
def protokoll_lesen(grenze: int = 200, berichte: bool = False, ziel: str | None = None) -> dict:
|
||||||
|
"""Was geschah, neueste zuerst; mit ziel nur ein Gerät (Zeitleiste je Gerät, seit 25.09.2026)."""
|
||||||
from services.homelab import protokoll
|
from services.homelab import protokoll
|
||||||
return {"eintraege": protokoll.eintraege(grenze, berichte)}
|
return {"eintraege": protokoll.eintraege(grenze, berichte, ziel or None)}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/speicher")
|
@router.get("/speicher")
|
||||||
|
|||||||
@@ -347,7 +347,10 @@ def _lauf(x: dict, schw: Callable) -> dict | None:
|
|||||||
elif ergebnis == "eingespielt":
|
elif ergebnis == "eingespielt":
|
||||||
stufe = "ok"
|
stufe = "ok"
|
||||||
if baustein == "app":
|
if baustein == "app":
|
||||||
titel = f"Update eingespielt · {name}" + (f" {x['version_neu']}" if x.get("version_neu") else "")
|
# Seit 25.09.2026 „von → nach“, wo der Lauf beide kennt (Zeitleiste je Gerät).
|
||||||
|
von_nach = (f" {x['version_alt']} → {x['version_neu']}" if x.get("version_alt") and x.get("version_neu")
|
||||||
|
else f" {x['version_neu']}" if x.get("version_neu") else "")
|
||||||
|
titel = f"Update eingespielt · {name}{von_nach}"
|
||||||
elif baustein == "docker":
|
elif baustein == "docker":
|
||||||
titel = f"Docker-Images aktualisiert · {name}"
|
titel = f"Docker-Images aktualisiert · {name}"
|
||||||
elif baustein == "pakete":
|
elif baustein == "pakete":
|
||||||
@@ -407,7 +410,13 @@ def _waechter_pfad() -> Path:
|
|||||||
return Path(os.environ.get("MC_WAECHTER_STORE", str(einstellungen().daten_dir / "mc2-waechter.json")))
|
return Path(os.environ.get("MC_WAECHTER_STORE", str(einstellungen().daten_dir / "mc2-waechter.json")))
|
||||||
|
|
||||||
|
|
||||||
def _ziel_aus(hinweis_id: str) -> str | None:
|
_GERAET = re.compile(r"pve|(?:ct|vm)-\d+")
|
||||||
|
|
||||||
|
|
||||||
|
def _ziel_aus(hinweis_id: str, quelle: object = None) -> str | None:
|
||||||
|
"""Das Gerät eines Hinweises: seit 25.09.2026 aus seiner Quelle, wenn sie ein Gerät ist, sonst aus der ID."""
|
||||||
|
if isinstance(quelle, str) and _GERAET.fullmatch(quelle):
|
||||||
|
return quelle
|
||||||
if m := re.search(r"\b(ct|vm)-(\d+)\b", hinweis_id):
|
if m := re.search(r"\b(ct|vm)-(\d+)\b", hinweis_id):
|
||||||
return f"{m.group(1)}-{m.group(2)}"
|
return f"{m.group(1)}-{m.group(2)}"
|
||||||
if m := re.fullmatch(r"gast-platte:(\d+)", hinweis_id):
|
if m := re.fullmatch(r"gast-platte:(\d+)", hinweis_id):
|
||||||
@@ -428,15 +437,15 @@ def _hinweise(schw: Callable) -> list[dict]:
|
|||||||
continue
|
continue
|
||||||
art, hid = v.get("art"), str(v.get("id") or "")
|
art, hid = v.get("art"), str(v.get("id") or "")
|
||||||
titel, h = str(v.get("text") or hid), aktuell.get(hid) if isinstance(aktuell.get(hid), dict) else {}
|
titel, h = str(v.get("text") or hid), aktuell.get(hid) if isinstance(aktuell.get(hid), dict) else {}
|
||||||
|
ziel = _ziel_aus(hid, v.get("quelle") or h.get("quelle"))
|
||||||
if art == "neu":
|
if art == "neu":
|
||||||
erschienen.add(hid)
|
erschienen.add(hid)
|
||||||
e = _eintrag(v.get("ts"), "hinweis", f"Hinweis: {titel}", h.get("text"),
|
e = _eintrag(v.get("ts"), "hinweis", f"Hinweis: {titel}", h.get("text"),
|
||||||
STUFE_HINWEIS.get(str(v.get("stufe") or h.get("stufe")), "warn"), _ziel_aus(hid), None, schw)
|
STUFE_HINWEIS.get(str(v.get("stufe") or h.get("stufe")), "warn"), ziel, None, schw)
|
||||||
elif art == "erledigt":
|
elif art == "erledigt":
|
||||||
e = _eintrag(v.get("ts"), "hinweis", f"Erledigt: {titel}", None, "ok", _ziel_aus(hid), None, schw)
|
e = _eintrag(v.get("ts"), "hinweis", f"Erledigt: {titel}", None, "ok", ziel, None, schw)
|
||||||
elif art == "auto":
|
elif art == "auto":
|
||||||
e = _eintrag(v.get("ts"), "hinweis", titel, "Selbstreparatur des Wächters.", "info", _ziel_aus(hid),
|
e = _eintrag(v.get("ts"), "hinweis", titel, "Selbstreparatur des Wächters.", "info", ziel, None, schw)
|
||||||
None, schw)
|
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
eintraege.append(e)
|
eintraege.append(e)
|
||||||
@@ -552,8 +561,9 @@ def _pflege(geraete: dict, schw: Callable) -> list[dict]:
|
|||||||
|
|
||||||
# --- Zusammen -------------------------------------------------------------------------------------------
|
# --- Zusammen -------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
def eintraege(grenze: int = 200, berichte: bool = False) -> list[dict]:
|
def eintraege(grenze: int = 200, berichte: bool = False, ziel: str | None = None) -> list[dict]:
|
||||||
"""Alle Einträge, neueste zuerst, höchstens `grenze` (1 bis GRENZE_MAX)."""
|
"""Alle Einträge, neueste zuerst, höchstens `grenze` (1 bis GRENZE_MAX). Mit ziel (seit 25.09.2026) nur die eines
|
||||||
|
Geräts — die Zeitleiste je Gerät."""
|
||||||
grenze = max(1, min(int(grenze), GRENZE_MAX))
|
grenze = max(1, min(int(grenze), GRENZE_MAX))
|
||||||
schw = _schwaerzer()
|
schw = _schwaerzer()
|
||||||
geraete = _geraete()
|
geraete = _geraete()
|
||||||
@@ -568,6 +578,6 @@ def eintraege(grenze: int = 200, berichte: bool = False) -> list[dict]:
|
|||||||
alle += _hinweise(schw)
|
alle += _hinweise(schw)
|
||||||
alle += _meldungen(_betreffe(laeufe, sammellaeufe), schw)
|
alle += _meldungen(_betreffe(laeufe, sammellaeufe), schw)
|
||||||
alle += _pflege(geraete, schw)
|
alle += _pflege(geraete, schw)
|
||||||
liste = [e for e in alle if e is not None]
|
liste = [e for e in alle if e is not None and (ziel is None or e.get("ziel") == ziel)]
|
||||||
liste.sort(key=lambda e: e["zeit"], reverse=True)
|
liste.sort(key=lambda e: e["zeit"], reverse=True)
|
||||||
return liste[:grenze]
|
return liste[:grenze]
|
||||||
|
|||||||
@@ -664,10 +664,12 @@ def _speichere() -> None:
|
|||||||
log.warning("waechter: Stand %s nicht schreibbar", STORE_PATH, exc_info=True)
|
log.warning("waechter: Stand %s nicht schreibbar", STORE_PATH, exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
def _verlauf(art: str, hinweis_id: str, text: str, stufe: str | None = None) -> None:
|
def _verlauf(art: str, hinweis_id: str, text: str, stufe: str | None = None, quelle: str | None = None) -> None:
|
||||||
eintrag = {"ts": time.time(), "art": art, "id": hinweis_id, "text": text}
|
eintrag = {"ts": time.time(), "art": art, "id": hinweis_id, "text": text}
|
||||||
if stufe: # seit 24.09.2026: das Protokoll des Homelab-Teils zeigt, wie ernst ein Hinweis war
|
if stufe: # seit 24.09.2026: das Protokoll des Homelab-Teils zeigt, wie ernst ein Hinweis war
|
||||||
eintrag["stufe"] = stufe
|
eintrag["stufe"] = stufe
|
||||||
|
if quelle: # seit 25.09.2026: welches Gerät — für die Zeitleiste je Gerät
|
||||||
|
eintrag["quelle"] = quelle
|
||||||
_stand["verlauf"].append(eintrag)
|
_stand["verlauf"].append(eintrag)
|
||||||
del _stand["verlauf"][:-VERLAUF_MAX]
|
del _stand["verlauf"][:-VERLAUF_MAX]
|
||||||
|
|
||||||
@@ -744,7 +746,7 @@ def takt() -> None:
|
|||||||
h.pop("fehlt", None)
|
h.pop("fehlt", None)
|
||||||
hinweise[b.id] = h
|
hinweise[b.id] = h
|
||||||
if neu:
|
if neu:
|
||||||
_verlauf("neu", b.id, b.titel, b.stufe)
|
_verlauf("neu", b.id, b.titel, b.stufe, b.quelle)
|
||||||
if b.stufe == "rot" and (neu or jetzt - h.get("gemeldet", 0.0) >= REMIND_S) \
|
if b.stufe == "rot" and (neu or jetzt - h.get("gemeldet", 0.0) >= REMIND_S) \
|
||||||
and not ist_stumm(zustaende.get(b.id), b.stufe, jetzt):
|
and not ist_stumm(zustaende.get(b.id), b.stufe, jetzt):
|
||||||
vorsatz = "" if neu else "Immer noch: "
|
vorsatz = "" if neu else "Immer noch: "
|
||||||
@@ -763,7 +765,7 @@ def takt() -> None:
|
|||||||
continue
|
continue
|
||||||
h = hinweise.pop(hid)
|
h = hinweise.pop(hid)
|
||||||
h.pop("fehlt", None)
|
h.pop("fehlt", None)
|
||||||
_verlauf("erledigt", hid, h.get("titel", hid), h.get("stufe"))
|
_verlauf("erledigt", hid, h.get("titel", hid), h.get("stufe"), h.get("quelle"))
|
||||||
if h.get("stufe") == "rot" and h.get("gemeldet"):
|
if h.get("stufe") == "rot" and h.get("gemeldet"):
|
||||||
_telegram(BETREFF_OK, f"Erledigt: {h.get('titel', hid)}.")
|
_telegram(BETREFF_OK, f"Erledigt: {h.get('titel', hid)}.")
|
||||||
for kid in list(kandidaten):
|
for kid in list(kandidaten):
|
||||||
|
|||||||
@@ -168,8 +168,8 @@ def test_alle_quellen_mit_sprechenden_titeln(geschichte):
|
|||||||
assert lang.startswith("…\nZeile ") and len(lang) <= protokoll.DETAILS_MAX and lang.endswith("Zeile 199: " + "x" * 90)
|
assert lang.startswith("…\nZeile ") and len(lang) <= protokoll.DETAILS_MAX and lang.endswith("Zeile 199: " + "x" * 90)
|
||||||
|
|
||||||
# Update-Läufe mit ihren Schritten
|
# Update-Läufe mit ihren Schritten
|
||||||
assert e["Update eingespielt · Gitea 1.27.3"]["stufe"] == "ok"
|
assert e["Update eingespielt · Gitea 1.27.2 → 1.27.3"]["stufe"] == "ok"
|
||||||
assert e["Update eingespielt · Gitea 1.27.3"]["details"] == "snapshot: ok\nupdate: ok"
|
assert e["Update eingespielt · Gitea 1.27.2 → 1.27.3"]["details"] == "snapshot: ok\nupdate: ok"
|
||||||
assert e["Paket-Update zurückgerollt · Proxmox Backup Server"]["stufe"] == "warn"
|
assert e["Paket-Update zurückgerollt · Proxmox Backup Server"]["stufe"] == "warn"
|
||||||
assert e["Update nicht begonnen · NetBird"]["stufe"] == "warn"
|
assert e["Update nicht begonnen · NetBird"]["stufe"] == "warn"
|
||||||
host_lauf = e["5 Host-Pakete eingespielt · Proxmox-Host"]
|
host_lauf = e["5 Host-Pakete eingespielt · Proxmox-Host"]
|
||||||
@@ -333,3 +333,19 @@ def test_pflege_merkt_sich_anstoss_fehlschlag_und_erholung(daten, monkeypatch):
|
|||||||
assert titel == Counter({"Wöchentliche Suche angestoßen · AdGuard Home": 2,
|
assert titel == Counter({"Wöchentliche Suche angestoßen · AdGuard Home": 2,
|
||||||
"Wöchentliche Suche gescheitert · AdGuard Home": 1,
|
"Wöchentliche Suche gescheitert · AdGuard Home": 1,
|
||||||
"Wöchentliche Suche klappt wieder · AdGuard Home": 1})
|
"Wöchentliche Suche klappt wieder · AdGuard Home": 1})
|
||||||
|
|
||||||
|
|
||||||
|
def test_zeitleiste_je_geraet(geschichte):
|
||||||
|
"""Seit 25.09.2026: nur die Einträge eines Geräts — Updates von → nach, Aufträge, Hinweise."""
|
||||||
|
gitea = protokoll.eintraege(ziel="ct-104")
|
||||||
|
assert gitea and all(e["ziel"] == "ct-104" for e in gitea)
|
||||||
|
assert "Update eingespielt · Gitea 1.27.2 → 1.27.3" in {e["titel"] for e in gitea}
|
||||||
|
assert protokoll.eintraege(ziel="vm-999") == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_hinweise_finden_ihr_geraet_ueber_die_quelle():
|
||||||
|
assert protokoll._ziel_aus("zertifikat:npm", "ct-101") == "ct-101"
|
||||||
|
assert protokoll._ziel_aus("docker:rippy-api-1", "vm-106") == "vm-106"
|
||||||
|
assert protokoll._ziel_aus("gast-platte:106", None) == "ct-106" # alte Einträge ohne Quelle
|
||||||
|
assert protokoll._ziel_aus("sicherung:probe", "pve") == "pve"
|
||||||
|
assert protokoll._ziel_aus("platte", "platte") is None
|
||||||
|
|||||||
@@ -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">
|
<Link to="/homelab/monitoring" hash={z.id} className="text-[15px] font-semibold break-words hover:text-lab-text">
|
||||||
{z.name}
|
{z.name}
|
||||||
</Link>
|
</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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span role="cell" className={cn("flex items-center gap-1.5 text-[13px]", z.erreichbar === false ? "text-rot-text" : "text-text-3")}>
|
<span role="cell" className={cn("flex items-center gap-1.5 text-[13px]", z.erreichbar === false ? "text-rot-text" : "text-text-3")}>
|
||||||
|
|||||||
@@ -479,10 +479,13 @@ export const useSammellaufQuittieren = () =>
|
|||||||
neuLaden: ["homelab-sammellauf"],
|
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({
|
useQuery({
|
||||||
queryKey: ["homelab-protokoll", berichte],
|
queryKey: ["homelab-protokoll", berichte, ziel],
|
||||||
queryFn: () => api<{ eintraege: ProtokollEintrag[] }>(`/api/homelab/protokoll?grenze=300&berichte=${berichte ? 1 : 0}`),
|
queryFn: () => api<{ eintraege: ProtokollEintrag[] }>(
|
||||||
|
`/api/homelab/protokoll?grenze=300&berichte=${berichte ? 1 : 0}${ziel ? `&ziel=${encodeURIComponent(ziel)}` : ""}`,
|
||||||
|
),
|
||||||
enabled: aktiv,
|
enabled: aktiv,
|
||||||
refetchInterval: takt(30_000),
|
refetchInterval: takt(30_000),
|
||||||
retry: false,
|
retry: false,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from "react"
|
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 { Bell, ChevronRight, Cog, History, RefreshCw, Search, TriangleAlert, type LucideIcon } from "lucide-react"
|
||||||
import { cn } from "cn"
|
import { cn } from "cn"
|
||||||
import { Zustandsfeld } from "@/components/cockpit/Chip"
|
import { Zustandsfeld } from "@/components/cockpit/Chip"
|
||||||
@@ -7,7 +8,9 @@ import { useHomelab, useHomelabProtokoll, useInstanz, usePartner } from "@/lib/a
|
|||||||
import type { ProtokollEintrag } from "@/lib/typen"
|
import type { ProtokollEintrag } from "@/lib/typen"
|
||||||
|
|
||||||
// Was der Homelab-Teil getan hat: Aufträge an den Ausführer (Snapshot, Update, Suchen …), Update-Läufe,
|
// 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"]
|
type Art = ProtokollEintrag["art"]
|
||||||
|
|
||||||
@@ -55,8 +58,10 @@ function Eintrag({ e, geraet }: { e: ProtokollEintrag; geraet: string | null })
|
|||||||
return (
|
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">
|
<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="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">
|
{/* Die Linie verbindet die Punkte eines Tages zur Zeitleiste. */}
|
||||||
<span aria-hidden className={cn("size-2.5 rounded-full", s.punkt)} />
|
<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>
|
</span>
|
||||||
<div className="flex min-w-0 flex-col gap-0.5">
|
<div className="flex min-w-0 flex-col gap-0.5">
|
||||||
<span className="flex flex-wrap items-center gap-x-2 text-[15px]">
|
<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 aktiv = instanz.data?.rolle === "homelab" || !!(partner.data?.eingerichtet && partner.data.erreichbar)
|
||||||
const [art, setArt] = useState<Art | "alle">("alle")
|
const [art, setArt] = useState<Art | "alle">("alle")
|
||||||
const [berichte, setBerichte] = useState(false)
|
const [berichte, setBerichte] = useState(false)
|
||||||
const protokoll = useHomelabProtokoll(aktiv, berichte)
|
const navigate = useNavigate()
|
||||||
const namen = new Map((useHomelab(aktiv).data?.ziele ?? []).map((z) => [z.id, z.name]))
|
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 (instanz.isPending || partner.isPending) return <Zustandsfeld text="Wird gelesen …" />
|
||||||
if (!aktiv) return <Zustandsfeld fehler text="Der Homelab-Teil antwortet gerade nicht." />
|
if (!aktiv) return <Zustandsfeld fehler text="Der Homelab-Teil antwortet gerade nicht." />
|
||||||
@@ -108,11 +117,21 @@ export function HomelabProtokollSeite() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel titel="Protokoll">
|
<Panel titel={geraet ? `Zeitleiste · ${namen.get(geraet) ?? geraet}` : "Protokoll"}>
|
||||||
<p className="-mt-2 text-[15px] text-text-2">
|
<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,
|
{geraet
|
||||||
Hinweise, gesendete Meldungen und die nächtliche Paketsuche.
|
? "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>
|
</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">
|
<div className="flex flex-wrap items-center gap-2" role="group" aria-label="Filter">
|
||||||
{ARTEN.map((a) => (
|
{ARTEN.map((a) => (
|
||||||
<button key={a.id} type="button" onClick={() => setArt(a.id)} aria-pressed={art === a.id}
|
<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">
|
<ol className="m-0 flex list-none flex-col p-0">
|
||||||
{t.eintraege.map((e, i) => {
|
{t.eintraege.map((e, i) => {
|
||||||
// Das Gerät als Schild — außer, der Titel nennt es schon.
|
// 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} />
|
return <Eintrag key={`${e.zeit}-${e.titel}-${i}`} e={e} geraet={name && !e.titel.includes(name) ? name : null} />
|
||||||
})}
|
})}
|
||||||
</ol>
|
</ol>
|
||||||
|
|||||||
Reference in New Issue
Block a user