# 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 (die zwölf Zweck-Kategorien erkennen, siehe `gueltigeZwecke` weiter unten), 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ß. **Praktische Umsetzung dieser Doppelrolle:** `admin` bekommt zusätzlich dieselben Fachebene-Rechte wie `verantwortlicher` (siehe `requireFachebene`/`hatEntscheidungsrecht`) — pragmatisch gelöst ohne Datenmodell-Umbau, siehe Offene Punkte weiter unten. 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). **Support-Login (Ebene 5, `betreiber`) ist umgesetzt** — auf `GET /betreiber/accounts/{id}` kann sich der Betreiber über `POST .../nutzer/{userID}/anmelden-als` als ein aktiver Kunden-Nutzer anmelden, ohne dessen Passwort zu kennen (`handleBetreiberLoginAls` in `betreiber_handlers.go`, `store.CreateImpersonatedSession`, Migration 0014). Nicht für deaktivierte Nutzer möglich. Die neue Sitzung ersetzt die eigene Betreiber-Sitzung (kein Sitzungs-Stack/"Zurück zum Betreiber" — der Betreiber meldet sich danach mit den eigenen Zugangsdaten neu an). Jede Nutzung erzeugt einen `audit_log`-Eintrag (Action `betreiber_login_als_nutzer`); während der gesamten Sitzung zeigt die Nav einen auffälligen Banner ("Support-Zugriff durch ...", siehe `currentImpersonator` in `middleware.go`), damit nie unklar ist, im Kontext eines fremden Kontos zu handeln. **Firmen-CRUD für den Betreiber ist umgesetzt:** `GET/POST /betreiber/accounts/neu` legt eine Firma samt erstem `admin`-Login an (dasselbe Ergebnis wie die öffentliche Registrierung, nur vom Betreiber ausgelöst — z. B. für vertriebsunterstütztes Onboarding oder Testkonten), `POST /betreiber/accounts/{id}/umbenennen` korrigiert den Firmennamen (`store.UpdateAccount`). Jede Firmenanlage erzeugt einen `audit_log`-Eintrag. Bewusst **kein** Löschen — ein Hard-Delete würde gegen die Fremdschlüssel aus `antrag`/`app_user`/`audit_log` laufen und Historie zerstören (dasselbe Muster wie bei Nutzern: deaktivieren statt löschen, aber ein Sperren/Deaktivieren-Zustand für Accounts existiert noch nicht, siehe unten). **Weiterhin nicht gebaut:** Anmeldeverfahren-Konfiguration, das Abo-System. **Wichtige Korrektur (2026-08-31):** die eigentliche Zahlungsabwicklung/Rechnungsstellung läuft über ein **anderes, externes System** — Deklarix baut dafür **kein Stripe/Zahlungs-SDK**. Deklarix bildet nur den **Subskriptionsstatus** ab (Plan, gebuchte Mitarbeiterzahl, Status wie `test`/`aktiv`/`gekündigt`, Ablaufdatum), der von außen gesetzt wird — noch offen, wie genau ("von außen" heißt aktuell: manuell durch den Betreiber, eine Schnittstelle zum externen Abrechnungssystem ist nicht spezifiziert). Das Preismodell selbst (3 €/Mitarbeiter/Monat, Mindestabnahme 10 Mitarbeiter, 14 Tage Testphase) bleibt als Orientierung gültig, nur der Stripe-Teil der Empfehlung vom 2026-08-29 entfällt. Sperren/Deaktivieren eines Accounts durch den Betreiber hängt weiter an dieser (jetzt kleineren) Subskriptions-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, Verarbeitungsländer, Training-Opt- out, Zertifizierungen) muss belegbar sein, sonst haftet Deklarix für eine Aussage, die nicht nachweisbar ist. Beide Katalogansichten zeigen eine dreistufige Ampel zum Prüfalter (`pruefStatus()` in `betreiber_werkzeug_handlers.go`, CSS-Klassen `status-niedrig`/ `-mittel`/`-hoch`, dieselben Farbtoken wie die KI-VO-Risikoampel): **aktuell** (grün, < 150 Tage), **bald fällig** (gelb, 150–180 Tage — dasselbe 30-Tage-Vorwarnfenster wie bei der Wiedervorlage), **abgelaufen** (rot, > 180 Tage). `aufbewahrung_tage` ist NULLABLE (Migration 0015) — `NULL` bedeutet "vom Anbieter nicht beziffert" und ist bewusst von `0` ("belegte Zusicherung: sofortige Löschung", z. B. DeepL) unterschieden. Vorher war die Spalte `NOT NULL DEFAULT 0`, wodurch beide Fälle identisch aussahen — `formatAufbewahrung()` zeigt entsprechend "unbekannt" statt "0 Tage". `geeignete_zwecke` ist seit derselben Migration ein kontrolliertes Vokabular statt Freitext: das Formular zeigt Checkboxen statt eines Freitextfelds (`gueltigeZwecke` in `betreiber_werkzeug_handlers.go`, serverseitig validiert via `istGueltigerZweck`). Grund: ein künftiger automatischer Zweckabgleich (Bewertungslogik Schritt 3, weiterhin nicht gebaut) würde an inkonsistentem Freitext ("Text" statt "Textgenerierung") scheitern. Am 2026-08-31 von sechs auf zwölf Kategorien erweitert, nachdem der Nutzer zurecht einwandte, dass sechs Kategorien die tatsächliche Bandbreite an Einsatzgebieten nicht abdecken: Textgenerierung, Übersetzung, Code, Bild, Transkription, Recherche, **Datenanalyse, Video, Audio/Sprache, Kundenservice/Chatbot, Präsentation/Design, Automatisierung/Agenten**. Es gibt keinen einheitlichen Branchenstandard für KI-Funktions- kategorien (regulatorische Taxonomien wie die KI-VO selbst oder OECD/ NIST klassifizieren nach Risiko, nicht nach Funktion; Marktplatz- Kategorien wie bei G2 sind Marketing, kein Standard) — diese zwölf sind Deklarix' eigene, bewusst begrenzte Liste für den künftigen Zweckabgleich, keine allgemeine KI-Taxonomie. Neues Feld `subprozessoren` (TEXT[], Migration 0015) macht Unterauftragsverarbeiter (z. B. Anthropic bei Microsoft 365 Copilot, AWS bei DeepL) als eigenes, durchsuchbares Feld sichtbar statt nur als Freitext in `einschraenkungen` — das ist oft der Grund, warum ein scheinbar EU-verarbeitetes Werkzeug die USA doch nicht ausschließt. Sowohl der zentrale Katalog (`GET /betreiber/werkzeuge`) als auch die mandantenseitige Sicht (`GET /verwaltung/werkzeuge`) zeigen den Katalog als Tabelle (Name, Anbieter, Verarbeitungsländer, DPF, AVV, Training aus, Aufbewahrung, Zertifizierungen, Geeignete Zwecke, Zuletzt geprüft) statt als Liste — vorher waren nur Name/Anbieter/Ort auf einen Blick sichtbar, der Rest war erst nach Klick auf den Detaileintrag zu sehen. Der zentrale Katalog ist aktuell mit 19 recherchierten Einträgen befüllt (u. a. ChatGPT Enterprise, Microsoft 365 Copilot, Claude for Work, GitHub Copilot Business, DeepL Pro/API, Google Gemini für Google Workspace, Le Chat Enterprise, Perplexity Enterprise Pro, Adobe Firefly Enterprise, Otter.ai Business, Notion AI, Grammarly Business, Midjourney, Julius AI, Synthesia, ElevenLabs, Zendesk AI, Canva Magic Studio, Zapier) — deckt damit alle zwölf Zweck-Kategorien mit mindestens einem Eintrag ab. Jede Zusicherung ist mit einer echten, offiziellen Anbieter-Quelle belegt (Stand 2026-08-31), nicht geraten — und zwar auch dann, wenn keine Quelle eine positive Zusicherung hergibt: Synthesia und Zapier haben `training_standard`/ `training_opt_out` bewusst NICHT gesetzt, weil keine belegte Aussage zum Trainingsausschluss gefunden wurde (nicht mit "trainiert nachweislich" verwechseln — es ist ein "wissen wir nicht, deshalb keine Haken"). Mehrere Einträge sind bewusst mit kritischen Einschränkungen versehen statt schöngefärbt, das ist die vorgesehene Funktion des Katalogs: Adobe Firefly (Trainingsausschluss ist nicht Standard), Otter.ai (Training auf de-identifizierten Aufnahmen, laufende Rechtsstreitigkeiten), **Midjourney (kein AVV, keine EU-Datenresidenz, unbegrenzte Speicherung inkl. Training — laut CLAUDE.md-Leitplanken für personenbezogene/vertrauliche Daten nicht geeignet, absichtlich als Negativbeispiel im Katalog, nicht weggelassen)**. **Schema-Lücke behoben (2026-08-31, Migration 0016):** der grobe `verarbeitungsort`-Eimer (`EU`/`USA`/`gemischt`/`on-prem`) wurde durch `verarbeitungslaender TEXT[]` ersetzt — echte Länder statt Buckets. Grund: "USA" war schon immer ein Drittland im Sinne der DSGVO (Art. 44 ff.) wie jedes andere auch, das Wort "Drittland" für einen fünften Bucket-Wert wäre also verwirrend gewesen, nicht erhellend. Das Formular bietet dafür eine echte Weltländerliste (`weltLaenderliste` in `laenderliste.go`, `` ohne Gruppierung — bei einem Formular mit über einem Dutzend Feldern (Werkzeug anlegen) wirkte das "klobig, einfach nur untereinander" (O-Ton Nutzer). Jetzt: `.form-card` (weiße Karte mit Rahmen/Schatten, Ant-Design-Tokens aus `design/enterprise.css` übernommen) enthält mehrere `
` (thematische Abschnitte mit Titel, per `border-top` getrennt), darin `.form-grid` (2 Spalten ab 640px für kurze Felder wie Name/Anbieter nebeneinander, `.form-full` erzwingt volle Breite für lange Felder). Checkbox-Gruppen mit überschaubar vielen Optionen (Zusicherungen, die zwölf Zwecke) sind `.chip-group` — abgerundete Pillen statt einer langen Untereinander-Liste, ausgewählte Chips werden über `:has(input:checked)` blau hervorgehoben (kein JavaScript). Ein Löschen-Button steht nicht mehr einfach unter dem Formular, sondern in einer abgesetzten `.form-gefahrenzone`-Fußzeile der Karte. Dieselben Klassen sind bewusst generisch (nicht an ein Formular gebunden) — noch nicht auf andere Formulare (Login/Registrierung, Fragebogen, übrige Admin-Formulare) angewendet, das wäre der nächste Schritt, falls gewünscht. Beim Umbau auffällig gewordener, vorbestehender Bug: das Lösch-Formular in `betreiber_werkzeug_form.html` war fest auf `/betreiber/werkzeuge/{id}/loeschen` verdrahtet, obwohl dieselbe Vorlage auch vom Mandanten-Formular (`/verwaltung/werkzeuge/...`) genutzt wird — ein Mandant hätte beim Löschen eines eigenen Werkzeugs 403 bekommen. Jetzt `{{.ActionBase}}/{{.ID}}/loeschen`, wie die Formular-`action` selbst. **Typografie-Nachbesserung (2026-08-31):** Nutzer-Feedback nach dem ersten Formular-Redesign: Überschriften wirkten zu klein und zu dicht am Rand. Ursache für Letzteres: `` in `
` positioniert sich per Spec an der Border-Box des Fieldsets, nicht an dessen Padding-Box — ohne eigenes Padding sitzt die Abschnittsüberschrift optisch enger am Kartenrand als der übrige, per `.form-section { padding: 20px }` eingerückte Inhalt. `.form-section > legend` hat jetzt selbst `padding: 0 0 16px 0` und eine größere Schrift (1.0625rem/700 statt 0.9375rem/600). Gleichzeitig `h2` (1.25rem) und `h3` (1.0625rem) erstmals explizit gesetzt (vorher reiner Browser-Default) und `.fragebogen legend` auf dieselbe Gewichtsstufe wie die Formular-Abschnitte angehoben (600/1rem statt 500/0.9375rem) — für eine einheitliche Überschriften-Hierarchie statt Zufallsgrößen je nach UA-Stylesheet. **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:/static/...`-URL umschreiben (Assets brauchen kein Auth-Cookie), dann `chromium --headless --disable-gpu --no-sandbox --window-size=, --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` 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 ` → erfolgreich 3. Commit mit semantischer Message: `feat:`, `fix:`, `refactor:`, `docs:` 4. Bei Release: `./scripts/release.sh ` --- ## Offene Punkte - ~~Rangfolge der Datenklassen~~ (`rules/OPEN.md`, Punkt 1) — **am 2026-08-29 vom Produktverantwortlichen bestätigt**, keine Änderung nötig. - ~~Anforderung `kein_training_auf_eingabe` und `auftragsdaten`~~ (`rules/OPEN.md`, Punkt 2) — **bestätigt**, `auftragsdaten` bleibt eingeschlossen. - ~~Genaue Fragebogen-Felder für die drei "verboten"-Varianten~~ — **erledigt in Schritt 2**, C3 hat jetzt eine Folgefrage `c3_art`. Ob die drei Varianten fachlich vollständig sind, wurde am 2026-08-29 ebenfalls **bestätigt** (siehe `rules/OPEN.md`, Punkt 3). - ~~Fallback ohne zutreffende Datenklasse~~ (`rules/OPEN.md`, Punkt 5) — **bestätigt**, `intern` bleibt die konservative Standardannahme. - **Welche Anforderungen hart gegen ein Werkzeug gefiltert werden** (`rules/OPEN.md`, Punkt 6 — `avv_erforderlich`/`eu_verarbeitung`/ `kein_training_auf_eingabe` hart, `menschliche_aufsicht`/ `kennzeichnungspflicht`/`dsfa_erforderlich` nur als Auflage vermerkt) — weiterhin nicht ausdrücklich bestätigt, aber plausibel, keine Rückmeldung dazu bisher eingeholt. - **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~~ — **pragmatisch gelöst, kein Datenmodell-Umbau:** `app_user.role` bleibt ein einzelner Wert (kein `roles`-Array, keine zwei `app_user`-Zeilen pro Person). Stattdessen bekommt die Rolle `admin` in `requireFachebene` (Ebene 3) dieselben Rechte wie `verantwortlicher` — sieht Posteingang/Register/ Wiedervorlage und darf entscheiden (`hatEntscheidungsrecht` in `fachebene_handlers.go`). Grund: ohne das könnte eine frisch registrierte Firma mit nur einem `admin`-Login keinen einzigen eingereichten Antrag sehen oder bearbeiten — der Kernablauf wäre für Einzelpersonen-/Kleinfirmen-Onboarding komplett blockiert. `pruefer` bleibt unverändert nur lesend, ohne Entscheidungsrecht. Kompromiss statt "sauberer" Lösung — falls künftig eine Firma admin und verantwortlicher bewusst auf zwei verschiedene Personen verteilen will, funktioniert das weiterhin unverändert (zwei separate Logins mit den jeweiligen Rollen). - **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.