Files
betreuungs-cockpit/docs/SCHNITTSTELLEN.md
T
Florian Wilde 22b3a9b9e9 feat: Stammdaten, Ersteinrichtung, Zweitfaktor und Protokollansicht
Damit ist Etappe 1 fachlich vollstaendig.

Ersteinrichtung in drei Schritten, bewusst ausserhalb des
Anwendungsrahmens: der setzt Benutzer, zweiten Faktor UND eine betreute
Person voraus - beim allerersten Start gibt es nichts davon. Die Aktion
fuer den ersten Zugang sperrt sich selbst, sobald ein Benutzer existiert.

Zweiter Faktor: Das Geheimnis wird zuerst nur abgelegt, ohne
totpAktiviertAm. Erst ein echter Code aus der App schaltet ihn scharf.
Wer sich beim Abtippen vertut oder die falsche App nimmt, wuerde sich
sonst dauerhaft aussperren. Erst danach gibt es die
Wiederherstellungscodes - einmal angezeigt, danach nur noch als Hash.

Betreuungsjahr (src/lib/stammdaten/betreuungsjahr.ts): laeuft ab dem Tag
der Bestellung, nicht ab dem Kalenderjahr. Schrittweise vorwaerts
gerechnet statt ueber eine Differenz, damit Schaltjahre und der
29. Februar von selbst stimmen. Daran haengen Jahresbericht,
Rechnungslegung und die Ausschlussfrist der Aufwandspauschale.

Aendert sich das Bestellungsdatum, werden die Pflichtfristen neu
abgeleitet - ueber einen dynamischen Import, damit die Stammdaten auch
dann speicherbar bleiben, wenn das Fristenmodul fehlt.

Protokollansicht zeigt je Eintrag die geaenderten Felder als alt -> neu.
Die Aenderungen liegen verschluesselt, weil dort Diagnosen und
Kontostaende stehen koennen.

Seed legt bewusst keine echten Personendaten an, nur den ersten Zugang.
Mit SEED_DEMO=true kommt ein vollstaendiger Beispielfall dazu - Erika
Musterfrau, COPD GOLD III, Pflegegrad 3, Heimeinzug Januar 2026,
leerstehendes Haus, zwei Versicherungen, laufender Antrag beim Bezirk
mit acht Nachforderungen. Nur ausserhalb der Produktion.

Docker Compose ohne Reverse Proxy - der ist auf dem Zielserver
vorhanden. Die Datenbank hat keine Portfreigabe und ist nur ueber das
interne Netz erreichbar; die Anwendung horcht auf 127.0.0.1.

Etappe: 1
Phase: Fundament
2026-08-22 02:16:22 +02:00

8.6 KiB

Schnittstellen zwischen den Modulen

Verträge, auf die sich mehrere Etappen stützen. Wer eine dieser Schnittstellen implementiert, hält sich exakt an die hier beschriebene Signatur; wer sie benutzt, darf sich darauf verlassen.


1. Dateiablage — src/lib/speicher/index.ts

Zwei Treiber hinter einer Schnittstelle: datei (lokales Verzeichnis, für Entwicklung und kleine Installationen) und s3 (S3-kompatibler Speicher in der EU). Gesteuert über SPEICHER_TREIBER bzw. das Vorhandensein von S3_ACCESS_KEY_ID.

export type Ablageziel = {
  /** Schlüssel im Speicher, z.B. "betreute/<id>/dokumente/<uuid>.pdf" */
  schluessel: string
  groesseBytes: number
  /** SHA-256 hex — Nachweis der Unverändertheit gegenüber Gericht. */
  pruefsumme: string
}

/** Legt eine Datei ab. Der Schlüssel wird erzeugt, nie vom Nutzer bestimmt. */
export function ablegen(
  betreuteId: string,
  bereich: 'dokumente' | 'berichte' | 'exporte' | 'vorschau',
  dateiname: string,
  inhalt: Buffer,
  mimeTyp: string,
): Promise<Ablageziel>

/** Holt eine Datei zurück in den Speicher des Servers. */
export function lesen(schluessel: string): Promise<Buffer>

/**
 * Kurzlebige, signierte URL. Gültigkeit in Sekunden, Standard 300.
 * Beim Dateitreiber zeigt sie auf eine eigene, ebenfalls signierte Route.
 */
export function signierteUrl(schluessel: string, sekunden?: number): Promise<string>

export function entfernen(schluessel: string): Promise<void>
export function existiert(schluessel: string): Promise<boolean>

Regeln

  • Der Schlüssel enthält immer die betreuteId als zweites Segment. Jede Leseoperation prüft vorher, dass der angemeldete Benutzer Zugriff auf genau diese betreuteId hat.
  • Uploads werden nie als HTML ausgeliefert. Ausgehende Antworten tragen Content-Disposition: attachment und X-Content-Type-Options: nosniff.
  • Erlaubte Typen: application/pdf, image/jpeg, image/png, image/heic, image/webp, image/tiff. Geprüft wird an den Magic Bytes, nicht am gemeldeten MIME-Typ und nicht an der Endung.
  • Größenlimit aus env.uploadMaxBytes (Standard 25 MB).

2. PDF-Erzeugung — src/lib/pdf/

Grundlage ist @react-pdf/renderer (kein Chromium im Container).

src/lib/pdf/rahmen.tsx

export type Berichtskopf = {
  titel: string
  betreute: { name: string; geburtsdatum: Date | null }
  /** z.B. "Amtsgericht Bayreuth · Az. XVII 123/25" */
  behoerde?: string
  aktenzeichen?: string
  zeitraumVon?: Date | null
  zeitraumBis?: Date | null
  stichtag?: Date | null
  rechtsgrundlage?: string
  erstelltAm: Date
  erstelltVon: string
}

/** Seitenrahmen mit Kopf, Fußzeile (Seite x von y, Erstelldatum) und
 *  optionalem Unterschriftenfeld. Jeder Bericht benutzt ihn. */
export function Berichtsseite(props: {
  kopf: Berichtskopf
  mitUnterschrift?: boolean
  children: React.ReactNode
}): JSX.Element

src/lib/pdf/erzeugen.ts

/** Rendert ein react-pdf-Dokument zu einem Buffer. */
export function pdfErzeugen(dokument: React.ReactElement): Promise<Buffer>

/**
 * Fügt mehrere PDFs zu einem zusammen (Belegmappe, Unterlagenpaket).
 * Bilder werden vorher in einseitige PDFs gewandelt.
 * Nutzt pdf-lib.
 */
export function pdfsZusammenfuehren(teile: Buffer[]): Promise<Buffer>

/** Seitenzahlen und Fußzeile über ein zusammengeführtes PDF legen. */
export function paginieren(pdf: Buffer, fusszeile: string): Promise<Buffer>

Regeln

  • Schrift: die in @react-pdf/renderer eingebauten Standardschriften (Helvetica). Kein Nachladen von Google Fonts.
  • Jeder Bericht trägt Kopf (Betreute, Aktenzeichen, Gericht/Behörde, Zeitraum), Fußzeile mit Seitenzahl und Erstellungsdatum sowie ein Unterschriftenfeld.
  • Beträge über formatCent(), Daten über formatDatum().

3. OCR — src/lib/ocr/index.ts

export type OcrErgebnis = {
  text: string
  status: 'fertig' | 'fehlgeschlagen' | 'nicht_verfuegbar'
  seitenzahl?: number
  meldung?: string
}

/** Ruft Tesseract (deutsch) auf. Nie werfen — Status zurückgeben. */
export function ocrLesen(inhalt: Buffer, mimeTyp: string): Promise<OcrErgebnis>

/** Ist Tesseract auf diesem System vorhanden? */
export function ocrVerfuegbar(): Promise<boolean>

Regeln

  • Aufruf über das System-Binary tesseract (env.tesseractBefehl), Sprache deu. Im Docker-Image ist es installiert, auf einem Windows- Entwicklungsrechner in der Regel nicht — dann liefert die Funktion nicht_verfuegbar, und der Upload funktioniert trotzdem.
  • PDFs werden vorher mit pdftoppm in Bilder gewandelt; fehlt auch das, wird der eingebettete Text des PDFs mit pdf-lib gelesen.
  • Der OCR-Lauf blockiert den Upload nicht: Dokument wird mit ocrStatus = "ausstehend" gespeichert, die Erkennung läuft danach.

4. Fristenableitung — src/lib/fristen/pflichtfristen.ts

export type Pflichtfrist = {
  /** Stabiler Schlüssel, verhindert Dubletten: "anfangsbericht",
   *  "jahresbericht-2027", "aufwandspauschale-2026", … */
  systemSchluessel: string
  titel: string
  beschreibung: string
  datum: Date
  rechtsgrundlage: string
  typ: 'GESETZLICH' | 'BEHOERDLICH' | 'SELBST_GESETZT' | 'WIEDERVORLAGE'
  ausschlussfrist: boolean
  vorwarnungTage: number[]
  wiederholungRrule?: string
}

/** Leitet alle Pflichtfristen aus den Stammdaten ab. Rein, ohne Datenbank. */
export function pflichtfristenAbleiten(stammdaten: {
  bestellungAm: Date | null
  befreiterBetreuer: boolean
  ehrenamtlich: boolean
  pflegegrad: number | null
  jahreVoraus?: number
}): Pflichtfrist[]

/** Schreibt sie in die Datenbank, ohne Dubletten (upsert auf
 *  betreuteId + systemSchluessel). */
export function pflichtfristenSchreiben(betreuteId: string): Promise<number>

Abzuleiten sind mindestens:

Schlüssel Frist Grundlage
vermoegensverzeichnis 3 Monate nach Bestellung § 1835 BGB
anfangsbericht 3 Monate nach Bestellung § 1863 Abs. 1 BGB
jahresbericht-<jahr> jährlich zum Betreuungsjahr § 1863 Abs. 3 BGB
vermoegensuebersicht-<jahr> jährlich, mit dem Jahresbericht § 1859, § 1865 BGB
aufwandspauschale-<jahr> 6 Monate nach Ende des Betreuungsjahres, Ausschlussfrist § 1878 BGB
landespflegegeld-<jahr> 31.12. Bayerisches Landespflegegeldgesetz
zuzahlungsbefreiung-<jahr> jährlich § 62 SGB V
rueckkaufswerte-<jahr> jährlich selbst gesetzt
pflegegrad-pruefung konfigurierbar § 18 SGB XI

Betreuungsjahr: läuft ab dem Tag der Bestellung, nicht ab dem Kalenderjahr. Die Aufwandspauschale verfällt sechs Monate nach dessen Ende — diese Frist wird als ausschlussfrist: true markiert und im Dashboard rot mit ausdrücklicher Verfallwarnung gezeigt.


5. Hinweis-Wissensbasis — daten/hinweise.json

Pflegbare JSON-Datei, kein Hardcoding.

{
  "version": 1,
  "karten": [
    {
      "schluessel": "wohnraum-aufgeben",
      "titel": "Wohnung kündigen? Erst das Gericht fragen.",
      "text": "Die Aufgabe von Wohnraum der betreuten Person ist dem Gericht mitzuteilen und bedarf der Genehmigung.",
      "rechtsgrundlage": "§ 1833 BGB",
      "link": "https://www.gesetze-im-internet.de/bgb/__1833.html",
      "kontext": ["IMMOBILIE", "VORGANG:IMMOBILIE", "VORLAGE:wohnungsaufloesung"],
      "gewicht": 10
    }
  ]
}

kontext ist eine Liste von Auslösern: ein Modul, VORGANG:<Kategorie>, VORLAGE:<schluessel> oder FRIST:<systemSchluessel>. Die Oberfläche zeigt passende Karten, sortiert nach gewicht absteigend, jede mit <Rechtshinweis>.


6. Vorgangs-Vorlagen — src/lib/vorgaenge/vorlagen.ts

export type Vorgangsvorlage = {
  schluessel: string
  titel: string
  kategorie: VorgangKategorie
  beschreibung: string
  genehmigungspflichtig: boolean
  rechtsgrundlage?: string
  /** Vorbefüllte Aufgabenliste. */
  aufgaben: { titel: string; beschreibung?: string; tageBisFaellig?: number }[]
  /** Fristen, die mit angelegt werden. */
  fristen?: { titel: string; tageBisFaellig: number; rechtsgrundlage?: string }[]
}

export const VORLAGEN: Vorgangsvorlage[]

Mindestens: heimeinzug, antrag-hilfe-zur-pflege, wohnungsaufloesung, hausverkauf, darlehen-91-sgb-xii, versicherungsverwertung, pflegegrad-hoeherstufung, steuererklaerung, widerspruch-bescheid.


7. E-Mail — src/lib/mail.ts

export function mailSenden(nachricht: {
  an: string
  betreff: string
  text: string
}): Promise<{ gesendet: boolean; meldung?: string }>

Ist kein SMTP konfiguriert, wird nichts gesendet und gesendet: false zurückgegeben — ohne Fehler. Erinnerungen sind eine Zugabe, kein Kernstück; die Fristen stehen auch so im Dashboard.