feat(ui): Huelle mit Seitenleiste und Platzhalterseiten (#002)

Die Huelle bekommt Benutzer und optional den aktiven Pfad als Props und
kennt weder Sitzung noch Datenbank; ohne Pfad ermittelt die Navigation
ihn per usePathname(), weil ein Layout den Pfad nicht kennt. Die
Benutzerermittlung steckt in huellenBenutzerErmitteln(), damit #004 nur
diese Funktion ersetzen muss.

Unter 1024 px klappt die Leiste ein; geschlossen ist sie invisible und
damit auch nicht per Tab erreichbar. Alle Navigationsziele haben eine
Platzhalterseite, damit nichts auf eine 404 fuehrt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 09:20:30 +02:00
co-authored by Claude Opus 5.5
parent da0607522e
commit 419e8ff8bd
17 changed files with 473 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
import type { Metadata } from "next";
import { Platzhalterseite } from "@/components/huelle/Platzhalterseite";
export const metadata: Metadata = { title: "Einstellungen" };
export default function Einstellungen() {
return (
<Platzhalterseite
titel="Einstellungen"
untertitel="Deployment-Key, eigene Vorlagen, Benachrichtigungen"
phase={3}
issue="#019"
/>
);
}
+15
View File
@@ -0,0 +1,15 @@
import type { Metadata } from "next";
import { Platzhalterseite } from "@/components/huelle/Platzhalterseite";
export const metadata: Metadata = { title: "Hostgruppen" };
export default function Hostgruppen() {
return (
<Platzhalterseite
titel="Hostgruppen"
untertitel="Hosts bündeln, um Schlüssel gemeinsam zuzuweisen"
phase={1}
issue="#008"
/>
);
}
+15
View File
@@ -0,0 +1,15 @@
import type { Metadata } from "next";
import { Platzhalterseite } from "@/components/huelle/Platzhalterseite";
export const metadata: Metadata = { title: "Hosts" };
export default function Hosts() {
return (
<Platzhalterseite
titel="Hosts"
untertitel="Verwaltete Server, Host-Keys und Erreichbarkeit"
phase={1}
issue="#008"
/>
);
}
+13
View File
@@ -0,0 +1,13 @@
import type { HuellenBenutzer } from "@/components/huelle/typen";
/**
* Liefert den Benutzer, den die Seitenleiste anzeigt.
*
* TODO (#004): durch die echte Sitzung ersetzen (Benutzer + Rolle aus der Datenbank,
* Herkunft „authentik“ oder „Notfall-Admin“) und ohne Sitzung zur Anmeldung umleiten.
* Bis dahin ein fester Beispielbenutzer, damit der Prototyp ohne Anmeldung läuft.
* async schon jetzt, damit sich die Aufrufer beim Umstieg nicht ändern müssen.
*/
export async function huellenBenutzerErmitteln(): Promise<HuellenBenutzer> {
return { name: "Florian Wilde", rolle: "ADMIN", herkunft: "authentik" };
}
+9
View File
@@ -0,0 +1,9 @@
import type { ReactNode } from "react";
import { Huelle } from "@/components/huelle/Huelle";
import { huellenBenutzerErmitteln } from "./huellen-benutzer";
/** Alle angemeldeten Seiten teilen sich die Hülle mit Seitenleiste (#002). */
export default async function AppLayout({ children }: { children: ReactNode }) {
const benutzer = await huellenBenutzerErmitteln();
return <Huelle benutzer={benutzer}>{children}</Huelle>;
}
+10
View File
@@ -0,0 +1,10 @@
import type { Metadata } from "next";
import { Platzhalterseite } from "@/components/huelle/Platzhalterseite";
export const metadata: Metadata = { title: "Personen" };
export default function Personen() {
return (
<Platzhalterseite titel="Personen" untertitel="Wem welche Schlüssel gehören" phase={2} issue="#013" />
);
}
+15
View File
@@ -0,0 +1,15 @@
import type { Metadata } from "next";
import { Platzhalterseite } from "@/components/huelle/Platzhalterseite";
export const metadata: Metadata = { title: "Audit-Log" };
export default function Protokoll() {
return (
<Platzhalterseite
titel="Audit-Log"
untertitel="Lückenloses, verkettetes Protokoll aller Aktionen"
phase={5}
issue="#027"
/>
);
}
+24
View File
@@ -0,0 +1,24 @@
import type { Metadata } from "next";
import Link from "next/link";
import { Platzhalterseite } from "@/components/huelle/Platzhalterseite";
export const metadata: Metadata = { title: "Schlüssel" };
export default function Schluessel() {
return (
<div className="flex flex-col gap-5">
<Platzhalterseite
titel="Schlüssel"
untertitel="SSH-Schlüssel erzeugen, importieren und ausrollen"
phase={2}
issue="#012"
/>
<p className="text-muted text-[14px]">
Prototyp ansehen:{" "}
<Link href="/schluessel/beispiel" className="text-accent font-medium hover:underline">
Schlüsseldetail mit Beispieldaten
</Link>
</p>
</div>
);
}
+15
View File
@@ -0,0 +1,15 @@
import type { Metadata } from "next";
import { Platzhalterseite } from "@/components/huelle/Platzhalterseite";
export const metadata: Metadata = { title: "Vorlagen" };
export default function Vorlagen() {
return (
<Platzhalterseite
titel="Vorlagen"
untertitel="Wiederkehrende Zugänge wie Deployment oder Backup"
phase={3}
issue="#019"
/>
);
}
+15
View File
@@ -0,0 +1,15 @@
import type { Metadata } from "next";
import { Platzhalterseite } from "@/components/huelle/Platzhalterseite";
export const metadata: Metadata = { title: "Zuweisungen" };
export default function Zuweisungen() {
return (
<Platzhalterseite
titel="Zuweisungen"
untertitel="Welcher Schlüssel auf welchem Host-Konto gilt"
phase={2}
issue="#016"
/>
);
}