Files
deklarix/CLAUDE.md
noroot eb99891e38 feat: eigene Werkzeugkatalog-Einträge für Mandanten
Ein Mandant kann jetzt zusätzlich zum Sperren zentraler Katalogeinträge
auch eigene, nur für sich sichtbare Werkzeuge anlegen/bearbeiten/
löschen (GET/POST /verwaltung/werkzeuge/neu bzw. /{id}, POST
/{id}/loeschen — werkzeug.account_id = eigener Account). Nutzt
dasselbe Formular wie der zentrale Katalog des Betreibers
(werkzeugFormData/betreiber-werkzeug-form.html, ein neues ActionBase-
Feld unterscheidet die Ziel-URL); ein zentraler oder fremder Eintrag
bleibt über diese Route unerreichbar (404). Schließt die letzte
dokumentierte Lücke bei "eigene Werkzeug-Freigaben/-Sperrungen" (Ebene 4).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 15:23:43 +02:00

754 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Deklarix
> KI-Antragsprüfung für Unternehmen: Mitarbeitende beschreiben ein
> KI-Vorhaben in eigenen Worten, das System leitet Datenklasse,
> KI-VO-Einstufung und Anforderungsprofil ab, gleicht sie gegen einen
> gepflegten Werkzeugkatalog ab und erzeugt einen begründeten
> Entscheidungsvorschlag. Ein Mensch entscheidet, das System bereitet
> nur vor.
---
## Produktwechsel (2026-08-28)
Deklarix war ursprünglich eine Pre-Publish-Kennzeichnungsprüfung für
Werbe-Content (UWG/MStV) für Creator, Agenturen, Marken und Kanzleien.
Dieser Scope wurde **komplett verworfen** und durch das oben
beschriebene Produkt ersetzt — kein Feature-Zusatz, ein Produktwechsel.
Wiederverwendet wurden nur produktunabhängige Bausteine: Auth/Session/
Account-Mandantentrennung, das Prinzip "Regelwerk als versionierte
YAML-Dateien, kein ML für die Bewertung", die apt/systemd/Gitea-
Deployment-Pipeline und das Design-System. Alles Werberecht-Spezifische
(Kennzeichnungsregeln, Beweisarchiv für Social-Media-Posts, Instagram/
TikTok-OAuth, Insights-Erinnerung) wurde entfernt, nicht nur ersetzt —
siehe Migration `0008_pivot_ki_antragspruefung` für den genauen Schnitt.
---
## Grundregel
**Das System entscheidet nicht. Es bereitet vor.** Der/die
Verantwortliche bestätigt, ändert oder überschreibt einen
Bewertungsvorschlag — mit Begründungsfeld bei Abweichung. Jede
Entscheidung wird als Registereintrag protokolliert. Die Bewertung wird
**nie** ohne ihre vollständige Herleitung angezeigt (welche Antwort
welche Anforderung ausgelöst hat) — ohne das ist ein Vorschlag im Audit
wertlos.
Regelbasiert, nicht ML: die Ableitungsregeln (Datenklasse, KI-VO-
Einstufung, Anforderungsprofil) liegen als versionierte YAML-Dateien
vor, nicht im Code — ein fachlich Verantwortlicher soll sie anpassen
können, ohne Go-Code anzufassen. Ein LLM ist v1 höchstens optional zur
Zweckerkennung aus dem Freitext vorgesehen (Textgenerierung/Übersetzung/
Code/Bild/Transkription/Recherche erkennen), nie für die Einstufung
selbst — und auch dort nur als Vorschlag, den der Mensch bestätigt.
---
## Fünf Ebenen, fünf Rollen
Die Oberfläche gliedert sich in fünf Ebenen mit steigender Sensibilität.
Vier Mandanten-Rollen sind eine Voreinstellung, kein fest verdrahtetes
Modell (spätere Ausbaustufe: konfigurierbare Rollen) — `betreiber` ist
technisch getrennt vom Mandantenbereich, keine Voreinstellung.
| Ebene | Zugang | Rolle(n) | Zweck |
|---|---|---|---|
| 1 — Öffentlich | ohne Konto | — | Anmeldung, Passwort zurücksetzen, Einladung annehmen, Antrag über Mandantenlink |
| 2 — Mitarbeiter | Konto nötig | `mitarbeiter` | Antrag stellen, eigene Anträge verfolgen, Rückfragen beantworten, freigegebene Werkzeuge einsehen. Bewusst schlank — dauert der Antrag zu lange, wird er umgangen |
| 3 — Fachebene | Konto nötig | `verantwortlicher` (Entscheidungsrecht), `pruefer` (identische Sicht, kein Entscheidungsrecht) | Posteingang offener Anträge, Bewertungsvorschlag mit Herleitung, entscheiden, Register, Berichte/Exporte, Audit-Log lesend |
| 4 — Mandanten-Verwaltung | Konto nötig | `admin` | Nutzer/Rollen, Abteilungen, Anmeldeverfahren, eigene Werkzeug-Freigaben/-Sperrungen, Rechnungsdaten — bezogen auf GENAU EINEN Mandanten |
| 5 — Plattform | Konto nötig, technisch getrennt | `betreiber` | Werkzeugkatalog, Regelwerk-Versionierung, Mandantenverwaltung/Freischaltung, Nutzungssicht — Netcell-IT-Personal, plattformweit über alle Mandanten |
**Funktionstrennung bleibt bestehen**, auch wenn in kleinen Firmen
dieselbe Person Admin und Verantwortlicher ist — das sind zwei Rollen
auf demselben Menschen, keine verschmolzene Rolle. `admin` (Ebene 4,
Mandanten-Verwaltung für den eigenen Account) und `betreiber` (Ebene 5,
Plattform-Betrieb für Netcell-IT über alle Mandanten) sind nicht
dasselbe, auch wenn beide "Admin"-artige Rechte haben — nur `betreiber`
ist der Nachfolger dessen, was früher (vor dem Produktwechsel)
`admin` hieß.
Rechte werden **als Prüfung an jeder Aktion** durchgesetzt (Middleware
je Handler), nicht als grob unterschiedene Seitenbereiche.
**Ebene 4 ist inzwischen zu großen Teilen umgesetzt** (Anspruch: alles,
was im Datenmodell existiert, muss über das Frontend steuerbar sein,
nicht nur einsehbar — Deklarix soll ein buchbarer Service werden, ein
reiner Anzeige-Screen reicht nicht):
- **Nutzerverwaltung** (`internal/web/admin_handlers.go`, Middleware
`requireAdmin`): `GET /verwaltung/nutzer` listet alle Logins des
eigenen Mandanten mit Status (aktiv/deaktiviert), `GET/POST
/verwaltung/nutzer/neu` legt einen weiteren Login mit einer der vier
Mandanten-Rollen an (`mitarbeiter`, `verantwortlicher`, `pruefer`,
`admin``betreiber` kann kein Mandanten-Admin vergeben, das ist
Ebene 5). Es gibt noch **keine** Einladungsmail — der Admin setzt das
Initialpasswort direkt im Formular. Nutzer werden **nicht gelöscht**
(`app_user` wird von `antrag`/`entscheidung`/`audit_log` per Foreign
Key referenziert — ein Hard-Delete würde die Historie zerstören),
sondern über `POST /verwaltung/nutzer/{id}/deaktivieren` bzw.
`.../aktivieren` (de-)aktiviert (Spalte `app_user.active`, Migration
0012). Ein deaktivierter Login kann sich nicht mehr anmelden
(`handleLogin` prüft `Active` erst NACH der Passwortprüfung, um keine
Kontoexistenz zu verraten) und verliert eine bereits laufende Sitzung
sofort (`authenticate`-Middleware prüft `Active` bei jedem Request).
Ein Admin kann sich nicht selbst deaktivieren (Aussperr-Schutz).
- **Abteilungen** (`GET /verwaltung/abteilungen`, anlegen + löschen):
reine Stammdaten für das Fragebogen-Feld "Abteilung" — ohne diese
Seite blieb die Abteilung-Auswahl im Fragebogen faktisch leer und
unbenutzbar, das war kein Komfort-, sondern ein Funktionsdefizit.
- **Eigene Werkzeug-Sperrungen und -Einträge** (`internal/web/mandant_werkzeug_handlers.go`,
`GET /verwaltung/werkzeuge`): ein Mandant kann einen zentralen
Katalogeintrag für sich sperren/entsperren (`werkzeug_sperre`), ohne
den zentralen Katalog selbst zu verändern — das bleibt Sache der
Plattform (Ebene 5, `betreiber_werkzeug_handlers.go`). Zusätzlich kann
ein Mandant eigene, nur für sich sichtbare Katalogeinträge anlegen/
bearbeiten/löschen (`GET/POST /verwaltung/werkzeuge/neu`, `GET/POST
/verwaltung/werkzeuge/{id}`, `POST /verwaltung/werkzeuge/{id}/loeschen`
`werkzeug.account_id` = der eigene Account). Nutzt dasselbe Formular
wie der zentrale Katalog (`werkzeugFormData`/`betreiber-werkzeug-form.html`,
ein `ActionBase`-Feld unterscheidet Betreiber- von Mandanten-Ziel-URL)
— ein zentraler oder fremder Eintrag ist über diese Route nicht
erreichbar (404).
**Weiterhin nicht gebaut:** Anmeldeverfahren-Konfiguration, Rechnungsdaten/
Abrechnung (kein Abo-System, bewusst "Nicht bauen v1"), Account-
Verwaltung durch den Betreiber (Ebene 5 zeigt Accounts nur lesend an —
Bearbeiten/Sperren hängt an der noch nicht getroffenen Abrechnungs-/
Freischaltungs-Architektur, siehe Offene Punkte: `account.verified`
wurde beim Produktwechsel sogar entfernt).
**Mandantenfähigkeit:** jede Tabelle trägt `account_id`. Aktuell wird
Isolation in der Anwendungsschicht erzwungen (Handler vergleichen
`AccountID` gegen den angemeldeten Account, wie bisher) — die
Zielvorgabe "Isolation auf Datenbankebene erzwungen" (Postgres Row-
Level Security) ist eine bewusst noch nicht getroffene Entscheidung,
siehe Offene Punkte unten.
---
## Fragebogen (Ebene 2, "Antrag stellen")
Adaptiv: Folgefragen erscheinen nur, wenn die Antwort sie auslöst.
Alltagssprache, keine Fachbegriffe — die stehen nur in der Ableitung.
**A — Vorhaben:** `titel`, `beschreibung` (Freitext: was soll die KI
tun?), `ergebnis` (Freitext: was soll herauskommen?), `haeufigkeit`
(einmalig/gelegentlich/täglich/automatisiert), `abteilung` (aus
Stammdaten).
**B — Daten** (bestimmt die Datenklasse). Mehrfachauswahl Ja/Nein/
Unsicher, "Unsicher" zählt wie "Ja": Namen/E-Mail/Personenangaben (B1),
Gesundheit/Religion/Herkunft/Gewerkschaft/Sexualleben (B2), Patienten-/
Mandanten-/Versichertendaten (B3), Bewerbungs-/Personal-/
Leistungsdaten Beschäftigter (B4), von Kunden anvertraute Daten (B5),
Konstruktions-/Kalkulations-/Vertragsunterlagen (B6), nur allgemein
zugängliche/erfundene Inhalte (B7).
**C — Verwendung des Ergebnisses** (bestimmt die KI-VO-Einstufung):
geht das Ergebnis unverändert nach außen (C1)? beeinflusst es eine
Entscheidung über einen Menschen (C2, Folgefrage: Einstellung/
Beförderung/Kündigung, Kreditwürdigkeit, Leistungsbewilligung, Zugang
zu Bildung, medizinische Beurteilung, keins davon)? erkennt/bewertet es
Emotionen, Verhalten oder biometrische Merkmale (C3)? läuft es ohne
menschliche Prüfung (C4)? merkt der Empfänger, dass es von einer KI
stammt (C5)?
**D — Werkzeug:** ggf. schon ein Werkzeug im Sinn (optional, Freitext),
Firmen- oder privater Zugang, Firmen- oder Privatgerät.
---
## Ableitungen (`rules/*.yaml`, `internal/rules`)
**Datenklasse** (`rules/datenklasse.yaml`) — höchste zutreffende Stufe
gewinnt: `oeffentlich` (B7) < `intern` (B6) < `auftragsdaten` (B5) <
`personenbezogen` (B1, B4) < `berufsgeheimnis` (B3, § 203 StGB) <
`besondere_kategorie` (B2, Art. 9 DSGVO). **Die Rangfolge ist eine
Annahme** (Spezifikation nennt keine expliziten Ränge) — siehe
`rules/OPEN.md`, vor Produktiveinsatz mit dem/der Datenschutz-
beauftragten bestätigen.
**KI-VO-Einstufung** (`rules/kivo_einstufung.yaml`) — Prüfreihenfolge,
erste zutreffende Stufe gewinnt: `verboten` (Art. 5 KI-VO: C3 mit
Emotionserkennung am Arbeitsplatz, Social Scoring oder biometrischer
Kategorisierung) → `hochrisiko` (C2 mit Beschäftigung/Kreditwürdigkeit/
Leistungsbewilligung/Bildung/Gesundheit) → `transparenzpflicht` (C1 ja
und C5 nein) → `minimal` (alles übrige). Die genauen Fragebogen-Felder
für die drei "verboten"-Varianten sind noch nicht abschließend
spezifiziert — siehe `rules/OPEN.md`.
**Anforderungsprofil** (`rules/anforderungen.yaml`) — aus Datenklasse
und/oder Einstufung abgeleitete Menge von Anforderungen an ein
Werkzeug: `avv_erforderlich`, `eu_verarbeitung`,
`kein_training_auf_eingabe`, `loeschfrist_max_tage`,
`menschliche_aufsicht`, `kennzeichnungspflicht`, `dsfa_erforderlich`.
`internal/rules` lädt und validiert diese drei Dateien (eindeutige IDs,
eindeutige Ränge, jede Anforderung braucht mindestens einen Auslöser)
und wertet sie seit Schritt 3 auch aus: `EvaluateDatenklasse`,
`EvaluateEinstufung`, `IstVerboten`, `DeriveAnforderungen` operieren auf
`rules.Antworten` (geparst aus `antrag.antworten` via `ParseAntworten`)
— reine, für sich getestete Funktionen ohne DB-/Web-Zugriff.
`FilterWerkzeuge`/`ErfuelltAnforderung` filtern einen Werkzeugkatalog
hart gegen die abgeleiteten Anforderungen (`WerkzeugEigenschaften` ist
ein schlanker, von `store.Werkzeug` unabhängiger Typ, damit
`internal/rules` weiterhin ohne `internal/store` auskommt). Seit
Schritt 4 wird das auch tatsächlich aufgerufen: `POST /antraege`
berechnet nach dem Einreichen sofort eine `bewertung` (append-only,
`internal/web/antrag_handlers.go:bewerteAntrag`) — bei `verboten`
(K.-o.-Prüfung) ohne Werkzeugsuche — und `GET /antraege/{id}` zeigt sie
inklusive Herleitung, RegelwerkVersion/KatalogVersion und dem
Pflicht-Hinweis "Das System entscheidet nicht. Es bereitet vor."
---
## Werkzeugkatalog (`werkzeug`, `werkzeug_sperre`)
Der eigentliche Wert des Produkts. Zentral gepflegt (Ebene 5,
`account_id IS NULL`), für alle Mandanten identisch sichtbar; jeder
Mandant kann zusätzlich eigene Einträge (`account_id` gesetzt) und
Sperrungen zentraler Einträge (`werkzeug_sperre`) führen, ohne den
zentralen Katalog zu verändern.
**Pflicht:** `letzte_pruefung` und `quelle` sind NOT NULL — jede
Zusicherung im Katalog (AVV verfügbar, Verarbeitungsort, Training-Opt-
out, Zertifizierungen) muss belegbar sein, sonst haftet Deklarix für
eine Aussage, die nicht nachweisbar ist. Einträge älter als 180 Tage
sollen im Ergebnis als "nicht aktuell geprüft" markiert werden (noch
nicht in der UI umgesetzt, siehe Offene Punkte).
`CurrentKatalogVersion` liefert eine reproduzierbare Kennung des
aktuellen Katalogzustands (Anzahl Einträge + letzte Änderung) — wird in
jeder Bewertung/Entscheidung eingefroren, damit im Audit nachvollziehbar
bleibt, mit welchem Katalogstand ein Vorschlag erzeugt wurde.
---
## Bewertungslogik (geplant, Schritt 3+)
1. **K.-o.-Prüfung.** Einstufung `verboten` → sofort Ablehnung (Art. 5
KI-VO), keine Werkzeugsuche.
2. **Harte Filter.** Werkzeuge aussortieren, die eine Pflichtanforderung
nicht erfüllen — Grund je Werkzeug festhalten, auch aussortierte
Werkzeuge werden im Ergebnis mit Begründung gezeigt.
3. **Zweckabgleich.** Aus `beschreibung`/`ergebnis` den Zweck ableiten
(Textgenerierung, Übersetzung, Code, Bild, Transkription, Recherche)
und gegen `geeignete_zwecke` filtern.
4. **Rangfolge.** Verbleibende Werkzeuge sortieren: EU-Verarbeitung,
Training standardmäßig aus, kurze Aufbewahrung, Aktualität der
Prüfung.
5. **Auflagen ableiten.** Statt Ablehnung Bedingungen erzeugen
(Pseudonymisierung, keine Klarnamen, Vier-Augen-Prüfung,
Kennzeichnung, Aufbewahrung begrenzen).
6. **Vorschlag erzeugen.** Empfehlung, Alternativen, Auflagen,
vollständige Herleitung.
Jede Bewertung wird als eigene Zeile gespeichert (append-only, wie
`finding`/`extraction` im alten Produkt) — eine neue Bewertung ersetzt
nicht die alte, sie ergänzt sie, damit Nachvollziehbarkeit auch bei
mehrfacher Neubewertung eines Antrags erhalten bleibt.
---
## Entscheidung, Register, Wiedervorlage (Schritt 5-7 erledigt)
**Schritt 5 (Entscheidung, Snapshot, Audit-Log) ist umgesetzt.** Ebene 3
(Fachebene) hat einen Posteingang (`GET /faelle`, alle offenen —
Status "eingereicht" — Anträge des Mandanten, nicht nur die eigenen)
und eine Entscheiden-Seite (`GET /faelle/{id}`, `POST
/faelle/{id}/entscheiden`, siehe `internal/web/fachebene_handlers.go`).
Rollen `verantwortlicher` und `pruefer` sehen dieselbe Sicht
(`requireFachebene`), nur `verantwortlicher` hat Entscheidungsrecht —
ein `pruefer`-POST auf `/entscheiden` bekommt 403 (er darf wissen, dass
der Fall existiert, nur nicht entscheiden; anders als bei
`requireFachebene`/`requireBetreiber`, die bei falscher Rolle 404
liefern, um die Existenz der Seite selbst zu verbergen).
Der/die Verantwortliche wählt: genehmigt / genehmigt mit Auflagen /
abgelehnt / Rückfrage. **Vorschlag-Vergleich:** `vorschlagFuer` leitet
aus der Bewertung ab, was das System vorschlagen würde (`abgelehnt` bei
`verboten`, sonst `genehmigt` wenn mindestens ein zulässiges Werkzeug
existiert, sonst `rueckfrage`) — weicht die tatsächliche Entscheidung
davon ab, ist das Begründungsfeld Pflicht (sonst 400). **Snapshot bei
Entscheidung:** `entscheidung.werkzeug_snapshot` (JSONB) friert den
vollständigen `store.Werkzeug`-Datensatz zum Entscheidungszeitpunkt ein
— ein späterer Katalog-Wandel darf nicht rückwirkend verändern, worauf
eine Entscheidung beruhte. Bei "genehmigt"/"genehmigt mit Auflagen" ist
ein Werkzeug aus der zulässigen Liste der Bewertung Pflicht (hart
geprüft, kein Override eines ausgeschlossenen Werkzeugs). `entscheidung`
ist append-only (Migration 0010) wie `bewertung`; jede Entscheidung
erzeugt zusätzlich einen `audit_log`-Eintrag (Action
`antrag_entschieden`).
Genehmigungen erhalten ein Ablaufdatum (`gueltig_bis`: 12 Monate, bei
`hochrisiko` 6 — `gueltigkeitFuer`).
**Schritt 7 (Wiedervorlage, Katalog-Benachrichtigung) ist umgesetzt**
(`internal/web/wiedervorlage_handlers.go`, `GET /wiedervorlage`, Ebene
3). Es gibt keine E-Mail-Infrastruktur — "Benachrichtigung" ist bewusst
eine In-App-Liste, kein Versand. Eine aktive Genehmigung erscheint dort,
wenn mindestens einer zutrifft: (1) `gueltig_bis` ist erreicht oder
liegt innerhalb von 30 Tagen, (2) das zugesagte Werkzeug wurde aus dem
Katalog entfernt, (3) `werkzeugDiff` erkennt eine Abweichung zwischen
dem eingefrorenen `werkzeug_snapshot` und dem aktuellen Katalogeintrag
bei AVV-Verfügbarkeit, Training-Standard oder Verarbeitungsort. Keine
gefundene Abweichung → die Genehmigung erscheint nicht (kein stiller
Blanko-Eintrag für jede Genehmigung). `store.ListAktiveGenehmigungenForAccount`
liefert dafür alle `genehmigt`/`genehmigt_mit_auflagen`-Entscheidungen
eines Mandanten (Join über `antrag.account_id`, da `entscheidung`
selbst kein `account_id` trägt).
**Schritt 6 (Registereintrag, Export) ist umgesetzt.** Jede Genehmigung
erzeugt automatisch einen `registereintrag` (append-only, Migration
0011): Zweck (`antrag.titel`), Abteilung, Werkzeug, Datenklasse,
Einstufung, Auflagen (die Anforderungen der Bewertung), Verantwortliche/r,
Entscheidungsdatum, Gültigkeit — siehe `internal/web/register_handlers.go`.
`GET /registereintraege` (Ebene 3, `verantwortlicher`/`pruefer`) zeigt
sie, `GET /registereintraege/export.{csv,pdf}` exportiert. Abgelehnte
Entscheidungen und Rückfragen erzeugen bewusst KEINEN Registereintrag —
das Register ist eine Genehmigungsliste, kein vollständiges Antragslog
(das leistet bereits `GET /faelle`).
---
## Onboarding
**Firma (Ebene 1, öffentlich, `POST /register`):** Registrierungsformular
mit Firmendaten → Testzugang sofort aktiv → Freischaltung des
Bezahlbetriebs nach Prüfung durch den Betreiber (letzteres noch nicht
gebaut). Beim Anlegen entstehen automatisch: Account, erster Nutzer mit
Rolle `admin` (siehe Rollentabelle — vereinfacht v1: nicht zusätzlich
separat als `verantwortlicher` angelegt, siehe Offene Punkte),
leere Abteilungsliste, Werkzeugkatalog aus der Plattform vorbelegt
(noch nicht automatisiert — der zentrale Katalog ist aktuell leer, bis
Ebene 5/Betreiber-Pflege gebaut ist).
**Mitarbeiter, geplante Reihenfolge:** (1) Einladungslink
(Sammellink, Selbstanmeldung, Abteilung beim ersten Antrag), (2) CSV-
Import, (3) SSO (Entra ID/Google Workspace), (4) Antrag ganz ohne
vorheriges Konto über Mandantenlink + E-Mail-Bestätigungscode (Konto
entsteht nebenbei). Keins davon ist gebaut — aktuell nur Firma-
Registrierung.
**Abrechnung:** nach vom Kunden angegebener Mitarbeiterzahl, **nicht**
nach angelegten Nutzern — sonst entsteht der Anreiz, Mitarbeitende
nicht anzulegen. Noch nicht gebaut (kein Abo-System).
---
## Frontend
Anspruch (2026-08-29 explizit vom Nutzer bestätigt, da Deklarix ein
buchbarer Service werden soll): **für Browser UND Mobile optimiert,
und jede Entität im Datenmodell muss über das Frontend steuerbar sein**
— eine reine Anzeige-Seite ohne CRUD ist ein Defizit, kein v1-Kompromiss.
Stack bleibt `html/template` + htmx, kein React/Vue/SPA (siehe
Baureihenfolge/Architektur-Entscheidungen) — die responsive Optik wird
rein mit CSS erreicht, ohne das Template-Modell zu ändern.
**Navigation** (`internal/web/templates/layout.html`, `"nav"`-Block,
einmal definiert und von jeder Seite eingebunden — Änderungen hier
wirken auf das ganze Produkt): ein Checkbox-Hack (versteckte Checkbox +
`<label for=...>` als Klick-Ziel) klappt auf dem Handy ein Menü auf,
ganz ohne JavaScript. **Nicht** das naheliegendere `<details>`/`<summary>`
verwenden — aktuelle Chromium-Versionen steuern dessen Auf-/Zuklapp-
Zustand über eine interne, mit CSS (auch nicht mit `!important`)
nicht überschreibbare Animation; ein Versuch, `.app-nav { display:
flex !important }` bei geschlossenem `<details>` zu erzwingen, bleibt
wirkungslos (mit Chromium-Headless-Screenshot verifiziert). Ab
960px Breite (`@media (min-width: 960px)`, `app.css`) wird exakt
dieselbe Markup-Struktur per CSS zu einer permanenten, dunklen
Seitenleiste (Farb-Tokens aus `design/enterprise.css` übernommen) —
kein separates Desktop-Template.
**Tabellen** (`table`/`th`/`td` in `app.css`) sind seit dieser Änderung
gestylt (vorher komplett ungestylt, betraf v. a. `register_liste.html`);
breite Tabellen stehen in einem `.table-scroll`-Wrapper (horizontales
Scrollen auf dem Handy statt gequetschter Spalten).
**Verifikationsmethode für CSS-Änderungen:** Live-Cookie-Auth per
Chromium-Headless/CDP ist im Sandbox-Environment nicht möglich (kein
websocket-Python-Modul) — stattdessen: Seite per `curl -b cookies.txt`
authentifiziert abrufen, `/static/...`-Pfade im HTML auf eine absolute
`http://localhost:<port>/static/...`-URL umschreiben (Assets brauchen
kein Auth-Cookie), dann `chromium --headless --disable-gpu --no-sandbox
--window-size=<W>,<H> --screenshot=out.png file:///pfad/zur/seite.html`
bei mobiler (z. B. 390×844) und Desktop-Breite (z. B. 1440×900)
screenshotten. Für einen Menü-Aufklapp-Zustand: `checked`-Attribut
manuell ins gespeicherte HTML einfügen vor dem Screenshot.
---
## Projektstruktur
```
/var/www/deklarix/
├── cmd/
│ └── deklarix/
│ └── main.go # Entry Point: Migration, Regelwerk laden, HTTP-Server
├── internal/
│ ├── auth/ # Passwort-Hashing, Session-Token (produktunabhängig)
│ ├── rules/ # Lädt/validiert Datenklasse-/Einstufungs-/Anforderungs-YAML
│ ├── store/ # Postgres, Migrationen
│ └── web/ # Handler, Templates
├── rules/ # YAML-Regelwerk + OPEN.md (offene fachliche Fragen)
├── design/
│ └── enterprise.css # enconf Ant-Design-Theme — Referenz für Design-Tokens
│ (Farben/Radius/Schatten), nicht direkt eingebunden
│ (Ant-Design-Klassennamen passen nicht zu Deklarix'
│ reinem HTML/htmx-Stack, siehe internal/web/static/app.css)
├── packaging/
│ └── DEBIAN/
│ └── control.tmpl # .deb Package-Control-Template
├── scripts/
│ ├── build.sh # Cross-Compile amd64 + arm64
│ ├── test.sh # Tests + vet + build-check
│ └── release.sh # Vollständiger Release-Prozess
├── go.mod
├── go.sum
└── CLAUDE.md
```
---
## Datenmodell
- `account` — ein Mandant (ein Unternehmen). Jeder Antrag gehört genau
einem Account.
- `app_user` — ein Login innerhalb eines Accounts. Rolle ist eine von
`mitarbeiter`, `verantwortlicher`, `pruefer`, `admin` (alle vier
Mandanten-bezogen) oder `betreiber` (plattformweit, Ebene 5).
- `session` — eine angemeldete Sitzung (Token, Ablaufzeit); echte
Tabelle statt zustandsloser signierter Tokens, damit Logout eine
Sitzung wirklich beendet.
- `audit_log` — Protokoll (append-only): jede Antragsänderung, jede
Bewertung, jede Entscheidung, jede Katalogänderung mit Zeitstempel
und Akteur.
- `abteilung` — Stammdaten für Fragebogen-Feld A.abteilung, je Mandant.
- `werkzeug` — Katalogeintrag (siehe oben); `account_id` NULL = zentral,
gesetzt = mandantenspezifische Ergänzung. NICHT append-only — der
Katalog wird gepflegt, eine Entscheidung friert den zu ihrem
Zeitpunkt gültigen Datensatz separat ein (geplant).
- `werkzeug_sperre` — Sperrung eines (auch zentralen) Katalogeintrags
durch einen einzelnen Mandanten.
- `antrag` — das Vorhaben aus Fragebogen-Abschnitt A plus die
vollständigen Antworten aus B/C/D als JSON (`antworten`, JSONB) — ein
starres Spaltenschema könnte den adaptiven Fragebogen nicht abbilden.
NICHT append-only (normale Zustandsänderung `entwurf`
`eingereicht``entschieden`, wie `submission` es im alten Produkt
war).
- `bewertung` — der berechnete Vorschlag (Schritt 4).
- `entscheidung` — die Entscheidung eines/einer Verantwortlichen über
einen Antrag, mit Snapshot des gewählten Werkzeugs (`werkzeug_snapshot`,
JSONB) und Ablaufdatum (`gueltig_bis`) bei Genehmigung (Schritt 5).
- `registereintrag` — automatisch bei jeder Genehmigung erzeugt
(Schritt 6), denormalisierter Nachweis (eigene Textspalten statt
Joins über bewertung/entscheidung/werkzeug).
**Append-only:** kein UPDATE auf `audit_log` (Trigger `forbid_update_delete`,
wiederverwendet aus dem alten Produkt), ebenso `bewertung`,
`entscheidung` und `registereintrag`.
---
## Baureihenfolge
1. ~~Datenmodell, Regelwerk als YAML, Katalogstruktur~~**erledigt**
(dieser Stand: `account`/`app_user`/`session`/`audit_log`/
`abteilung`/`werkzeug`/`werkzeug_sperre`/`antrag` in Postgres,
`internal/rules` lädt und validiert die drei YAML-Regelwerke,
Web-Layer kompiliert mit Auth + Plattform-Bereich-Gerüst).
2. ~~Fragebogen mit adaptiver Logik~~**erledigt** (`GET /antraege/neu`
einseitiges Formular, `POST /antraege` legt an und reicht direkt ein,
`GET /antraege` eigene Anträge, `GET /antraege/{id}` Detail. Adaptive
Folgefragen C2/C3 rein per CSS `:has()` ein-/ausgeblendet, kein
JavaScript. `antworten`-JSON nutzt exakt die Fakten-Schlüssel aus
`rules/*.yaml` (b1-b7, c1-c5, c2_folge, c3_art) — das hat auch die
OPEN.md-Frage zu den "verboten"-Fragebogen-Feldern final geklärt.)
3. ~~Ableitungen und harte Filter~~**erledigt** (`internal/rules`:
`EvaluateDatenklasse`/`EvaluateEinstufung`/`DeriveAnforderungen`/
`IstVerboten`/`FilterWerkzeuge`, vollständig getestet gegen die
echten `rules/*.yaml`-Dateien.)
4. ~~Ergebnisdarstellung mit Herleitung~~**erledigt** (`POST /antraege`
berechnet und speichert eine `bewertung` sofort beim Einreichen,
`GET /antraege/{id}` zeigt Datenklasse, KI-VO-Einstufung,
Anforderungen, zulässige/ausgeschlossene Werkzeuge — jeweils mit
Herleitung — sowie RegelwerkVersion/KatalogVersion und den
Pflicht-Hinweis "Das System entscheidet nicht. Es bereitet vor.".
`bewertung` ist append-only. Getestet inkl. K.-o.-Prüfung
(`verboten` überspringt die Werkzeugsuche) und hartem Filter
gegen den Katalog.)
5. ~~Entscheidung, Snapshot, Audit-Log~~**erledigt** (Ebene 3:
`GET /faelle` Posteingang, `GET /faelle/{id}` + `POST
/faelle/{id}/entscheiden`. `verantwortlicher` entscheidet,
`pruefer` sieht dieselbe Seite ohne Entscheidungsrecht [403 bei
Entscheidungsversuch]. Begründung ist Pflicht bei Abweichung vom
abgeleiteten Vorschlag (`vorschlagFuer`), Genehmigung friert den
vollständigen Werkzeugdatensatz ein und braucht ein zulässiges
Werkzeug aus der Bewertung, `entscheidung` ist append-only, jede
Entscheidung erzeugt einen `audit_log`-Eintrag.)
6. ~~Registereintrag und Export~~**erledigt** (`internal/web/register_handlers.go`:
`GET /registereintraege` zeigt alle Registereinträge des Mandanten,
`GET /registereintraege/export.{csv,pdf}` exportiert sie. Jede
Genehmigung — `genehmigt` oder `genehmigt_mit_auflagen` — erzeugt in
`handleFallEntscheiden` automatisch einen `registereintrag`
[append-only, Migration 0011]: Zweck [`antrag.titel`], Abteilung,
Werkzeug, Datenklasse, Einstufung, Auflagen [die Anforderungen der
Bewertung], Verantwortliche/r, Entscheidungsdatum, Gültigkeit —
bewusst denormalisiert [eigene Textspalten statt Joins], damit ein
Eintrag nicht von einem späteren Katalog-/Abteilungs-Wandel abhängt.
Abgelehnte Entscheidungen und Rückfragen erzeugen keinen Eintrag.
Der PDF-Export nutzt das bereits vorhandene, bis dahin ungenutzte
`go-pdf/fpdf` [keine neue Abhängigkeit] mit `UnicodeTranslatorFromDescriptor`
für deutsche Umlaute.)
7. ~~Wiedervorlage und Katalog-Benachrichtigung~~**erledigt**
(`GET /wiedervorlage`: abgelaufene/bald ablaufende Genehmigungen und
Genehmigungen, deren Werkzeug sich seither im Katalog geändert hat
oder entfernt wurde. In-App-Liste, kein E-Mail-Versand.)
Nicht bauen (v1): automatische Genehmigung ohne Mensch, Erkennung
tatsächlicher Werkzeug-Nutzung, Mitarbeiterüberwachung (nichts, was
Nutzung einzelner Personen auswertet), konfigurierbare Rollen mit
eigener Oberfläche, Konzernstrukturen mit Vererbung, mandantenspezifische
Regelwerke, Workflow-Designer, SAML, Schnittstellen zu Fremdsystemen.
---
## Go Commands
```bash
export PATH=$PATH:/usr/local/go/bin # Immer setzen!
# Entwickeln
go run ./cmd/deklarix/
# Tests
./scripts/test.sh
# oder direkt:
go test -race ./...
go vet ./...
# Build (amd64 + arm64)
./scripts/build.sh 1.0.0
# Build (nur amd64)
./scripts/build.sh 1.0.0 amd64
```
---
## Build & Release-Prozess
### Versioning (Semantic Versioning: MAJOR.MINOR.PATCH)
- **MAJOR** — Breaking changes, API-Inkompatibilitäten
- **MINOR** — Neue Features, rückwärtskompatibel
- **PATCH** — Bugfixes
### Release-Schritte
```bash
# 1. Alle Änderungen committen
git add -p && git commit -m "feat: ..."
# 2. Release-Skript (macht Tests → .deb-Build → Gitea-Upload → Tag → Push)
./scripts/release.sh 1.2.0
# Danach liegt in dist/:
# deklarix_1.2.0_amd64.deb
# deklarix_1.2.0_arm64.deb
```
### Was das Release-Skript tut
1. Prüft: sauberer Git-Status (keine uncommitted changes)
2. Führt `./scripts/test.sh` aus (vet + race tests + build-check, braucht `DATABASE_URL`)
3. Baut `.deb`-Pakete für `linux/amd64` und `linux/arm64` (`scripts/build.sh`)
4. Lädt die Pakete in Giteas Debian-Paketregistrierung hoch (Kanal `testing`,
Suites `bookworm` + `trixie`) — siehe „Deployment“ unten
5. Setzt Git-Tag `v<version>` mit Annotierung
6. Pusht `main` + Tag nach `origin`
### Deployment (apt, Infrastruktur-Standard wie bei enconf)
Updates laufen ausschließlich über ein apt-Paket, nie über manuelles
Kopieren eines Binaries. `.deb`-Pakete landen in Giteas eingebauter
Debian-Paketregistrierung unter der Gitea-Org `projekte` (gleicher
Mechanismus wie beim Schwesterprojekt enconf-webpanel). Zielserver
richten sich einmalig ein:
```bash
sudo mkdir -p /etc/apt/keyrings
sudo curl -fsSL https://git.netcell-it.de/api/packages/projekte/debian/repository.key \
-o /etc/apt/keyrings/deklarix-gitea.asc
echo "deb [signed-by=/etc/apt/keyrings/deklarix-gitea.asc] https://git.netcell-it.de/api/packages/projekte/debian $(lsb_release -cs) testing" \
| sudo tee /etc/apt/sources.list.d/deklarix.list
sudo apt-get update && sudo apt-get install deklarix
```
Danach: `apt upgrade` für Updates, `systemctl {start,stop,status} deklarix`
für den Dienst. Konfiguration liegt in `/etc/deklarix/deklarix.env`
(aus `deklarix.env.example` beim Erstinstall kopiert, `DATABASE_URL` ist
dort absichtlich auskommentiert — der Dienst startet erst, wenn sie
gesetzt ist; `postinst` startet ihn sonst bewusst nicht, um keine
Crash-Restart-Schleife gegen einen Platzhalter-Host zu erzeugen).
Aktuell gibt es nur den Kanal `testing` (kein `stable` — es gibt noch
keine Kunden, für die eine Trennung nötig wäre).
---
## Testing-Pattern & Qualitätssicherung
```go
// Datei: internal/web/health_test.go
package web_test
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestHealth(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/health", nil)
w := httptest.NewRecorder()
HealthHandler(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w.Code)
}
}
```
- Tests liegen neben dem Code: `paket/foo_test.go`
- Package: `package foo_test` (Black-Box-Test) oder `package foo` (White-Box)
- Race-Detector immer an: `go test -race ./...`
- Tabellenbasierte Tests für mehrere Inputs
**DATABASE_URL und Testsystem.** `internal/store` hat Integrationstests
gegen echtes Postgres (u. a. die Append-only-Garantie). Ohne
`DATABASE_URL` überspringt `go test` diese Tests einfach — praktisch für
schnelle lokale Iteration ohne lokales Postgres. `./scripts/test.sh`
(und damit `release.sh`) ist strenger: es bricht ohne `DATABASE_URL` mit
Fehler ab, statt die Append-only-Garantie still zu überspringen. Es
zieht dafür aber **kein** eigenes Postgres hoch (kein Docker-Zwang auf
dem lokalen Dev-Rechner) — das ist Aufgabe des dedizierten Testsystems,
dort muss `DATABASE_URL` vorkonfiguriert sein.
---
## Leitplanken, die dauerhaft gelten
- **Keine Rechtsdienstleistung.** Deklarix bereitet eine Einstufung vor,
entscheidet nicht. Keine Formulierung darf eine individuelle
rechtliche Bewertung suggerieren oder Erfolgsaussichten behaupten.
- **Keine erfundenen Einstufungen.** Jede Ableitungsregel steht in einer
Regel-YAML und wurde dort von Hand geprüft; offene fachliche Fragen
stehen in `rules/OPEN.md`, werden nicht geraten.
- **Werkzeugkatalog-Zusicherungen müssen belegbar sein.** `quelle` und
`letzte_pruefung` sind Pflichtfelder, keine Ausnahme.
- **Keine Mitarbeiterüberwachung.** Nichts im Produkt darf auswerten,
welche einzelne Person welches Werkzeug wie oft nutzt — das
widerspricht dem Vertrauensmodell, auf dem die freiwillige
Antragstellung beruht.
- **DSGVO und Hosting in der EU.**
---
## Wichtige Hinweise
### Go PATH
```bash
# Immer setzen — ist nicht im Standard-PATH des Servers
export PATH=$PATH:/usr/local/go/bin
```
### Git Push
```bash
git push origin main
# Remote: https://git.netcell-it.de/projekte/deklarix.git
```
### Server-Prozess
```bash
# Dienst läuft über systemd (installiert per apt, siehe Deployment oben)
sudo systemctl start deklarix
sudo systemctl status deklarix
# Config: /etc/deklarix/deklarix.env (DATABASE_URL, PORT, RULES_DIR)
# Logs prüfen
journalctl -u deklarix -f
```
### Bekannte Server
| Rolle | Host | Zugang |
|---|---|---|
| Testsystem + `app.deklarix.de` (DB-Integrationstests, apt-Verifikation, nginx-Reverse-Proxy + Let's-Encrypt-TLS auf den lokalen Dienst) | 89.163.205.4 | SSH `noroot`, sudo passwortlos |
| Installer-Endpoint (`get.deklarix.com`) | 89.163.205.110 | noch nicht eingerichtet |
| Marketing-Site (`deklarix.de`/`.com`) | 89.163.205.80 | noch nicht eingerichtet |
---
## Arbeitsweise für Claude Code
- Kleine Commits, ein Thema pro Commit
- Erst Test, dann Implementierung
- Keine neue Abhängigkeit ohne Rückfrage
- Fehler nicht verschlucken, keine stillen Fallbacks
- Bei fachlicher Unsicherheit: nicht raten, sondern als offene Frage in
`rules/OPEN.md` notieren
## Vor Änderungen
1. `go vet ./...` — keine Fehler
2. `./scripts/test.sh` — alle Tests grün
3. Bestehenden Code lesen — nicht raten
## Nach Änderungen
1. `./scripts/test.sh` → 0 Fehler
2. `./scripts/build.sh <version>` → erfolgreich
3. Commit mit semantischer Message: `feat:`, `fix:`, `refactor:`, `docs:`
4. Bei Release: `./scripts/release.sh <version>`
---
## Offene Punkte
- **Rangfolge der Datenklassen und Grenzfälle im Anforderungsprofil**
(siehe `rules/OPEN.md`, Punkte 1/2/5/6) — mit dem/der fachlich
Verantwortlichen (z. B. Datenschutzbeauftragte/r) bestätigen. Schritt
3 ist trotzdem schon umgesetzt (auf Basis dieser dokumentierten
Annahmen) — nicht auf die Klärung gewartet, um nicht blockiert zu
bleiben, aber die Ableitung kann sich noch ändern.
- ~~Genaue Fragebogen-Felder für die drei "verboten"-Varianten~~ —
**erledigt in Schritt 2**, C3 hat jetzt eine Folgefrage `c3_art`.
Weiterhin offen: ob die drei Varianten fachlich vollständig sind
(siehe `rules/OPEN.md`, Punkt 3).
- **Löschfristen je Datenklasse** (konkrete Tageswerte für
`loeschfrist_max_tage`) — noch nicht fachlich festgelegt.
- **Mandantenisolation auf Datenbankebene (Postgres Row-Level Security)**
— die Frontend-Spezifikation fordert das explizit ("nicht in der
Anwendungslogik"), aktuell ist Isolation ausschließlich
anwendungsseitig (Handler-Vergleich `AccountID`). Umstellung auf RLS
ist ein bewusst noch nicht getroffener Architektur-Entscheid —
Aufwand und Zeitpunkt mit dem Nutzer klären, bevor mehr Tabellen
entstehen, die sonst nachträglich migriert werden müssten.
- **"Admin und KI-Verantwortlicher" beim Firma-Onboarding** — die
Spezifikation will, dass der erste Nutzer beide Rollen gleichzeitig
hat; `app_user.role` ist aktuell ein einzelner Wert. Muss geklärt
werden: zwei Rollen pro Nutzer zulassen (Datenmodell-Änderung) oder
zwei `app_user`-Zeilen für dieselbe Person?
- **Zentraler Werkzeugkatalog muss noch befüllt werden** — die
Ebene-5/Betreiber-UI zur Katalogpflege ist jetzt gebaut (`GET
/betreiber/werkzeuge` Liste, `GET/POST /betreiber/werkzeuge/neu`
anlegen, `GET/POST /betreiber/werkzeuge/{id}` bearbeiten, `POST
.../loeschen` löschen — siehe `internal/web/betreiber_werkzeug_handlers.go`.
Bearbeitet ausschließlich `account_id IS NULL`-Einträge, ein
mandantenspezifischer Eintrag ist über diese Seiten nicht erreichbar
[404]). "Werkzeugkatalog aus der Plattform vorbelegt" beim Firma-
Onboarding bleibt trotzdem nicht automatisiert — der Katalog ist
inhaltlich weiterhin leer, bis der Betreiber tatsächlich Einträge
pflegt.
- Markenanmeldung Deklarix, sobald erster Kunde zahlt.