Später · 18 Plattengesundheit: Proxmox-Host, NAS über SNMP, NVMe der KI-Box
- kern/platten.py: SMART aus pvesh, smartctl und SNMP in einer Form; rot bei SMART FAILED, kritischer Warnung, aufgebrauchter Reserve, ≥ 90 % verbraucht; gelb bei ≥ 80 %, Hitze, gewachsenen Fehlerzählern (Ausgangslage je Platte, „Zur Kenntnis genommen“ setzt sie neu) - Ausführer: stündlich disks/list + disks/smart im Bericht (nur lesend) - services/homelab/nas_snmp.py: kleiner SNMPv2c-GetBulk-Leser ohne neue Abhängigkeit, QNAP hdTable/diskTable, Zugang 0600, Einstellungen „Verbindung testen und speichern“ - services/platten.py: NVMe der KI-Box über sudo smartctl (smartmontools am 26.09. nachinstalliert) - Wächter beider Rollen, Plattenfeld in beiden Cockpits, Doku Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5325544d89
commit
904598031a
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1 +1 @@
|
||||
import{i as e,r as t}from"./dist-aE7UmubC.js";import{G as n,K as r}from"./index-B_hvuxH0.js";import{a as i,i as a,n as o,r as s,t as c}from"./sheet-DG7u1mB-.js";var l=t(),u=e();function d(e){let t=(0,l.c)(8),{onSchliessen:d}=e,f;t[0]===d?f=t[1]:(f=e=>!e&&d(),t[0]=d,t[1]=f);let p;t[2]===Symbol.for(`react.memo_cache_sentinel`)?(p=(0,u.jsxs)(a,{className:`p-0`,children:[(0,u.jsx)(i,{asChild:!0,children:(0,u.jsx)(`div`,{children:(0,u.jsx)(n,{})})}),(0,u.jsx)(s,{className:`sr-only`,children:`Bereiche und Seiten`})]}),t[2]=p):p=t[2];let m;t[3]===d?m=t[4]:(m=(0,u.jsxs)(o,{side:`left`,className:`flex flex-col gap-7 border-white/[0.06] bg-[#0a0d14] px-4 pt-5 pb-[calc(1.25rem+env(safe-area-inset-bottom))] data-[side=left]:w-[300px]`,children:[p,(0,u.jsx)(r,{onGewaehlt:d})]}),t[3]=d,t[4]=m);let h;return t[5]!==f||t[6]!==m?(h=(0,u.jsx)(c,{open:!0,onOpenChange:f,children:m}),t[5]=f,t[6]=m,t[7]=h):h=t[7],h}export{d as MenueSchublade};
|
||||
import{i as e,r as t}from"./dist-aE7UmubC.js";import{G as n,K as r}from"./index-CuG_RlEw.js";import{a as i,i as a,n as o,r as s,t as c}from"./sheet-DG7u1mB-.js";var l=t(),u=e();function d(e){let t=(0,l.c)(8),{onSchliessen:d}=e,f;t[0]===d?f=t[1]:(f=e=>!e&&d(),t[0]=d,t[1]=f);let p;t[2]===Symbol.for(`react.memo_cache_sentinel`)?(p=(0,u.jsxs)(a,{className:`p-0`,children:[(0,u.jsx)(i,{asChild:!0,children:(0,u.jsx)(`div`,{children:(0,u.jsx)(n,{})})}),(0,u.jsx)(s,{className:`sr-only`,children:`Bereiche und Seiten`})]}),t[2]=p):p=t[2];let m;t[3]===d?m=t[4]:(m=(0,u.jsxs)(o,{side:`left`,className:`flex flex-col gap-7 border-white/[0.06] bg-[#0a0d14] px-4 pt-5 pb-[calc(1.25rem+env(safe-area-inset-bottom))] data-[side=left]:w-[300px]`,children:[p,(0,u.jsx)(r,{onGewaehlt:d})]}),t[3]=d,t[4]=m);let h;return t[5]!==f||t[6]!==m?(h=(0,u.jsx)(c,{open:!0,onOpenChange:f,children:m}),t[5]=f,t[6]=m,t[7]=h):h=t[7],h}export{d as MenueSchublade};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
-2
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
-11
File diff suppressed because one or more lines are too long
+11
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
-2
File diff suppressed because one or more lines are too long
Vendored
+3
-3
@@ -9,15 +9,15 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<title>Homelab Orchestrator</title>
|
||||
<script type="module" crossorigin src="/assets/index-B_hvuxH0.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-CuG_RlEw.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/dist-aE7UmubC.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/button-BHVKhtQ6.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/dist-CakcBaML.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/abfragen-pD_qBEtc.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/abfragen-CDCgpAto.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/useSelector-DBNxxw17.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/useMatch-DDVpPKJI.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/useRouterState-DJr4hrdj.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Cz-7Ak8T.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CyAQuGX0.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>␍
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import type { Platte } from "@/lib/typen"
|
||||
import { Plattenliste } from "./Plattenliste"
|
||||
|
||||
// Plattengesundheit (Ausbauplan „Später“, Punkt 18, 26.09.2026): die echte Host-NVMe und eine kränkelnde NAS-Platte.
|
||||
const platte = (mehr: Partial<Platte>): Platte => ({
|
||||
id: "pve:1", name: "PELADN 512GB", wo: "Proxmox-Host /dev/nvme0n1", typ: "nvme", groesse: 512110190592,
|
||||
gesund: true, verbraucht: 2, temperatur: 41, stunden: 9992, fehler: { medien: 0 }, stufe: "ok", saetze: [], ...mehr,
|
||||
})
|
||||
|
||||
describe("Plattenliste", () => {
|
||||
it("gesunde Platte: Zustand, Verbrauch, Wärme, Laufzeit", () => {
|
||||
render(<Plattenliste platten={[platte({})]} />)
|
||||
expect(screen.getByText("Proxmox-Host /dev/nvme0n1 · NVMe · 512 GB")).toBeInTheDocument()
|
||||
expect(screen.getByText(/meldet sich gesund/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/2 % verbraucht · 41 °C · läuft seit 1,1 Jahren/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("was schlechter wird, steht als Satz in der Zeile", () => {
|
||||
render(<Plattenliste platten={[platte({ id: "nas:2", name: "ST4000VN008", wo: "NAS HDD2", typ: "hdd", groesse: null,
|
||||
gesund: false, verbraucht: null, stunden: null, stufe: "rot", saetze: ["meldet sich selbst als defekt (SMART)"] })]} />)
|
||||
expect(screen.getByText(/meldet sich selbst als defekt \(SMART\)/)).toHaveClass("text-rot-text")
|
||||
expect(screen.getByText("NAS HDD2 · Festplatte")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("ohne Werte ein Satz", () => {
|
||||
render(<Plattenliste platten={[]} />)
|
||||
expect(screen.getByText("Noch keine Werte.")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,54 @@
|
||||
import { cn } from "cn"
|
||||
import type { Platte } from "@/lib/typen"
|
||||
|
||||
// Plattengesundheit (Ausbauplan „Später“, Punkt 18, seit 26.09.2026): je Platte, ob sie sich gesund meldet, wie viel
|
||||
// verbraucht ist, wie warm sie ist und seit wann sie läuft. Gemeldet wird nur, was schlecht ist oder schlechter wird
|
||||
// (backend/kern/platten.py) — dann steht der Satz dazu in der Zeile.
|
||||
|
||||
const PUNKT = { ok: "bg-gruen", gelb: "bg-bernstein", rot: "bg-rot shadow-[0_0_8px_var(--rot)]" } as const
|
||||
const TEXT = { ok: "text-text-3", gelb: "text-bernstein", rot: "text-rot-text" } as const
|
||||
const TYP: Record<string, string> = { nvme: "NVMe", ssd: "SSD", hdd: "Festplatte" }
|
||||
|
||||
function groesse(bytes: number | null): string | null {
|
||||
if (!bytes) return null
|
||||
const tb = bytes / 1e12
|
||||
return tb >= 1 ? `${tb.toLocaleString("de-DE", { maximumFractionDigits: 1 })} TB` : `${Math.round(bytes / 1e9)} GB`
|
||||
}
|
||||
|
||||
function laufzeit(stunden: number | null): string | null {
|
||||
if (!stunden) return null
|
||||
const jahre = stunden / 8766
|
||||
return jahre >= 1 ? `läuft seit ${jahre.toLocaleString("de-DE", { maximumFractionDigits: 1 })} Jahren`
|
||||
: `läuft seit ${Math.round(stunden / 24)} Tagen`
|
||||
}
|
||||
|
||||
export function Plattenliste({ platten }: { platten: Platte[] }) {
|
||||
if (platten.length === 0) return <p className="text-sm text-text-3">Noch keine Werte.</p>
|
||||
return (
|
||||
<ul className="m-0 flex list-none flex-col p-0">
|
||||
{platten.map((p) => {
|
||||
const gesund = p.gesund === true ? "meldet sich gesund" : p.gesund === false ? "meldet sich krank" : "Zustand unbekannt"
|
||||
const werte = [
|
||||
p.verbraucht != null ? `${p.verbraucht} % verbraucht` : null,
|
||||
p.temperatur != null ? `${p.temperatur} °C` : null,
|
||||
laufzeit(p.stunden),
|
||||
].filter(Boolean)
|
||||
return (
|
||||
<li key={p.id} className="flex items-start gap-3 border-t border-white/[0.06] py-3 first:border-t-0">
|
||||
<span aria-hidden className={cn("mt-1.5 size-2 shrink-0 rounded-full", PUNKT[p.stufe])} />
|
||||
<span className="flex min-w-0 flex-col gap-0.5">
|
||||
<span className="text-[15px] font-semibold break-words">{p.name}</span>
|
||||
<span className="text-[13px] text-text-3">
|
||||
{[p.wo, TYP[p.typ] ?? null, groesse(p.groesse)].filter(Boolean).join(" · ")}
|
||||
</span>
|
||||
<span className={cn("text-sm", p.stufe === "ok" ? "text-text-2" : TEXT[p.stufe])}>
|
||||
{p.saetze.length ? p.saetze.join("; ") : gesund}
|
||||
{werte.length ? <span className="text-text-3"> · {werte.join(" · ")}</span> : null}
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import { stromVerbunden } from "./strom"
|
||||
import type {
|
||||
Ablauf, AktionsErgebnis, Altrest, AufraeumKandidat, BoxMesswerte, EinstellungenStand, Hinweis, HomelabEinstellungen,
|
||||
HomelabLauf, HomelabMesswerte, HomelabSpeicher, HomelabStand, HomelabZertifikate, NeuesGeraet, NeuerStack, NeuWahl, DockerLage, Neuerungen, HomelabWartung, WartungsRegel,
|
||||
LucyAutonomie, LucyStufe, Rueckfragen, Wochenbericht, Instanz, InstanzMitStand, Job, Modell, Nutzung, PartnerStatus,
|
||||
LucyAutonomie, LucyStufe, Rueckfragen, Wochenbericht, Platte, HomelabPlatten, Instanz, InstanzMitStand, Job, Modell, Nutzung, PartnerStatus,
|
||||
ProtokollEintrag, Radar, SammelPlanSchritt, Sammellauf, Sicherung, Start, TelegramTest, UpdateDetails, ZielAktion,
|
||||
ZielStand, Zeitraum,
|
||||
} from "./typen"
|
||||
@@ -512,6 +512,32 @@ export const useNeuerungen = (zid: string, baustein: string, aktiv: boolean) =>
|
||||
retry: false,
|
||||
})
|
||||
|
||||
/** Plattengesundheit (seit 26.09.2026): die KI-Box selbst und das Homelab (Proxmox-Host, NAS über SNMP). */
|
||||
export const useBoxPlatten = () =>
|
||||
useQuery({ queryKey: ["platten"], queryFn: () => api<{ platten: Platte[] }>("/api/platten"), refetchInterval: 600_000, retry: false })
|
||||
|
||||
export const useHomelabPlatten = (aktiv: boolean) =>
|
||||
useQuery({
|
||||
queryKey: ["homelab-platten"],
|
||||
queryFn: () => api<HomelabPlatten>("/api/homelab/platten"),
|
||||
enabled: aktiv,
|
||||
refetchInterval: 600_000,
|
||||
retry: false,
|
||||
})
|
||||
|
||||
/** SNMP-Zugang des NAS: speichern (erst prüft der Homelab-Teil das NAS damit) oder mit null entfernen. */
|
||||
export const useNasSnmp = () =>
|
||||
useAktion(
|
||||
(zugang: { community: string; adresse: string } | null) =>
|
||||
zugang === null
|
||||
? api<AktionsErgebnis>("/api/homelab/einstellungen/nas-snmp", { method: "DELETE" })
|
||||
: post<AktionsErgebnis>("/api/homelab/einstellungen/nas-snmp", zugang),
|
||||
{
|
||||
erfolg: (v, e) => (e as AktionsErgebnis).text ?? (v === null ? "SNMP-Zugang entfernt." : "SNMP-Zugang gespeichert."),
|
||||
neuLaden: ["homelab-einstellungen", "homelab-platten"],
|
||||
},
|
||||
)
|
||||
|
||||
/** Lucys Freiheiten je Aktionsart (seit 25.09.2026, Einstellungen → Lucy). */
|
||||
export const useLucyAutonomie = (aktiv = true) =>
|
||||
useQuery({
|
||||
|
||||
@@ -555,6 +555,8 @@ export interface HomelabEinstellungen {
|
||||
}
|
||||
/** Zugang für die Statistik von AdGuard („% geblockt“ auf der Kachel, seit 25.09.2026). */
|
||||
adguard?: { url: string; zugang: "hinterlegt" | "fehlt"; benutzer: string | null }
|
||||
/** Seit 26.09.2026: SNMP-Zugang des NAS (nie der Zugangsname selbst). */
|
||||
nas_snmp?: { zugang: "hinterlegt" | "fehlt"; adresse: string | null }
|
||||
karenz_h: number
|
||||
ausfuehrer: { verbunden: boolean; zuletzt: number | null; nur_lesen: boolean; sicherung_speicher: string | null }
|
||||
}
|
||||
@@ -891,3 +893,25 @@ export interface Wochenbericht {
|
||||
zuletzt: { zeit: number; text: string } | null
|
||||
naechster: number
|
||||
}
|
||||
|
||||
// --- Plattengesundheit (GET /api/platten, /api/homelab/platten; seit 26.09.2026) -----------------------------------
|
||||
|
||||
export interface Platte {
|
||||
id: string
|
||||
name: string
|
||||
wo: string
|
||||
typ: string
|
||||
groesse: number | null
|
||||
gesund: boolean | null
|
||||
verbraucht: number | null
|
||||
temperatur: number | null
|
||||
stunden: number | null
|
||||
fehler: Record<string, number>
|
||||
stufe: "ok" | "gelb" | "rot"
|
||||
saetze: string[]
|
||||
}
|
||||
|
||||
export interface HomelabPlatten {
|
||||
platten: Platte[]
|
||||
nas: { eingerichtet: boolean; ok: boolean; detail: string | null; system: string | null }
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Freiheiten, WochenberichtAbschnitt } from "@/components/lucy/LucyEinste
|
||||
import { OffeneRueckfragen, RueckfragenVerlauf } from "@/components/lucy/Rueckfragen"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
useAdguardZugang, useArcaneEcht, useArcaneSchluessel, useEinstellungenStand, useHomelabEinstellungen, useInstanz, usePartner,
|
||||
useAdguardZugang, useArcaneEcht, useNasSnmp, useArcaneSchluessel, useEinstellungenStand, useHomelabEinstellungen, useInstanz, usePartner,
|
||||
useRadarSchalten, useSoftwareStand, useTelegramTest, useUpdateFensterSetzen,
|
||||
} from "@/lib/abfragen"
|
||||
import { api, post } from "@/lib/api"
|
||||
@@ -584,6 +584,65 @@ function AdguardZugang({ h }: { h: HomelabEinstellungen }) {
|
||||
)
|
||||
}
|
||||
|
||||
/** NAS-Platten über SNMP (seit 26.09.2026): der Zugangsname („Community“), vorher fragt der Homelab-Teil das NAS. */
|
||||
function NasSnmp({ h }: { h: HomelabEinstellungen }) {
|
||||
const speichern = useNasSnmp()
|
||||
const [community, setCommunity] = useState("")
|
||||
const [adresse, setAdresse] = useState("")
|
||||
const [loeschFrage, setLoeschFrage] = useState(false)
|
||||
const n = h.nas_snmp
|
||||
if (!n) return null
|
||||
const feld = "h-11 w-full rounded-xl border border-white/12 bg-white/[0.04] px-3 text-[15px] outline-none focus:border-lab/60 focus:bg-white/[0.06]"
|
||||
return (
|
||||
<Abschnitt titel="NAS-Platten (SNMP)">
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm text-text-2">
|
||||
Zugang {n.zugang === "hinterlegt" ? <Chip art="gruen">hinterlegt</Chip> : <Chip art="grau">keiner</Chip>}
|
||||
{n.adresse && <span className="text-text-3">NAS {n.adresse}</span>}
|
||||
</div>
|
||||
<p className="text-sm text-text-2">
|
||||
Damit meldet der Orchestrator, wenn eine Platte im NAS kränkelt. Am QNAP: Systemsteuerung → Netzwerk- und
|
||||
Dateidienste → SNMP → „SNMP-Dienst aktivieren“, Version SNMP v1/v2, einen Zugangsnamen („Community“) wählen, der
|
||||
nicht „public“ heißt — und ihn hier eintragen.
|
||||
</p>
|
||||
<form
|
||||
className="flex flex-wrap gap-2"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
if (community.trim()) {
|
||||
speichern.mutate({ community: community.trim(), adresse: adresse.trim() },
|
||||
{ onSuccess: () => { setCommunity(""); setAdresse("") } })
|
||||
}
|
||||
}}
|
||||
>
|
||||
<label htmlFor="nas-community" className="sr-only">Zugangsname (Community)</label>
|
||||
<input id="nas-community" type="password" autoComplete="new-password" value={community}
|
||||
onChange={(e) => setCommunity(e.target.value)} placeholder="Zugangsname (Community)" className={cn(feld, "min-w-0 flex-1 basis-48")} />
|
||||
<label htmlFor="nas-adresse" className="sr-only">Adresse des NAS</label>
|
||||
<input id="nas-adresse" autoComplete="off" value={adresse} onChange={(e) => setAdresse(e.target.value)}
|
||||
placeholder={n.adresse ? `Adresse (${n.adresse})` : "Adresse des NAS"} className={cn(feld, "min-w-0 flex-1 basis-40")} />
|
||||
<Button type="submit" disabled={!community.trim() || speichern.isPending}>
|
||||
{speichern.isPending ? "Prüft …" : "Verbindung testen und speichern"}
|
||||
</Button>
|
||||
</form>
|
||||
<p className="text-xs text-text-3">
|
||||
Vor dem Speichern fragt der Homelab-Teil das NAS damit. Antwortet es nicht, wird nichts gespeichert. Der
|
||||
Zugangsname wird nie wieder angezeigt; die Adresse nur ändern, wenn sie von der angezeigten abweicht.
|
||||
</p>
|
||||
{n.zugang === "hinterlegt" && (
|
||||
loeschFrage ? (
|
||||
<span className="flex flex-wrap gap-2">
|
||||
<Button variant="gefahr" size="sm" disabled={speichern.isPending}
|
||||
onClick={() => { speichern.mutate(null); setLoeschFrage(false) }}>Wirklich entfernen?</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => setLoeschFrage(false)}>Nein</Button>
|
||||
</span>
|
||||
) : (
|
||||
<Button variant="ghost" size="sm" className="self-start" onClick={() => setLoeschFrage(true)}>Zugang entfernen</Button>
|
||||
)
|
||||
)}
|
||||
</Abschnitt>
|
||||
)
|
||||
}
|
||||
|
||||
function HomelabBetrieb({ h }: { h: HomelabEinstellungen }) {
|
||||
const f = h.ausfuehrer
|
||||
return (
|
||||
@@ -620,6 +679,7 @@ function HomelabKarte() {
|
||||
<>
|
||||
<ArcaneSchluessel h={h.data} />
|
||||
<AdguardZugang h={h.data} />
|
||||
<NasSnmp h={h.data} />
|
||||
<Abschnitt titel="Wartungsfenster und Regel je Gerät"><Wartungsfenster aktiv={aktiv} /></Abschnitt>
|
||||
<HomelabBetrieb h={h.data} />
|
||||
</>
|
||||
|
||||
@@ -11,9 +11,10 @@ import { Speicher } from "@/components/homelab/Speicher"
|
||||
import { AlleKnopf, SammellaufFortschritt } from "@/components/homelab/Sammellauf"
|
||||
import { UpdateTabelle } from "@/components/homelab/UpdateTabelle"
|
||||
import { Zertifikate } from "@/components/homelab/Zertifikate"
|
||||
import { Plattenliste } from "@/components/cockpit/Plattenliste"
|
||||
import {
|
||||
useHomelab, useHomelabDocker, useHomelabHinweise, useHomelabLaeufe, useHomelabMesswerte, useHomelabSpeicher,
|
||||
useHomelabZertifikate,
|
||||
useHomelabPlatten, useHomelabZertifikate,
|
||||
useInstanz, useNeuEntdeckt, useNeuUebernehmen, usePartner, useSammellauf, useSammellaufQuittieren, useSicherungsProbe,
|
||||
useZielAktion,
|
||||
} from "@/lib/abfragen"
|
||||
@@ -198,6 +199,7 @@ export function HomelabSeite() {
|
||||
const mess = useHomelabMesswerte("1h", !!stand)
|
||||
const speicher = useHomelabSpeicher(!!stand)
|
||||
const zertifikate = useHomelabZertifikate(!!stand)
|
||||
const platten = useHomelabPlatten(!!stand)
|
||||
const probe = useSicherungsProbe()
|
||||
const neu = useNeuEntdeckt(!!stand)
|
||||
const docker = useHomelabDocker(!!stand)
|
||||
@@ -267,6 +269,20 @@ export function HomelabSeite() {
|
||||
/>
|
||||
</Panel>
|
||||
)}
|
||||
{platten.data && (platten.data.platten.length > 0 || platten.data.nas.eingerichtet) && (
|
||||
<Panel titel="Plattengesundheit" id="platten">
|
||||
<Plattenliste platten={platten.data.platten} />
|
||||
{platten.data.nas.eingerichtet && !platten.data.nas.ok && platten.data.nas.detail && (
|
||||
<p className="text-sm text-bernstein">{platten.data.nas.detail}</p>
|
||||
)}
|
||||
{!platten.data.nas.eingerichtet && (
|
||||
<p className="text-xs text-text-3">
|
||||
Die Platten des NAS erscheinen, sobald am QNAP SNMP an ist und der Zugangsname unter Einstellungen → Homelab
|
||||
steht.
|
||||
</p>
|
||||
)}
|
||||
</Panel>
|
||||
)}
|
||||
{zertifikate.data?.gelesen && zertifikate.data.zertifikate.length > 0 && (
|
||||
<Panel titel="Zertifikate" id="zertifikate">
|
||||
<Zertifikate daten={zertifikate.data} />
|
||||
|
||||
@@ -5,10 +5,11 @@ import { Chip, Zustandsfeld } from "@/components/cockpit/Chip"
|
||||
import { Flugplan } from "@/components/cockpit/Flugplan"
|
||||
import { Instrument } from "@/components/cockpit/Instrument"
|
||||
import { Panel } from "@/components/cockpit/Panel"
|
||||
import { Plattenliste } from "@/components/cockpit/Plattenliste"
|
||||
import { Warnpanel } from "@/components/cockpit/Warnpanel"
|
||||
import { Zaehlwerk } from "@/components/cockpit/Zaehlwerk"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useRadar, useStart } from "@/lib/abfragen"
|
||||
import { useBoxPlatten, useRadar, useStart } from "@/lib/abfragen"
|
||||
import { RADAR_STATUS_TEXT, temperaturFarbe } from "@/lib/anzeige"
|
||||
import { knappeVorhersage, STUFE_FARBE } from "@/lib/prognose"
|
||||
import { useLetzteMetrik } from "@/lib/strom"
|
||||
@@ -94,6 +95,7 @@ function RadarKasten({ radar }: { radar: Radar | undefined }) {
|
||||
export function StartSeite() {
|
||||
const { data: start, error, isPending } = useStart()
|
||||
const radar = useRadar()
|
||||
const platten = useBoxPlatten()
|
||||
|
||||
if (isPending) return <Zustandsfeld text="Die Box meldet sich gleich …" />
|
||||
// Scheitert nur ein späteres Nachladen, bleibt der letzte Stand stehen — die Verbindungsanzeige
|
||||
@@ -122,6 +124,11 @@ export function StartSeite() {
|
||||
<RadarKasten radar={radar.data} />
|
||||
</Panel>
|
||||
</div>
|
||||
{platten.data && platten.data.platten.length > 0 && (
|
||||
<Panel titel="Plattengesundheit" id="platten">
|
||||
<Plattenliste platten={platten.data.platten} />
|
||||
</Panel>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user