Workstream 1: Projekt-Fundament & Design-System (Phase 0)

Greenfield-Next.js-15-App-Router-Gerüst (TS strict) mit:
- Route-Groups (auth)/(app) inkl. loading/error/not-found je Group;
  Guard-Slot-Kommentar im (app)/layout.tsx (vom Auth-WS zu füllen).
- "Amtlich"/Netzknoten-Designsystem: Tailwind-Tokens (Navy #1B3A5B,
  Signalrot #E2231A, Anthrazit, Nebelgrau, bereit/Wartung), tabular-nums,
  Serif-Display/Inter-Sans via CSS-Variablen, Inline-SVG-Logo.
- Radix-Basiskomponenten (button/input/label/badge/tabs/dialog/select/
  switch/slider); StatusBadge entspricht asset_status.
- Kanonisches src/lib/env.ts (Zod, Fail-Fast) mit ALLEN DB-/Auth-/Geo-Slots
  inkl. AUTH_URL; isHttps-Ableitung. Zentrale i18n-Tabelle de.ts + t().
- Drizzle-Setup: client.ts (Pool-Singleton), leeres schema/index.ts-Barrel
  (KEIN Migrations-Eigentümer), drizzle.config.ts, .env.example.
- next.config.ts: output:standalone, experimental.authInterrupts,
  Security-Header. Vitest + Fail-Fast-Env-Test (TDD, 5/5 grün).

Bewusst KEINE Auth-Logik und KEINE fachlichen Tabellen.

Verifikation: typecheck/lint/test grün; npm run build erzeugt
.next/standalone/server.js; curl /anmelden -> lang="de" + FlorianNetz.
next/font/google durch CSS-Variablen ersetzt (air-gapped-Build).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthias Hochmeister
2026-06-08 16:57:01 +02:00
parent 6ebcd270ad
commit 4707844dbc
45 changed files with 10966 additions and 1 deletions

15
src/app/(app)/error.tsx Normal file
View File

@@ -0,0 +1,15 @@
"use client";
import { Button } from "@/components/ui/button";
import { t } from "@/lib/i18n/de";
export default function AppError({ reset }: { error: Error; reset: () => void }) {
return (
<div className="mx-auto max-w-md py-16 text-center">
<p className="text-anthrazit">{t("fehler.allgemein")}</p>
<Button onClick={reset} className="mt-4">
{t("aktion.erneutVersuchen")}
</Button>
</div>
);
}

21
src/app/(app)/layout.tsx Normal file
View File

@@ -0,0 +1,21 @@
import { AppShell } from "@/components/layout/app-shell";
// Gated App-Shell.
//
// GUARD-SLOT (Default-deny, dreifach — Querschnittsstandard 1):
// Der Auth-Workstream (Workstream 3) fügt hier als ALLERERSTE Anweisung den
// serverseitigen Session-Guard ein, z. B.:
//
// import { requireSession } from "@/lib/auth/guards";
// ...
// await requireSession(); // leitet anonyme Aufrufe auf /anmelden um
//
// Lese-Seiten dürfen sich NICHT allein auf die Middleware verlassen.
export default async function AppLayout({
children,
}: {
children: React.ReactNode;
}) {
// await requireSession(); // <- vom Auth-Workstream zu aktivieren
return <AppShell>{children}</AppShell>;
}

View File

@@ -0,0 +1,9 @@
import { t } from "@/lib/i18n/de";
export default function AppLoading() {
return (
<div className="py-16 text-center text-sm text-anthrazit/70">
{t("aktion.laden")}
</div>
);
}

View File

@@ -0,0 +1,14 @@
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { t } from "@/lib/i18n/de";
export default function AppNotFound() {
return (
<div className="mx-auto max-w-md py-16 text-center">
<p className="text-anthrazit">{t("fehler.nichtGefunden")}</p>
<Button asChild className="mt-4">
<Link href="/start">{t("aktion.zurueckZurStartseite")}</Link>
</Button>
</div>
);
}

View File

@@ -0,0 +1,24 @@
import { StatusBadge } from "@/components/ui/badge";
import { t } from "@/lib/i18n/de";
export default function StartPage() {
return (
<div className="space-y-6">
<header>
<h1 className="font-display text-2xl text-navy">{t("app.name")}</h1>
<p className="mt-1 text-sm text-anthrazit/70">
Vernetzte Übersicht der Fahrzeuge und Geräte der Feuerwehren.
</p>
</header>
<section className="rounded-md border border-rand bg-white p-6">
<h2 className="font-display text-lg text-navy">Statusübersicht</h2>
<div className="mt-4 flex flex-wrap items-center gap-3">
<StatusBadge status="einsatzbereit" />
<StatusBadge status="wartung" />
<StatusBadge status="ausser_dienst" />
</div>
</section>
</div>
);
}

View File

@@ -0,0 +1,33 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { t } from "@/lib/i18n/de";
// Platzhalter-Anmeldeseite. Der Auth-Workstream (Workstream 3) ersetzt die
// Logik durch Authentik-OIDC bzw. lokalen Login.
export default function AnmeldenPage() {
return (
<div>
<h1 className="font-display text-xl text-navy">{t("auth.anmelden")}</h1>
<p className="mt-1 text-sm text-anthrazit/70">{t("auth.erforderlich")}</p>
<form className="mt-6 space-y-4">
<div className="space-y-1.5">
<Label htmlFor="email">E-Mail</Label>
<Input id="email" name="email" type="email" autoComplete="username" />
</div>
<div className="space-y-1.5">
<Label htmlFor="passwort">Passwort</Label>
<Input
id="passwort"
name="passwort"
type="password"
autoComplete="current-password"
/>
</div>
<Button type="submit" className="w-full" disabled>
{t("auth.anmelden")}
</Button>
</form>
</div>
);
}

15
src/app/(auth)/error.tsx Normal file
View File

@@ -0,0 +1,15 @@
"use client";
import { Button } from "@/components/ui/button";
import { t } from "@/lib/i18n/de";
export default function AuthError({ reset }: { error: Error; reset: () => void }) {
return (
<div className="py-8 text-center">
<p className="text-anthrazit">{t("fehler.allgemein")}</p>
<Button onClick={reset} className="mt-4">
{t("aktion.erneutVersuchen")}
</Button>
</div>
);
}

17
src/app/(auth)/layout.tsx Normal file
View File

@@ -0,0 +1,17 @@
import { NetzknotenLogo } from "@/components/brand/netzknoten-logo";
import { t } from "@/lib/i18n/de";
// Schlankes Login-Layout ohne App-Chrome (keine Topbar/Navigation).
export default function AuthLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-nebel px-4">
<div className="mb-6 flex items-center gap-2">
<NetzknotenLogo size={32} />
<span className="font-display text-xl text-navy">{t("app.name")}</span>
</div>
<div className="w-full max-w-sm rounded-md border border-rand bg-white p-6 shadow-sm">
{children}
</div>
</div>
);
}

View File

@@ -0,0 +1,7 @@
import { t } from "@/lib/i18n/de";
export default function AuthLoading() {
return (
<div className="py-8 text-center text-sm text-anthrazit/70">{t("aktion.laden")}</div>
);
}

25
src/app/layout.tsx Normal file
View File

@@ -0,0 +1,25 @@
import type { Metadata } from "next";
import "@/styles/globals.css";
export const metadata: Metadata = {
title: "FlorianNetz",
description:
"FlorianNetz — vernetzte Übersicht der Fahrzeuge und Geräte der Feuerwehren.",
};
// Schriften werden über CSS-Variablen (--font-sans / --font-display) in
// globals.css gesetzt. Bewusst KEIN next/font/google: das würde zur Build-Zeit
// Google-Fonts laden und in abgeschotteten (air-gapped) Deployments/CI brechen.
// Die Stacks fallen sauber auf Inter→system-ui bzw. Source Serif 4→Georgia
// zurück; selbst gehostete Schriften können später ergänzt werden.
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="de">
<body className="font-sans">{children}</body>
</html>
);
}

7
src/app/page.tsx Normal file
View File

@@ -0,0 +1,7 @@
import { redirect } from "next/navigation";
// Root-Redirect. Der Auth-Workstream verfeinert dies (Login vs. /start je
// nach Session). Im Fundament leiten wir auf die Startseite weiter.
export default function RootPage() {
redirect("/start");
}