Basisbausteine im shadcn/ui-Stil, aber mit drei bewussten Abweichungen: - Bedienelemente sind 44px hoch statt 36px. Ich bediene die App oft am Handy im Heim oder auf dem Amt; kleiner ist dort nicht treffsicher. - Auswahlfelder benutzen das native <select>. Android und iOS oeffnen dafuer ihre eigene Liste - schneller als jeder Nachbau, und mit Screenreader und Tastatur funktioniert es ohne Zutun. - Eingabefelder haben mindestens 16px Schrift, sonst zoomt iOS-Safari beim Hineintippen das Formular heran. <Feld> verdrahtet Beschriftung, Erklaerung und Fehler ueber aria-describedby. Der Erklaerungstext ist kein Beiwerk: die Anwendung benutzt Begriffe aus dem Betreuungsrecht, die niemand nachschlagen soll. <Rechtshinweis> ist der stehende Vermerk, dass hier keine Rechtsberatung stattfindet. Er steht an jeder Stelle mit Paragraphenbezug. Der Anwendungsrahmen fuehrt drei Bedingungen zusammen, ohne die niemand an Fachdaten kommt: angemeldet, zweiter Faktor eingerichtet, mindestens eine betreute Person zugeordnet. Die Navigation zeigt Zaehler fuer ueberfaellige Fristen und neuen Posteingang - die Frage "wo muss ich hin?" wird schon im Menue beantwortet. Das Farbschema wird per Inline-Skript mit CSP-Nonce gesetzt, bevor die Seite gezeichnet wird - sonst blitzt beim Laden das helle Thema auf. docs/KONVENTIONEN.md haelt fest, was in allen weiteren Etappen gilt: Verzeichnisaufbau, Cent-Betraege, Umgang mit verschluesselten Feldern, Zugriffspruefung, Audit-Pflicht, Ampelfarben, Commit-Trailer. Etappe: 1 Phase: Fundament
5.3 KiB
Konventionen im Betreuungs-Cockpit
Verbindlich für jeden, der Code in dieses Projekt schreibt. Wer davon abweicht, erzeugt Reibung an der nächsten Schnittstelle.
Sprache
- Alles auf Deutsch: Bezeichner, Kommentare, Commit-Nachrichten,
Oberflächentexte, Fehlermeldungen. Ausnahmen sind Framework-Vorgaben
(
page.tsx,layout.tsx,default,props) und etablierte Fachbegriffe ohne gute Übersetzung (Token,Hash,Cookie). - Umlaute in Oberflächentexten: ja, korrekt geschrieben.
Umlaute in Bezeichnern und Dateinamen: nein, umschrieben
(
vermoegen,naechsterSchritt,zurueck). - Du-Ansprache. Keine Juristensprache ohne Erklärung — steht ein Paragraph im Text, gehört eine Erklärung daneben.
Verzeichnisse
src/
app/
(auth)/ Anmeldung, Ersteinrichtung, Zweitfaktor
(app)/ Alle Fachseiten, hinter Anmeldung + 2FA
api/
components/
ui/ Basisbausteine (Button, Input, Card, …)
<bereich>/ Fachkomponenten je Modul
lib/
crypto/ Feldverschlüsselung, Hashing, TOTP
auth/ Sitzung, Rollen, Rate-Limiting
validierung/ Zod-Schemata je Entität
<bereich>/ Fachlogik ohne React
server/
actions/ Server Actions ("use server")
prisma/
tests/ Vitest (Logik)
e2e/ Playwright (kritische Flows)
Ein Modul bringt mit: Seite(n) unter app/(app)/<modul>/, Fachkomponenten
unter components/<modul>/, Zod-Schema unter lib/validierung/<modul>.ts,
Server Actions unter server/actions/<modul>.ts, reine Logik unter
lib/<modul>/.
Datenzugriff
import { db, nichtGeloescht } from '@/lib/db'
- Jede Abfrage auf Fachdaten filtert
...nichtGeloescht(Papierkorb). - Jede Abfrage ist auf
betreuteIdeingegrenzt. - Zugriff wird immer über
erfordereZugang(betreuteId, modul, art)geprüft — auch in Server Actions, nicht nur in der Seite. Nie nur im UI ausblenden.
const { betreute, benutzer, darfSchreiben } = await erfordereZugang(id, 'FRISTEN', 'schreiben')
Geldbeträge
Immer BigInt in Cent, Feldname endet auf Cent. Anzeige über
formatCent(). Nie number für Beträge, nie Decimal, nie Fließkomma.
Auf dem Weg zu Client Components über centZuZahl().
Datum
- Fristen, Stichtage, Bescheiddaten:
@db.Date, ohne Uhrzeit. - Zeitstempel (
erstelltAm, Protokolle): vollesDateTime. - Anzeige über
formatDatum()/formatDatumZeit()aus@/lib/utils. - Datumsrechnung immer über
date-fns, nie mitsetDatevon Hand.
Verschlüsselte Felder
Feldname endet auf Verschluesselt. Nie direkt schreiben:
import { sealen, oeffnen } from '@/lib/crypto/feld'
const daten = { ...rest, ...sealen('Vorgang', { beschreibung: eingabe.beschreibung }) }
const offen = oeffnen('Vorgang', datensatzAusDb) // offen.beschreibung ist Klartext
Neues verschlüsseltes Feld? Erst in VERSCHLUESSELTE_FELDER in
src/lib/crypto/feld.ts eintragen, sonst wirft sealen().
Verschlüsselte Felder lassen sich nicht filtern oder sortieren.
Audit-Log
Jede schreibende Aktion protokolliert:
await protokollieren({
aktion: 'GEAENDERT',
entitaet: 'Frist',
entitaetId: frist.id,
bezeichnung: frist.titel,
betreuteId,
userId: benutzer.id,
userEmail: benutzer.email,
userName: benutzer.name,
aenderung: aenderungErmitteln(vorher, nachher),
})
Server Actions
- Immer
'use server'oben, immer Zod-Validierung, immer Zugriffsprüfung. - Rückgabe für Formulare: ein Zustandsobjekt
{ fehler?: string; felder?: Record<string, string>; werte?: … }, benutzt mituseActionState. Keine geworfenen Fehler ans Formular. - Nach dem Schreiben
revalidatePath()auf die betroffene Seite. - Löschen heißt:
geloeschtAmsetzen. Endgültiges Löschen nur im Papierkorb und nur nacherfordereFrischeAnmeldung().
Oberfläche
- Basisbausteine aus
@/components/ui/*. Formularfelder immer in<Feld id beschriftung erklaerung fehler>einwickeln — das verdrahtet Label, Erklärung und Fehler barrierefrei. - Jede Seite beginnt mit
<SeitenKopf titel beschreibung pfad aktionen />. - Leere Listen bekommen
<Leerzustand>mit einem konkreten nächsten Schritt, nie nur "keine Daten". - Steht ein Paragraph auf der Seite, steht
<Rechtshinweis>darunter. - Mindestens 44 px Touchziele, Schrift nie unter 16 px in Eingabefeldern.
- Farbe ist nie die einzige Information — Ampeln immer mit Text.
- Tabellen scrollen im eigenen Rahmen, nie die Seite.
- Keine externen Fonts, keine CDNs, keine Tracker. Bilder als Data-URI oder aus dem eigenen Objektspeicher.
Statusfarben (Ampel)
| Lage | Variante |
|---|---|
| überfällig | rot |
| diese Woche fällig | orange |
| dieser Monat fällig | gelb |
| erledigt / im Plan | gruen |
Tests
- Fachlogik (Fristberechnung, Beträge, Bewertungen): Vitest unter
tests/. - Kritische Abläufe: Playwright unter
e2e/. - Ein Test beschreibt auf Deutsch, was fachlich gelten soll — nicht, welche Funktion aufgerufen wird.
Commits
- Deutsch, Imperativ, erste Zeile ≤ 72 Zeichen.
- Körper erklärt warum, nicht was.
- Trailer am Ende, ohne Leerzeile dazwischen:
Etappe: 3
Phase: Papier
Diese Trailer wertet npm run protokoll aus — daraus entsteht das
Bauprotokoll mit den Zeiten je Etappe.