Jede neue Firma (Registrierung + Betreiber-Firmenanlage) bekommt jetzt automatisch vier leere Genehmiger-Rollen mit erklärender Beschreibung (Datenschutzbeauftragter, Geschäftsführer, KI-Manager, CISO) - Admin muss nur noch Personen zuordnen statt bei null anzufangen. Welche Bedingung welche Rolle tatsächlich auslöst, bleibt weiterhin komplett konfigurierbar pro Firma (Migration 0018 fügt genehmiger_rolle.beschreibung als reines Freitext-Orientierungsfeld hinzu, keine feste fachliche Bindung). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1231 lines
64 KiB
Markdown
1231 lines
64 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 (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.
|
||
|
||
**Filter & Paginierung** (seit 2026-08-31, `pagination.go` +
|
||
`werkzeugFilter` in `betreiber_werkzeug_handlers.go`): beide
|
||
Katalog-Tabellen haben eine Filterleiste (Freitext über Name/Anbieter/
|
||
Verarbeitungsländer, Dropdown über die zwölf Zweck-Kategorien) und
|
||
serverseitige Paginierung (20 Einträge/Seite, `page`-Query-Parameter).
|
||
Alles über ein einziges GET-Formular, ohne JavaScript — Prev/Next-
|
||
Buttons setzen `page` per `<button type="submit" name="page"
|
||
value="...">`, damit q/zweck beim Blättern erhalten bleiben. Auf der
|
||
Mandanten-Seite (die pro Zeile zusätzlich Sperren/Entsperren-Formulare
|
||
hat) sind Filter und Paginierungs-Buttons bewusst in getrennte
|
||
`<form>`-Elemente aufgeteilt statt in eines um die ganze Tabelle —
|
||
verschachtelte `<form>`-Elemente sind ungültiges HTML, die
|
||
Paginierungs-Buttons tragen `q`/`zweck` deshalb als Hidden-Felder
|
||
nach. `pagination.go` ist bewusst generisch (Go-Generics,
|
||
`paginate[T any]`) für Wiederverwendung in weiteren Listen (Register,
|
||
Fälle, ...) — bisher nur auf den Werkzeugkatalog angewendet, das war
|
||
die von den beiden am dringendsten wachsende Tabelle.
|
||
|
||
Der zentrale Katalog ist aktuell mit 20 recherchierten Einträgen
|
||
befüllt (u. a. ChatGPT Enterprise, Microsoft 365 Copilot, Claude for
|
||
Work, Claude Code, 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. **Claude Code ist bewusst ein eigener
|
||
Eintrag neben Claude for Work**, kein Duplikat: es läuft lokal auf
|
||
Entwicklerrechnern mit direktem Zugriff auf Dateien/Codebasen und
|
||
speichert Sitzungsprotokolle lokal im Klartext (bis zu 30 Tage,
|
||
konfigurierbar) — ein zusätzliches lokales Aufbewahrungsrisiko, das bei
|
||
einer reinen Web-Chat-Anwendung wie Claude for Work nicht existiert,
|
||
obwohl beide über dieselben Commercial Terms/DPA laufen. 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`, `<select multiple>`, Mehrfachauswahl per Strg/Cmd-
|
||
Klick — kein JavaScript nötig) plus zwei Pseudo-Einträge ("EU-Region
|
||
(kein einzelnes Land benannt)" für Anbieter, die nur eine Region statt
|
||
eines Staates zusichern, z. B. Microsofts EU Data Boundary; "On-Premise
|
||
(selbst gehostet)"). Der harte Filter `eu_verarbeitung`
|
||
(`internal/rules/evaluate.go`, `alleLaenderInEUEWR`) verlangt jetzt,
|
||
dass ALLE genannten Länder EU/EWR-Mitgliedstaaten sind — eine leere
|
||
Liste gilt als nicht erfüllt, nicht als Freifahrtschein. Zusätzlich neues
|
||
Feld `dpf_zertifiziert` (Checkbox "EU-US Data Privacy Framework
|
||
zertifiziert"): DPF ist eine schmalere, gerichtlich schon zweimal
|
||
gekippte Rechtsgrundlage (Safe Harbor, Privacy Shield) für USA-Transfers,
|
||
bisher nur als Fließtext in "Einschränkungen" erwähnt, jetzt strukturiert
|
||
und damit künftig filterbar. Alle 19 Katalogeinträge wurden mit den
|
||
tatsächlichen, bereits recherchierten Ländern nachgepflegt (z. B.
|
||
Mistral → Frankreich, DeepL → Deutschland, Synthesia → Irland, Claude
|
||
for Work → USA) statt der pauschalen alten Buckets.
|
||
|
||
Das öffnet jetzt auch DeepSeek (Verarbeitung in China) für einen
|
||
künftigen Katalogeintrag — die Sperre lag ausschließlich am alten
|
||
Wertebereich, nicht an einer fachlichen Entscheidung.
|
||
|
||
**Offen:** `dpf_zertifiziert` ist bei allen 19 Einträgen noch `false` —
|
||
für keinen wurde bisher gezielt recherchiert, ob der jeweilige
|
||
US-Anbieter tatsächlich DPF-zertifiziert ist (das Feld wurde erst mit
|
||
dieser Migration eingeführt, `false` heißt hier "nicht recherchiert",
|
||
nicht "nachweislich nicht zertifiziert"). Nachrecherche pro Anbieter
|
||
noch zu tun.
|
||
|
||
`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
|
||
(die zwölf Kategorien aus `gueltigeZwecke`, siehe Werkzeugkatalog
|
||
weiter unten) 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 Verarbeitungsländern. 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`).
|
||
|
||
---
|
||
|
||
## Freigabe-Workflow (konfigurierbare Mehrfach-Genehmigung, 2026-08-31)
|
||
|
||
**Bewusste Abkehr von einer dokumentierten v1-Ausschlussentscheidung.**
|
||
Diese CLAUDE.md schloss "konfigurierbare Rollen mit eigener Oberfläche"
|
||
und "Workflow-Designer" ursprünglich explizit für v1 aus (siehe "Nicht
|
||
bauen (v1)" unten). Der Nutzer hat das am 2026-08-31 bewusst und nach
|
||
Rückfrage aufgehoben ("Wir sollten das System schon perfekt bauen! Daher
|
||
auch gerne komplex") — Auslöser war die Frage, ob ein Datenschutz-
|
||
beauftragter und/oder die Geschäftsführung zusätzlich in die Entscheidung
|
||
eingebunden werden müssen. Statt DSB/GF fest zu verdrahten, können
|
||
Mandanten jetzt selbst definieren, wer bei welcher abgeleiteten
|
||
Bedingung zusätzlich zustimmen muss.
|
||
|
||
**Datenmodell (Migration 0017):** vier neue Tabellen, orthogonal zu den
|
||
fünf bestehenden Zugriffsrollen (`mitarbeiter`/`verantwortlicher`/
|
||
`pruefer`/`admin`/`betreiber` bleiben unverändert):
|
||
|
||
- `genehmiger_rolle` — eine mandantendefinierte Freigabe-Funktion (z. B.
|
||
"Datenschutzbeauftragter", "Geschäftsführung"). Kein Zugriffsrecht,
|
||
nur eine Freigabe-Zuständigkeit.
|
||
- `nutzer_genehmiger_rolle` — Zuordnung Person↔Genehmiger-Rolle,
|
||
many-to-many. Eine Person kann null, eine oder mehrere davon haben,
|
||
unabhängig von ihrer `app_user.role`.
|
||
- `freigabe_regel` — "wenn Bedingung X zutrifft, ist zusätzlich eine
|
||
Freigabe durch Genehmiger-Rolle Y nötig". Die Bedingung ist bewusst
|
||
**kein freier Regeleditor**, sondern auf die drei Vokabulare
|
||
beschränkt, die die Bewertungslogik ohnehin schon berechnet:
|
||
Anforderungs-ID (z. B. `dsfa_erforderlich`), Einstufungs-ID (z. B.
|
||
`hochrisiko`) oder Datenklasse-ID (z. B. `besondere_kategorie`).
|
||
Kompromiss zwischen "konfigurierbar" (Nutzeranforderung) und
|
||
"buildbar/auditierbar" (jede mögliche Bedingung ist bereits eine
|
||
geprüfte, bekannte Ableitung, keine beliebige Freitext-Regel).
|
||
- `freigabeschritt` — pro Antrag ein Datensatz je ausgelöster
|
||
Freigabe-Regel, Status `ausstehend`/`genehmigt`/`abgelehnt`.
|
||
|
||
`antrag.status` hat einen neuen Zwischenwert `wartet_auf_freigabe`
|
||
zwischen `eingereicht` und `entschieden` (CHECK-Constraint erweitert).
|
||
|
||
**Ablauf:** `handleFallEntscheiden` (`internal/web/fachebene_handlers.go`)
|
||
prüft nach einer Genehmigung/Genehmigung-mit-Auflagen, ob die Bewertung
|
||
mindestens eine `freigabe_regel` des Mandanten auslöst
|
||
(`triggeredGenehmigerRollen`). Falls ja: `antrag.status` wird
|
||
`wartet_auf_freigabe` statt `entschieden`, pro ausgelöster Regel entsteht
|
||
ein `freigabeschritt`, **kein** `registereintrag` entsteht noch. Falls
|
||
keine Regel greift, läuft exakt der alte Pfad (`registriereGenehmigung`)
|
||
— **verifiziert per dediziertem Test**
|
||
(`TestOhneFreigabeRegelnVerhaeltSichWieVorher`), da alle heutigen
|
||
Bestandskunden null Freigabe-Regeln haben und sich für sie nichts ändern
|
||
darf.
|
||
|
||
**Bestätigte Semantik (2026-08-31, Nutzer: "Beides stimmt!"):**
|
||
1. **Alle** ausgelösten Genehmiger-Rollen müssen zustimmen, nicht nur
|
||
eine von mehreren — geprüft durch erneutes `ListFreigabeschritteForAntrag`
|
||
nach jeder Einzelentscheidung.
|
||
2. **Eine Ablehnung kippt den gesamten Antrag**, geht nicht zurück an
|
||
die Fachebene zur Neuentscheidung. `KaskadiereAblehnung` setzt dabei
|
||
automatisch alle anderen noch `ausstehend`en Freigabeschritte
|
||
desselben Antrags ebenfalls auf `abgelehnt` — sonst würde ein
|
||
Freigabeschritt in der Liste einer anderen Person ewig unbearbeitet
|
||
hängen bleiben, obwohl der Antrag längst entschieden ist.
|
||
|
||
Eine Ablehnung (ob direkt oder per Kaskade) mutiert **nicht** die
|
||
ursprüngliche `genehmigt`-Zeile — `entscheidung` bleibt append-only wie
|
||
bisher. Stattdessen entsteht eine neue `entscheidung`-Zeile mit
|
||
`Entscheidung: "abgelehnt"`; `GetLatestEntscheidungForAntrag` (bereits
|
||
bestehender "letzte Zeile gewinnt"-Helfer) zeigt den Override überall
|
||
dort, wo er ohnehin schon verwendet wird — keine weitere Codeänderung
|
||
nötig, um die Überschreibung sichtbar zu machen.
|
||
|
||
**Frontend** (`internal/web/freigabe_handlers.go` + drei neue Templates):
|
||
`GET/POST /verwaltung/genehmiger-rollen` (Admin: Rolle anlegen, Mitglieder
|
||
zuordnen/entfernen — Formular nutzt einen einzelnen `<select>` mit drei
|
||
`<optgroup>`s für die drei Bedingungstypen, kein abhängiges Dropdown,
|
||
kein JavaScript), `GET/POST /verwaltung/freigabe-regeln` (Admin: Regel
|
||
anlegen/löschen), `GET /freigaben` + `POST /freigaben/{id}/entscheiden`
|
||
(jeder eingeloggte Nicht-Betreiber mit zugeordneter Genehmiger-Rolle:
|
||
eigene offene Freigaben sehen und entscheiden, mit Pflicht-Kommentarfeld
|
||
bei Ablehnung). Fall-Detail (`fall_detail.html`) zeigt die
|
||
Freigabeschritt-Historie mit Ampel-Badge **unabhängig vom aktuellen
|
||
Antragsstatus** — anfangs fälschlich auf `wartet_auf_freigabe`
|
||
beschränkt gebaut, beim Live-Verifizieren aufgefallen: sobald der letzte
|
||
Freigebende zustimmt und der Antrag auf `entschieden` springt, verschwand
|
||
die komplette Freigabehistorie aus der Ansicht, was der Grundregel
|
||
widerspricht, dass eine Entscheidung nie ohne vollständige Herleitung
|
||
gezeigt wird. Der Hinweistext "Genehmigung erst endgültig, wenn alle
|
||
erteilt sind" erscheint entsprechend nur noch, solange der Antrag
|
||
tatsächlich noch auf Freigabe wartet.
|
||
|
||
**CSS-Bugfix, beim Live-Verifizieren gefunden (sitesweit, nicht nur
|
||
diese Seite betreffend):** `.beitraege-liste > li:not(:has(> a)) form`
|
||
hatte `flex: 0 0 auto` (kein Schrumpfen). Bei den bisherigen Listen
|
||
(Nutzer, Abteilungen) enthielten diese eingebetteten Formulare nur
|
||
Buttons, die naturgemäß schmal bleiben — die Genehmiger-Rollen-Seite ist
|
||
die erste mit einem `<select>` darin, dessen Browser-Eigenbreite
|
||
(bestimmt durch die längste Options-Beschriftung, hier E-Mail-Adressen)
|
||
den ganzen Zeilen-Container über die Mobile-Viewport-Breite hinaustrieb
|
||
(horizontales Scrollen der ganzen Seite). Fix: `max-width: 100%` auf
|
||
derselben Regel ergänzt — generisch, wirkt auf jedes künftige
|
||
`<select>` in diesem Listen-Pattern, nicht nur hier.
|
||
|
||
**Live end-to-end verifiziert** (curl gegen echten lokalen Server +
|
||
echtes Postgres, nicht nur die Go-Testsuite): Happy Path (eine Regel,
|
||
eine Rolle, Genehmigung → `wartet_auf_freigabe` → Freigabe erteilt →
|
||
`entschieden` + 1 Registereintrag) und Kaskaden-Ablehnung (zwei Regeln,
|
||
zwei Rollen, eine lehnt ab → beide Freigabeschritte `abgelehnt`, Antrag
|
||
direkt `entschieden` als `abgelehnt`, 0 Registereinträge) — beide exakt
|
||
wie spezifiziert. Chromium-Headless-Screenshots (Desktop 1440×900 +
|
||
Mobile 390×844) aller vier neuen/geänderten Seiten bestätigen zusätzlich
|
||
das responsive Layout nach dem enconf-Card-Pattern.
|
||
|
||
**Standard-Genehmiger-Rollen bei Firmenanlage (2026-08-31, Migration
|
||
0018).** Jede neue Firma bekommt jetzt automatisch vier leere
|
||
Genehmiger-Rollen mit erklärender Beschreibung (`standardGenehmigerRollen`
|
||
in `internal/web/freigabe_handlers.go`, aufgerufen aus `handleRegister`
|
||
und `handleBetreiberAccountCreate` direkt nach `CreateAccount`):
|
||
|
||
- **Datenschutzbeauftragter** — "Prüft Anträge mit besonderen
|
||
Datenschutzanforderungen, z. B. wenn eine Datenschutz-Folgenabschätzung
|
||
erforderlich ist."
|
||
- **Geschäftsführer** — "Trifft die unternehmerische Entscheidung bei
|
||
besonders risikobehafteten Vorhaben."
|
||
- **KI-Manager** — "Bewertet den Antrag fachlich."
|
||
- **CISO** — "Prüft den Antrag aus Sicherheitssicht (Auditor)."
|
||
|
||
Bewusst **ohne** Mitglieder und **ohne** Freigabe-Regel — der Admin muss
|
||
nur noch Personen zuordnen (`/verwaltung/genehmiger-rollen`) und
|
||
optional eine auslösende Bedingung definieren
|
||
(`/verwaltung/freigabe-regeln`), statt bei null anzufangen. Welche
|
||
Bedingung welche Rolle tatsächlich auslöst, bleibt **vollständig
|
||
Sache der Firma** — das System schreibt keine feste fachliche Bindung
|
||
vor (z. B. keine hartkodierte Kopplung "Geschäftsführer ⇒ hochrisiko");
|
||
die Beschreibung ist reine UI-Orientierungshilfe, kein Ableitungsregel-
|
||
Ersatz. `genehmiger_rolle.beschreibung` ist ein freies, optionales
|
||
Textfeld (`NOT NULL DEFAULT ''`) — auch für vom Admin selbst angelegte
|
||
zusätzliche Rollen nutzbar. Getestet via `TestRegisterSeedsStandardGenehmigerRollen`
|
||
und live per curl gegen echten Server + Postgres verifiziert.
|
||
|
||
---
|
||
|
||
## 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~~ —
|
||
**erledigt** (`internal/web/einladung_handlers.go`: `account.einladung_token`,
|
||
Migration 0013, ein Sammellink pro Mandant statt Einmal-Token pro
|
||
Person, kein Ablaufdatum, per Admin unter `GET /verwaltung/einladung`
|
||
jederzeit erneuerbar [macht den alten Link sofort ungültig].
|
||
`GET/POST /einladung/{token}` — öffentlich, Ebene 1 — legt einen Login
|
||
mit Rolle `mitarbeiter` im Mandanten des Links an und meldet direkt an,
|
||
ohne E-Mail-Bestätigung [noch nicht gebaut, siehe unten]. Die Abteilung
|
||
wählt die Person weiterhin erst beim ersten Antrag, nicht bei der
|
||
Einladung.), (2) ~~CSV-Import~~ — **erledigt** (`internal/web/csv_import_handlers.go`,
|
||
`GET /verwaltung/nutzer/import`: CSV mit Kopfzeile, Pflichtspalte
|
||
"email", optionale Spalte "role" [Standard `mitarbeiter`]. Wie beim
|
||
einzelnen Nutzer-Anlegen gibt es keine Einladungsmail — pro Zeile wird
|
||
ein Einmalpasswort erzeugt [wiederverwendet `auth.NewSessionToken()`],
|
||
das NUR auf der Ergebnisseite direkt nach dem Import angezeigt wird und
|
||
danach nirgendwo mehr abrufbar ist. Kein Alles-oder-nichts-Import:
|
||
gültige Zeilen werden angelegt, eine ungültige Rolle oder bereits
|
||
vergebene E-Mail in einer Zeile blockiert nicht die übrigen Zeilen —
|
||
jede Zeile bekommt ihr eigenes Ergebnis [Passwort oder Fehlermeldung].),
|
||
(3) SSO (Entra ID/Google Workspace), (4) Antrag ganz ohne vorheriges
|
||
Konto über Mandantenlink + E-Mail-Bestätigungscode (Konto entsteht
|
||
nebenbei). (3)-(4) sind weiterhin nicht gebaut.
|
||
|
||
**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). Seit 2026-08-31 hat
|
||
`.table-scroll` zusätzlich einen rechten Fade-Rand (`mask-image`) —
|
||
ohne den sah eine am Fensterrand abgeschnittene breite Tabelle wie ein
|
||
Rendering-Fehler aus statt wie "hier geht's mit Scrollen weiter" (per
|
||
Screenshot am Werkzeugkatalog beobachtet: die letzten zwei Spalten
|
||
waren nur als einzelne Buchstaben sichtbar, ganz ohne Hinweis auf mehr
|
||
Inhalt).
|
||
|
||
**Formulare** (`.form-card`/`.form-section`/`.form-grid`/`.chip-group`
|
||
in `app.css`, seit 2026-08-31, erstmals angewendet auf
|
||
`betreiber_werkzeug_form.html`): vorher war jedes Formular eine flache
|
||
Spalte aus `<label>`+`<input>` 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 `<fieldset class="form-section">`
|
||
(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: `<legend>` in `<fieldset
|
||
class="form-section">` 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.
|
||
|
||
**enconf-Patterns übertragen (2026-08-31):** enconf (Schwesterprojekt)
|
||
ist React + Ant Design (`ProTable`/`StatCard`/`PageHeader`-Komponenten)
|
||
— nicht 1:1 übernehmbar, da Deklarix bewusst kein SPA ist, aber die
|
||
*Muster* wurden als reines CSS/HTML nachgebaut (recherchiert über die
|
||
Architect-RAG-Suche, `ac_search_code_global`, da das enconf-Repo selbst
|
||
außerhalb des Arbeitsverzeichnisses liegt):
|
||
|
||
- **`.table-responsive`** (ersetzt `.table-scroll` schrittweise) —
|
||
enconfs `ProTable.tsx` schaltet auf schmalen Screens von Tabelle auf
|
||
eine Card-Liste um (`renderMobileCard` via `Grid.useBreakpoint()`),
|
||
statt horizontal zu scrollen. Nachgebaut rein in CSS: unter 640px
|
||
werden `<tr>` zu Karten, `<td>` zu Label/Wert-Zeilen (`content:
|
||
attr(data-label)` im `::before`) — jedes `<td>` braucht dafür ein
|
||
`data-label="Spaltenname"`-Attribut. Ab 640px normale Tabelle mit
|
||
Scroll-Fade als Sicherheitsnetz. Bisher angewendet auf beide
|
||
Werkzeugkatalog-Tabellen; `.table-scroll` (nur Scroll-Fade, keine
|
||
Card-Umschaltung) bleibt für noch nicht migrierte Tabellen (Register,
|
||
Nutzer-Import-Ergebnis) bestehen.
|
||
- **`.page-header`** (Titel+Unterzeile links, primäre Aktion rechts,
|
||
stapelt auf Mobile) — ersetzt einen nackten `<h1>` mit einem Link
|
||
irgendwo unten auf der Seite. Bisher auf den Werkzeugkatalog
|
||
angewendet (Betreiber + Mandant, inkl. "Eigene Werkzeuge"-Abschnitt).
|
||
- **`.stat-cards`/`.stat-card`** — Kennzahl-Kacheln mit farbigem oberem
|
||
Akzentrand (blau normal, gelb/rot nur wenn der Wert > 0 ist — keine
|
||
Farbe ohne Grund) statt einfacher Text-Links; `.stat-card-action` für
|
||
die primäre Handlungs-Kachel (z. B. "Antrag stellen"). Kein
|
||
Icon-Set/keine Icon-Bibliothek eingeführt (enconf nutzt
|
||
`@ant-design/icons`, das ist React-spezifisch) — bewusste
|
||
Vereinfachung. Angewendet auf die Startseite (`index.html`).
|
||
|
||
**Ausgerollt (2026-08-31):** Register, Nutzerverwaltung, Abteilungen,
|
||
Fälle-Posteingang, Betreiber-Dashboard.
|
||
|
||
- **Register** (`register_liste.html`) und **Fälle-Posteingang**
|
||
(`faelle_liste.html`) haben jetzt `.page-header` + Freitext-Filter
|
||
(`q`) + Paginierung. Register zusätzlich `.table-responsive` (war
|
||
vorher `.table-scroll`) mit `data-label` je `<td>`.
|
||
- **Nutzerverwaltung** (`nutzer_liste.html`) hat `.page-header` +
|
||
Freitext-Filter (E-Mail/Rolle) + Paginierung — blieb `.beitraege-liste`
|
||
(schon Karten-artig), keine `.table-responsive`-Umstellung nötig.
|
||
- **Abteilungen** (`abteilung_liste.html`) bekam nur `.page-header` —
|
||
bewusst ohne Paginierung, Abteilungslisten sind typischerweise klein
|
||
(Handvoll Stammdaten pro Mandant).
|
||
- **Betreiber-Dashboard** (`betreiber_dashboard.html`) von
|
||
`.admin-kacheln` auf `.stat-cards` umgestellt (analog zu `index.html`).
|
||
|
||
Neue gemeinsame Helfer in `pagination.go`: `matchesQuery(q string,
|
||
felder ...string) bool` — eine einzige Freitext-Filter-Basis für alle
|
||
Listen-Seiten, damit nicht jede ihre eigene Such-Logik erfindet.
|
||
|
||
**Beim Ausrollen gefundener, bereits live gewesener Bug:** `.filter-bar`
|
||
direkt auf einem `<form>`-Element (statt auf einem `<div>` innerhalb
|
||
des Formulars) verlor gegen die allgemeine `form { flex-direction:
|
||
column }`-Regel — beide Selektoren haben für die Property
|
||
`flex-direction` dieselbe Spezifität, aber nur `form` setzte sie
|
||
explizit, `.filter-bar` überließ sie dem Initialwert. Ergebnis: die
|
||
Filterleiste stapelte senkrecht statt in einer Zeile. Betraf bereits
|
||
die im vorigen Schritt ausgelieferte Mandanten-Werkzeugkatalog-Seite.
|
||
Behoben durch explizites `flex-direction: row` in `.filter-bar` selbst
|
||
— jetzt unabhängig davon robust, ob die Klasse auf einem `<form>` oder
|
||
einem `<div>` sitzt.
|
||
|
||
**Ebenfalls ausgerollt (2026-08-31, zweite Runde):** Betreiber-Accounts
|
||
(`.page-header` + Filter über Firmenname + Paginierung), Betreiber-
|
||
Audit-Log (`.page-header` + Filter über Aktion/Zieltyp/Details +
|
||
Paginierung), "Meine Anträge" (`.page-header` + Filter über Titel/
|
||
Status/Häufigkeit + Paginierung), Wiedervorlage (nur `.page-header`,
|
||
bewusst ohne Paginierung — natürlich begrenzt auf "braucht gerade
|
||
Aufmerksamkeit").
|
||
|
||
**Bekannte Einschränkung beim Audit-Log:** `ListAuditLog` holt weiterhin
|
||
nur die neuesten `auditLogFetchLimit` (1000) Einträge aus der DB, dann
|
||
wird in Go gefiltert/paginiert (`auditLogFetchLimit`-Konstante in
|
||
`betreiber_handlers.go`) — keine echte DB-seitige Offset-Paginierung.
|
||
Für ein potenziell sehr groß werdendes, append-only Audit-Log ist das
|
||
mittelfristig nicht die saubere Lösung (Einträge jenseits der 1000
|
||
neuesten sind über die Such-/Paginierungs-UI nicht erreichbar), aber
|
||
proportional zum aktuellen Datenvolumen und konsistent mit dem
|
||
In-Memory-Paginierungsmuster der übrigen Listen. Bei echtem Bedarf:
|
||
`store.ListAuditLog` um `offset`/echtes SQL-`LIMIT`/`OFFSET` erweitern.
|
||
|
||
**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.)
|
||
8. ~~Konfigurierbarer Freigabe-Workflow (Mehrfach-Genehmigung)~~ —
|
||
**erledigt** (siehe "Freigabe-Workflow" weiter oben: Genehmiger-Rollen,
|
||
Freigabe-Regeln, Freigabeschritte, Migration 0017 — bewusste Abkehr
|
||
von der ursprünglichen v1-Ausschlussentscheidung, siehe "Nicht bauen
|
||
(v1)" unten).
|
||
|
||
Nicht bauen (v1): automatische Genehmigung ohne Mensch, Erkennung
|
||
tatsächlicher Werkzeug-Nutzung, Mitarbeiterüberwachung (nichts, was
|
||
Nutzung einzelner Personen auswertet), Konzernstrukturen mit Vererbung,
|
||
mandantenspezifische Regelwerke, SAML, Schnittstellen zu Fremdsystemen.
|
||
|
||
~~konfigurierbare Rollen mit eigener Oberfläche~~ / ~~Workflow-Designer~~
|
||
— **am 2026-08-31 bewusst aufgehoben**, siehe "Freigabe-Workflow"
|
||
weiter oben: Genehmiger-Rollen und Freigabe-Regeln sind jetzt genau das,
|
||
nur mit der Bedingung auf die drei bereits von der Bewertungslogik
|
||
berechneten Vokabulare beschränkt (kein beliebiger Regeleditor).
|
||
|
||
---
|
||
|
||
## 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~~ (`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.
|