GET /wiedervorlage (Ebene 3) zeigt aktive Genehmigungen, die erneut geprüft werden sollten: Ablaufdatum erreicht oder innerhalb von 30 Tagen, das zugesagte Werkzeug wurde aus dem Katalog entfernt, oder der aktuelle Katalogeintrag weicht bei AVV/Training-Standard/ Verarbeitungsort vom zum Entscheidungszeitpunkt eingefrorenen Snapshot ab. Keine E-Mail-Infrastruktur vorhanden — Benachrichtigung ist bewusst eine In-App-Liste. Damit ist die ursprüngliche Baureihenfolge (Schritt 1-7) vollständig umgesetzt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
702 lines
33 KiB
Markdown
702 lines
33 KiB
Markdown
# 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** (`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`).
|
|
|
|
**Weiterhin nicht gebaut:** Anmeldeverfahren-Konfiguration, eigene
|
|
Werkzeug-EINTRÄGE eines Mandanten (nur Sperrungen zentraler Einträge
|
|
sind umgesetzt, `account_id`-gesetzte eigene Katalogeinträge fehlen
|
|
noch), 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).
|
|
|
|
---
|
|
|
|
## 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 # Design-System (enconf-Basis, ungenutzt seit Pivot)
|
|
├── 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.
|