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>
);
}