Lange Zeilen unter „Letzte Deployments“ sprengten die Gitterspalte; der mobile Browser zoomte heraus, die Navigationsschublade verdeckte dann „Abmelden“. keinQuerscrollen misst jetzt gegen die eingestellte Breite – gegen innerWidth gemessen fiel das Herauszoomen nicht auf. Refs #023 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
113 lines
4.8 KiB
TypeScript
113 lines
4.8 KiB
TypeScript
import type { Metadata } from "next";
|
||
import Link from "next/link";
|
||
import { Karte } from "@/components/karte";
|
||
import { Status } from "@/components/status";
|
||
import { ART_TEXT, STATUS_ANZEIGE } from "@/lib/auftrag-text";
|
||
import { bytes, zeitpunkt } from "@/lib/formate";
|
||
import { seiteSchuetzen } from "@/server/auth/zugang";
|
||
import { auftraegeSuchen } from "@/server/services/auftraege";
|
||
import { serverListe } from "@/server/services/server";
|
||
import { ServerStatus } from "./server/server-status";
|
||
|
||
export const metadata: Metadata = { title: "Übersicht" };
|
||
|
||
/** Server mit Status und freien Ressourcen (aus dem letzten Verbindungstest), letzte Aufträge. */
|
||
export default async function Uebersicht() {
|
||
const benutzer = await seiteSchuetzen("lesen", "/");
|
||
const [server, { eintraege }] = await Promise.all([serverListe(), auftraegeSuchen({ seite: 1 })]);
|
||
const letzte = eintraege.slice(0, 8);
|
||
|
||
return (
|
||
<div className="flex flex-col gap-6">
|
||
<header>
|
||
<p className="text-muted text-sm">Willkommen, {benutzer.anzeigename}</p>
|
||
<h1 className="text-2xl font-semibold tracking-tight sm:text-3xl">Übersicht</h1>
|
||
</header>
|
||
|
||
{/* min-w-0: Gitterspalten schrumpfen sonst nicht unter lange Zeilen – am Handy zoomte der Browser dann heraus. */}
|
||
<div className="grid gap-6 lg:grid-cols-2 [&>*]:min-w-0">
|
||
<Karte
|
||
titel="Server"
|
||
aktion={
|
||
<Link href="/server" className="text-akzent text-sm">
|
||
Alle
|
||
</Link>
|
||
}
|
||
>
|
||
{server.length === 0 ? (
|
||
<Status art="offen">Noch kein Proxmox-Server angelegt.</Status>
|
||
) : (
|
||
<ul className="divide-linie flex flex-col divide-y">
|
||
{server.map((eintrag) => {
|
||
const knoten = eintrag.letzterTest?.knoten ?? [];
|
||
const online = knoten.filter((k) => k.online);
|
||
const frei = online.reduce(
|
||
(s, k) => s + ((k.speicherGesamt ?? 0) - (k.speicherBelegt ?? 0)),
|
||
0,
|
||
);
|
||
const cpus = online.reduce((s, k) => s + (k.cpus ?? 0), 0);
|
||
return (
|
||
<li key={eintrag.name} className="flex flex-col gap-1 py-2.5 first:pt-0 last:pb-0">
|
||
<div className="flex items-center justify-between gap-3">
|
||
<Link href={`/server/${eintrag.name}`} className="min-w-0 hover:underline">
|
||
<span className="font-medium">{eintrag.titel}</span>{" "}
|
||
<span className="text-muted font-mono text-sm">{eintrag.name}</span>
|
||
</Link>
|
||
<ServerStatus server={eintrag} />
|
||
</div>
|
||
{knoten.length > 0 ? (
|
||
<p className="text-muted text-xs">
|
||
{online.length} von {knoten.length} {knoten.length === 1 ? "Node" : "Nodes"} online
|
||
{online.length > 0 ? ` · ${cpus} CPUs · ${bytes(frei)} RAM frei` : ""}
|
||
{eintrag.letzterTest?.cluster ? ` · Cluster ${eintrag.letzterTest.cluster.name}` : ""}
|
||
</p>
|
||
) : null}
|
||
</li>
|
||
);
|
||
})}
|
||
</ul>
|
||
)}
|
||
</Karte>
|
||
<Karte
|
||
titel="Letzte Deployments"
|
||
aktion={
|
||
<Link href="/auftraege" className="text-akzent text-sm">
|
||
Verlauf
|
||
</Link>
|
||
}
|
||
>
|
||
{letzte.length === 0 ? (
|
||
<Status art="offen">Noch keine Deployments.</Status>
|
||
) : (
|
||
<ul className="divide-linie flex flex-col divide-y">
|
||
{letzte.map((a) => {
|
||
const [art, text] = STATUS_ANZEIGE[a.status];
|
||
return (
|
||
<li key={a.id}>
|
||
<Link
|
||
href={`/auftraege/${a.kurzId}`}
|
||
className="hover:bg-panel-hoch/60 -mx-2 flex items-center justify-between gap-3 rounded-lg px-2 py-2"
|
||
>
|
||
<span className="min-w-0 truncate">
|
||
<span className="font-medium">{a.name ?? ART_TEXT[a.art]}</span>
|
||
<span className="text-muted text-sm">
|
||
{" "}
|
||
{a.art !== "AUSROLLEN" && a.name ? `· ${ART_TEXT[a.art]} ` : ""}· {a.server.name}
|
||
{a.vmid ? ` · ${a.vmid}` : ""} · {zeitpunkt(a.erstelltAm)}
|
||
</span>
|
||
</span>
|
||
<Status art={art} className="shrink-0 text-sm">
|
||
{text}
|
||
</Status>
|
||
</Link>
|
||
</li>
|
||
);
|
||
})}
|
||
</ul>
|
||
)}
|
||
</Karte>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|