Phase 0: Fundament der Feuerwehr-Verwaltungsplattform
Legt den kompletten Unterbau an, auf dem die Fachmodule aufsetzen. Betrieb: - docker-compose.yml mit Traefik, App, Worker, PostgreSQL 17 + PostGIS, Redis, MinIO und Gotenberg; Entwicklungs-Override ohne Traefik, dafuer mit Mailpit und Adminer - Mehrstufiges Dockerfile: eigener Benutzer, tini als PID 1, Healthcheck - .env.example mit einem Kommentar zu jeder Variable Mandantentrennung: - Row Level Security auf allen mandantenpflichtigen Tabellen - Zwei getrennte Datenbankverbindungen: florian_app mit aktiver RLS fuer die Anwendung, Eigentuemer-Zugang nur fuer Migration, Seed und Anmeldung - fuerOrganisation() als einziger Weg zu Fachdaten Nachvollziehbarkeit: - AuditLog append-only per Trigger; UPDATE ist dauerhaft gesperrt, DELETE nur im gekennzeichneten Aufbewahrungslauf - Geheimnisse werden vor dem Schreiben entfernt Anmeldung: - OIDC gegen ein extern betriebenes authentik, Rollen aus Gruppen nur als Vorbelegung beim ersten Login - Lokaler E-Mail/Passwort-Login als Rueckfallweg - Terminal per PIN und RFID mit zwei Schranken (Geraet, dann Person) Weiteres: - lib/authz.ts mit 9 Rollen und 38 Einzelrechten - lib/storage.ts (MinIO), lib/audit.ts, lib/warteschlange.ts (BullMQ) - Integrationen fuer Mail, Telegram und LLM hinter abschaltbaren Interfaces - tRPC v11, Basis-Layout mit Seitenleiste und dunkler Darstellung - Seed mit einer Beispielwehr: 16 Personen, 3 Fahrzeuge, 40 Geraete - 32 Vitest-Tests (davon 8 gegen eine echte Datenbank), 10 Playwright-Tests - README, Anleitung mit Screenshots, Roadmap, Aufwandsschaetzung, Devlog, Versionierungsregeln und Changelog Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import { Worker, type Job } from "bullmq";
|
||||
import { env } from "@/lib/env";
|
||||
import { AUFTRAG, WARTESCHLANGE, redis, type AuftragsDaten } from "@/lib/warteschlange";
|
||||
import { verbindungenTrennen } from "@/lib/db";
|
||||
|
||||
/**
|
||||
* Hintergrundprozess.
|
||||
*
|
||||
* Läuft als eigener Container neben der Anwendung und arbeitet die
|
||||
* Warteschlange ab. In Phase 0 sind die Auftragsarten angelegt, aber fachlich
|
||||
* noch leer — die Fristenberechnung kommt in Phase 2, die Erinnerungen in
|
||||
* Phase 7. Was hier schon steht, ist das Gerüst: Aufnahme, Protokollierung,
|
||||
* Fehlerbehandlung und ein sauberes Herunterfahren.
|
||||
*/
|
||||
|
||||
function protokoll(nachricht: string, ...weiteres: unknown[]): void {
|
||||
console.info(`[worker] ${nachricht}`, ...weiteres);
|
||||
}
|
||||
|
||||
async function fristenPruefen(auftrag: Job<AuftragsDaten>): Promise<{ geprueft: number }> {
|
||||
protokoll(`Fristen prüfen für Organisation ${auftrag.data.organisationId}`);
|
||||
// Ab Phase 2: Geräte mit überschrittener Frist auf PRUEFUNG_FAELLIG setzen.
|
||||
return { geprueft: 0 };
|
||||
}
|
||||
|
||||
async function erinnerungSenden(auftrag: Job<AuftragsDaten>): Promise<{ versendet: number }> {
|
||||
protokoll(`Erinnerungen für Organisation ${auftrag.data.organisationId}`);
|
||||
// Ab Phase 7: Mail und Telegram über lib/integrations.
|
||||
return { versendet: 0 };
|
||||
}
|
||||
|
||||
async function pdfErzeugen(auftrag: Job<AuftragsDaten>): Promise<{ erzeugt: boolean }> {
|
||||
protokoll(`PDF erzeugen für Organisation ${auftrag.data.organisationId}`);
|
||||
// Ab Phase 2: HTML an Gotenberg, Ergebnis nach MinIO über lib/storage.
|
||||
return { erzeugt: false };
|
||||
}
|
||||
|
||||
async function aufbewahrungPruefen(auftrag: Job<AuftragsDaten>): Promise<{ entfernt: number }> {
|
||||
protokoll(`Aufbewahrungsfristen prüfen für Organisation ${auftrag.data.organisationId}`);
|
||||
// Ab Phase 8: Audit-Log exportieren und danach abräumen,
|
||||
// ausgetretene Mitglieder nach PERSON_ANONYMISIERUNG_TAGE anonymisieren.
|
||||
return { entfernt: 0 };
|
||||
}
|
||||
|
||||
const bearbeiter: Record<string, (auftrag: Job<AuftragsDaten>) => Promise<unknown>> = {
|
||||
[AUFTRAG.FRISTEN_PRUEFEN]: fristenPruefen,
|
||||
[AUFTRAG.ERINNERUNG_SENDEN]: erinnerungSenden,
|
||||
[AUFTRAG.PDF_ERZEUGEN]: pdfErzeugen,
|
||||
[AUFTRAG.AUFBEWAHRUNG_PRUEFEN]: aufbewahrungPruefen,
|
||||
};
|
||||
|
||||
const worker = new Worker<AuftragsDaten>(
|
||||
WARTESCHLANGE,
|
||||
async (auftrag) => {
|
||||
const bearbeiten = bearbeiter[auftrag.name];
|
||||
if (!bearbeiten) {
|
||||
throw new Error(`Unbekannte Auftragsart: ${auftrag.name}`);
|
||||
}
|
||||
const beginn = Date.now();
|
||||
const ergebnis = await bearbeiten(auftrag);
|
||||
protokoll(`${auftrag.name} erledigt in ${Date.now() - beginn} ms`);
|
||||
return ergebnis;
|
||||
},
|
||||
{
|
||||
connection: redis(),
|
||||
concurrency: env.WORKER_CONCURRENCY,
|
||||
},
|
||||
);
|
||||
|
||||
worker.on("failed", (auftrag, fehler) => {
|
||||
console.error(
|
||||
`[worker] Auftrag ${auftrag?.name ?? "?"} (${auftrag?.id ?? "?"}) fehlgeschlagen:`,
|
||||
fehler.message,
|
||||
);
|
||||
});
|
||||
|
||||
worker.on("ready", () => {
|
||||
protokoll(
|
||||
`bereit — Warteschlange "${WARTESCHLANGE}", ${env.WORKER_CONCURRENCY} parallele Aufträge`,
|
||||
);
|
||||
});
|
||||
|
||||
/** Laufende Aufträge zu Ende bringen, bevor der Prozess endet. */
|
||||
async function herunterfahren(signal: string): Promise<void> {
|
||||
protokoll(`${signal} empfangen — fahre herunter ...`);
|
||||
await worker.close();
|
||||
await verbindungenTrennen();
|
||||
redis().disconnect();
|
||||
protokoll("beendet.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.on("SIGTERM", () => void herunterfahren("SIGTERM"));
|
||||
process.on("SIGINT", () => void herunterfahren("SIGINT"));
|
||||
Reference in New Issue
Block a user