Fundament: ISMS-Module entfernt; Craftvia-Rollen, Module, Navigation, i18n-Split

- ISMS-Routen, Actions, Server-/Lib-Code, Komponenten, Prisma-Modelle, Seeds,
  Importer, Skripte und ISMS-Tests entfernt (Fundament bleibt: Auth, Identity,
  MFA/WebAuthn, RBAC, Audit, Mail, Storage, Backup/DSGVO, Plattform-Admin)
- Schema auf Fundament-Modelle reduziert; TenantSettings generisch (+phone/email)
- TENANT_MODELS (db.ts, backup/topology.ts) und PII-Felder ausgedünnt
- RBAC: Rollen tenant-admin/backoffice/team-lead/technician + Craftvia-Permissions
- Modul-Katalog (customers, sites, teams, work_orders, imports, field, reports,
  emergency, documents, notifications, lotse) + Navigation aus src/lib/nav.ts
- Modul-Routen mit requireModule-Layout und Platzhalterseite
- Message-Katalog je Namespace (messages/<locale>/<namespace>.json), fs-Loader
- check-module-guards: Modul-Key aus src/server/actions/<moduleKey>/
- Provisionierung, Admin-Konsole, Einstellungen, Files-Route, Mail entkoppelt
- Seed minimal (demo/demo2, Nutzer je Rolle); Fundament-Tests auf Role/
  NotificationPreference-Fixtures umgestellt

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 11:35:44 +02:00
co-authored by Claude Opus 5
parent c8e6f30a27
commit 8491c7f173
443 changed files with 1325 additions and 87773 deletions
+36 -10
View File
@@ -1,22 +1,48 @@
import { readdir, readFile } from "node:fs/promises";
import { join } from "node:path";
import { getRequestConfig } from "next-intl/server";
import { auth } from "@/server/auth";
import { prisma } from "@/server/db";
/**
* i18n (SPEC §7): alle UI-Strings kommen aus dem Message-Katalog.
* i18n: alle UI-Strings kommen aus dem Message-Katalog.
*
* Die UI-Sprache ist eine PERSÖNLICHE Präferenz und lebt an der globalen
* `Identity` (Option C, `uiLocale`) — sie folgt der Person über alle Mandanten.
* Getrennt von `TenantSettings.locale` (= Vorlagen-Import-Sprache des Inhalts).
* Katalog-Aufteilung (konfliktfrei für parallele Lanes):
* messages/<locale>/<namespace>.json — ein File je Top-Level-Namespace.
* Der Dateiname IST der Namespace: messages/de/customers.json → t = getTranslations("customers").
* Neue Lane = neues File in messages/de/ UND messages/en/ — kein Code-Eingriff nötig.
*
* Robust: auf Login-/Session-losen Seiten (oder wenn die Session keinen
* Identity-Kontext trägt, z. B. reine Plattform-Sessions) fällt die Sprache auf
* `de` zurück. Fehler bei der Auflösung dürfen das Rendering nie brechen.
* Laden: zur Laufzeit per fs aus `<cwd>/messages/<locale>/*.json` (gemerged). Für den
* standalone-Build kopiert `outputFileTracingIncludes` (next.config.ts) den Ordner mit;
* im Dockerfile liegt er damit automatisch unter /app/messages. In Produktion wird der
* gemergte Katalog je Locale gecacht, im Dev bei jedem Request neu gelesen (Hot-Edit).
*
* Die UI-Sprache ist eine PERSÖNLICHE Präferenz an der globalen `Identity` (`uiLocale`).
* Auf session-losen Seiten fällt sie auf `de` zurück; Fehler brechen das Rendering nie.
*/
const SUPPORTED = new Set(["de", "en"]);
const DEFAULT_LOCALE = "de";
type Messages = Record<string, unknown>;
const cache = new Map<string, Messages>();
export async function loadMessages(locale: string): Promise<Messages> {
const cached = cache.get(locale);
if (cached) return cached;
const dir = join(process.cwd(), "messages", locale);
const files = (await readdir(dir)).filter((f) => f.endsWith(".json")).sort();
const messages: Messages = {};
for (const file of files) {
const namespace = file.slice(0, -".json".length);
messages[namespace] = JSON.parse(await readFile(join(dir, file), "utf8"));
}
if (process.env.NODE_ENV === "production") cache.set(locale, messages);
return messages;
}
export default getRequestConfig(async () => {
let locale = "de";
let locale = DEFAULT_LOCALE;
try {
const session = await auth();
const identityId = session?.user?.identityId;
@@ -28,10 +54,10 @@ export default getRequestConfig(async () => {
if (identity?.uiLocale && SUPPORTED.has(identity.uiLocale)) locale = identity.uiLocale;
}
} catch {
locale = "de";
locale = DEFAULT_LOCALE;
}
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default,
messages: await loadMessages(locale),
};
});