Der Ausführer zählt je Gast, wie viele offene Updates aus einer Sicherheits-Suite kommen („-security“ in apt list), beim Proxmox-Host ebenso; für Alpine (NPMplus) schickt er die Paketliste, und der Homelab-Teil gleicht sie mit Alpines Sicherheitsdatenbank ab (im Hintergrund geladen, zwölf Stunden gemerkt, Alpine-Versionsvergleich). Die Update-Tabelle, die Kacheln und die Übersicht zeigen „· 3 Sicherheit“, die Rückfrage nennt es. Je Docker-Container steht, was Arcanes Lücken-Scan (Trivy, sonntags 02:00) im Image fand, kritische und hohe. Der Ausführer braucht ausfuehrer-einrichten.sh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
87 lines
4.0 KiB
TypeScript
87 lines
4.0 KiB
TypeScript
import { cn } from "cn"
|
|
import { Geraetesymbol } from "@/components/cockpit/Geraetesymbol"
|
|
import { geraetebild, type Punktzustand } from "@/lib/geraetebild"
|
|
import type { DockerContainer, DockerLage } from "@/lib/typen"
|
|
|
|
// Docker-Dienste unter Arcane einzeln (Ausbauplan Welle 2, Punkt 8, seit 25.09.2026): je Stack die Container mit
|
|
// Zustand, Gesundheit und Update-Stand — statt nur „Arcane (Docker) · 11 Images“.
|
|
|
|
const UPDATE: Record<DockerContainer["update"], { text: string; klasse: string }> = {
|
|
neu: { text: "Update verfügbar", klasse: "text-cyan-text" },
|
|
lokal: { text: "lokal gebaut", klasse: "text-text-3" },
|
|
aktuell: { text: "aktuell", klasse: "text-text-3" },
|
|
unbekannt: { text: "Update-Stand unklar", klasse: "text-bernstein" },
|
|
}
|
|
|
|
function punkt(c: DockerContainer): Punktzustand {
|
|
if (c.zustand === "restarting" || c.gesund === false) return "rot"
|
|
if (c.zustand === "running") return "ok"
|
|
return "aus"
|
|
}
|
|
|
|
function zustandText(c: DockerContainer): string {
|
|
if (c.gesund === false) return "ungesund"
|
|
if (c.zustand === "restarting") return "startet immer wieder neu"
|
|
if (c.zustand === "running") return c.gesund ? "läuft, gesund" : "läuft"
|
|
if (c.zustand === "exited") return "beendet"
|
|
if (c.zustand === "paused") return "angehalten"
|
|
return c.zustand
|
|
}
|
|
|
|
/** Aus „ghcr.io/getarcaneapp/manager:latest“ wird „manager“ — für das Logo. */
|
|
const imageName = (image: string | null) => (image ?? "").split("/").at(-1)?.split(":")[0] ?? ""
|
|
|
|
/** „2 kritische, 5 hohe Lücken“ aus Arcanes Scan (seit 25.09.2026) — nur, wenn es kritische oder hohe gibt. */
|
|
function lueckenText(c: DockerContainer): string | null {
|
|
const l = c.luecken
|
|
if (!l || (!l.kritisch && !l.hoch)) return null
|
|
const teile = [l.kritisch ? `${l.kritisch} kritische` : null, l.hoch ? `${l.hoch} hohe` : null].filter(Boolean)
|
|
return `${teile.join(", ")} Lücken`
|
|
}
|
|
|
|
function Zeile({ c }: { c: DockerContainer }) {
|
|
const u = UPDATE[c.update] ?? UPDATE.unbekannt
|
|
const luecken = lueckenText(c)
|
|
return (
|
|
<li className="flex min-w-0 items-center gap-3 border-t border-white/[0.06] py-2.5 first:border-t-0">
|
|
<Geraetesymbol bild={geraetebild({ art: "container", name: c.dienst ?? imageName(c.image) })} groesse="sm" punkt={punkt(c)} />
|
|
<span className="flex min-w-0 flex-1 flex-col">
|
|
<span className="truncate text-[14.5px] font-medium">{c.name}</span>
|
|
<span className="truncate text-[12.5px] text-text-3">{c.image}</span>
|
|
{luecken && (
|
|
<span className={cn("truncate text-[12.5px]", c.luecken?.kritisch ? "text-rot-text" : "text-bernstein")}>{luecken}</span>
|
|
)}
|
|
</span>
|
|
<span className="flex shrink-0 flex-col items-end text-right">
|
|
<span className={cn("text-[13px]", punkt(c) === "rot" ? "text-rot-text" : "text-text-2")}>{zustandText(c)}</span>
|
|
<span className={cn("text-[12px]", u.klasse)}>{u.text}</span>
|
|
</span>
|
|
</li>
|
|
)
|
|
}
|
|
|
|
export function DockerDienste({ lage }: { lage: DockerLage }) {
|
|
if (!lage.verfuegbar || lage.stacks.length === 0) return null
|
|
return (
|
|
<div className="grid gap-3 md:grid-cols-2 2xl:grid-cols-3">
|
|
{lage.stacks.map((s) => {
|
|
const laufen = s.container.filter((c) => c.zustand === "running").length
|
|
const neu = s.container.filter((c) => c.update === "neu").length
|
|
return (
|
|
<section key={s.name} aria-label={`Stack ${s.name}`} className="flaeche-innen flex min-w-0 flex-col rounded-2xl px-4 py-3">
|
|
<div className="flex items-baseline justify-between gap-3 pb-1">
|
|
<h3 className="m-0 truncate text-[15px] font-semibold">{s.name}</h3>
|
|
<span className="shrink-0 text-[12.5px] text-text-3">
|
|
{laufen} von {s.container.length} laufen{neu ? ` · ${neu} mit Update` : ""}
|
|
</span>
|
|
</div>
|
|
<ul className="m-0 list-none p-0">
|
|
{s.container.map((c) => <Zeile key={c.id} c={c} />)}
|
|
</ul>
|
|
</section>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|