Sechs neue Zweck-Kategorien (Datenanalyse, Video, Audio/Sprache, Kundenservice/Chatbot, Präsentation/Design, Automatisierung/Agenten) ergänzen die ursprünglichen sechs — deckt jetzt die tatsächliche Bandbreite an Einsatzgebieten ab statt nur klassischer Wissensarbeit. Werkzeug-Formular (Betreiber + Mandant, gleiche Vorlage) neu strukturiert: Karten-Layout mit thematischen Abschnitten, 2-Spalten-Grid für kurze Felder, Chip-Gruppen statt langer Checkbox-Listen, abgesetzte Gefahrenzone für den Löschen-Button. Dabei einen vorbestehenden Bug gefunden und behoben: das Lösch-Formular war fest auf die Betreiber-Route verdrahtet, obwohl dieselbe Vorlage auch vom Mandanten-Formular genutzt wird.
916 lines
46 KiB
Markdown
916 lines
46 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, Verarbeitungsort, 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, Verarbeitungsort, AVV, Training aus,
|
||
Aufbewahrung, Zertifizierungen, Geeignete Zwecke, Zuletzt geprüft) statt
|
||
als Liste — vorher waren nur Name/Anbieter/Ort auf einen Blick sichtbar,
|
||
der Rest war erst nach Klick auf den Detaileintrag zu sehen.
|
||
|
||
Der zentrale Katalog ist aktuell mit 10 recherchierten Einträgen befüllt
|
||
(ChatGPT Enterprise, Microsoft 365 Copilot, Claude for Work, GitHub
|
||
Copilot Business, DeepL Pro/API, Google Gemini für Google Workspace,
|
||
Le Chat Enterprise, Perplexity Enterprise Pro, Adobe Firefly Enterprise,
|
||
Otter.ai Business) — deckt damit erstmals alle sechs Zweck-Kategorien
|
||
ab (vorher fehlten Bild und Transkription vollständig, Recherche hatte
|
||
kein dediziertes Werkzeug). Jede Zusicherung ist mit einer echten,
|
||
offiziellen Anbieter-Quelle belegt (Stand 2026-08-31), nicht geraten —
|
||
zwei Einträge (Adobe Firefly: Trainingsausschluss ist nicht Standard;
|
||
Otter.ai: Training auf de-identifizierten Aufnahmen, laufende
|
||
Rechtsstreitigkeiten) sind bewusst mit kritischen Einschränkungen
|
||
versehen statt schöngefärbt, das ist die vorgesehene Funktion des
|
||
Katalogs.
|
||
|
||
`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 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~~ —
|
||
**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-/Radio-Gruppen mit vielen Optionen (Verarbeitungsort, 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.
|
||
|
||
**Verifikationsmethode für CSS-Änderungen:** Live-Cookie-Auth per
|
||
Chromium-Headless/CDP ist im Sandbox-Environment nicht möglich (kein
|
||
websocket-Python-Modul) — stattdessen: Seite per `curl -b cookies.txt`
|
||
authentifiziert abrufen, `/static/...`-Pfade im HTML auf eine absolute
|
||
`http://localhost:<port>/static/...`-URL umschreiben (Assets brauchen
|
||
kein Auth-Cookie), dann `chromium --headless --disable-gpu --no-sandbox
|
||
--window-size=<W>,<H> --screenshot=out.png file:///pfad/zur/seite.html`
|
||
bei mobiler (z. B. 390×844) und Desktop-Breite (z. B. 1440×900)
|
||
screenshotten. Für einen Menü-Aufklapp-Zustand: `checked`-Attribut
|
||
manuell ins gespeicherte HTML einfügen vor dem Screenshot.
|
||
|
||
---
|
||
|
||
## Projektstruktur
|
||
|
||
```
|
||
/var/www/deklarix/
|
||
├── cmd/
|
||
│ └── deklarix/
|
||
│ └── main.go # Entry Point: Migration, Regelwerk laden, HTTP-Server
|
||
├── internal/
|
||
│ ├── auth/ # Passwort-Hashing, Session-Token (produktunabhängig)
|
||
│ ├── rules/ # Lädt/validiert Datenklasse-/Einstufungs-/Anforderungs-YAML
|
||
│ ├── store/ # Postgres, Migrationen
|
||
│ └── web/ # Handler, Templates
|
||
├── rules/ # YAML-Regelwerk + OPEN.md (offene fachliche Fragen)
|
||
├── design/
|
||
│ └── enterprise.css # enconf Ant-Design-Theme — Referenz für Design-Tokens
|
||
│ (Farben/Radius/Schatten), nicht direkt eingebunden
|
||
│ (Ant-Design-Klassennamen passen nicht zu Deklarix'
|
||
│ reinem HTML/htmx-Stack, siehe internal/web/static/app.css)
|
||
├── packaging/
|
||
│ └── DEBIAN/
|
||
│ └── control.tmpl # .deb Package-Control-Template
|
||
├── scripts/
|
||
│ ├── build.sh # Cross-Compile amd64 + arm64
|
||
│ ├── test.sh # Tests + vet + build-check
|
||
│ └── release.sh # Vollständiger Release-Prozess
|
||
├── go.mod
|
||
├── go.sum
|
||
└── CLAUDE.md
|
||
```
|
||
|
||
---
|
||
|
||
## Datenmodell
|
||
|
||
- `account` — ein Mandant (ein Unternehmen). Jeder Antrag gehört genau
|
||
einem Account.
|
||
- `app_user` — ein Login innerhalb eines Accounts. Rolle ist eine von
|
||
`mitarbeiter`, `verantwortlicher`, `pruefer`, `admin` (alle vier
|
||
Mandanten-bezogen) oder `betreiber` (plattformweit, Ebene 5).
|
||
- `session` — eine angemeldete Sitzung (Token, Ablaufzeit); echte
|
||
Tabelle statt zustandsloser signierter Tokens, damit Logout eine
|
||
Sitzung wirklich beendet.
|
||
- `audit_log` — Protokoll (append-only): jede Antragsänderung, jede
|
||
Bewertung, jede Entscheidung, jede Katalogänderung mit Zeitstempel
|
||
und Akteur.
|
||
- `abteilung` — Stammdaten für Fragebogen-Feld A.abteilung, je Mandant.
|
||
- `werkzeug` — Katalogeintrag (siehe oben); `account_id` NULL = zentral,
|
||
gesetzt = mandantenspezifische Ergänzung. NICHT append-only — der
|
||
Katalog wird gepflegt, eine Entscheidung friert den zu ihrem
|
||
Zeitpunkt gültigen Datensatz separat ein (geplant).
|
||
- `werkzeug_sperre` — Sperrung eines (auch zentralen) Katalogeintrags
|
||
durch einen einzelnen Mandanten.
|
||
- `antrag` — das Vorhaben aus Fragebogen-Abschnitt A plus die
|
||
vollständigen Antworten aus B/C/D als JSON (`antworten`, JSONB) — ein
|
||
starres Spaltenschema könnte den adaptiven Fragebogen nicht abbilden.
|
||
NICHT append-only (normale Zustandsänderung `entwurf` →
|
||
`eingereicht` → `entschieden`, wie `submission` es im alten Produkt
|
||
war).
|
||
- `bewertung` — der berechnete Vorschlag (Schritt 4).
|
||
- `entscheidung` — die Entscheidung eines/einer Verantwortlichen über
|
||
einen Antrag, mit Snapshot des gewählten Werkzeugs (`werkzeug_snapshot`,
|
||
JSONB) und Ablaufdatum (`gueltig_bis`) bei Genehmigung (Schritt 5).
|
||
- `registereintrag` — automatisch bei jeder Genehmigung erzeugt
|
||
(Schritt 6), denormalisierter Nachweis (eigene Textspalten statt
|
||
Joins über bewertung/entscheidung/werkzeug).
|
||
|
||
**Append-only:** kein UPDATE auf `audit_log` (Trigger `forbid_update_delete`,
|
||
wiederverwendet aus dem alten Produkt), ebenso `bewertung`,
|
||
`entscheidung` und `registereintrag`.
|
||
|
||
---
|
||
|
||
## Baureihenfolge
|
||
|
||
1. ~~Datenmodell, Regelwerk als YAML, Katalogstruktur~~ — **erledigt**
|
||
(dieser Stand: `account`/`app_user`/`session`/`audit_log`/
|
||
`abteilung`/`werkzeug`/`werkzeug_sperre`/`antrag` in Postgres,
|
||
`internal/rules` lädt und validiert die drei YAML-Regelwerke,
|
||
Web-Layer kompiliert mit Auth + Plattform-Bereich-Gerüst).
|
||
2. ~~Fragebogen mit adaptiver Logik~~ — **erledigt** (`GET /antraege/neu`
|
||
einseitiges Formular, `POST /antraege` legt an und reicht direkt ein,
|
||
`GET /antraege` eigene Anträge, `GET /antraege/{id}` Detail. Adaptive
|
||
Folgefragen C2/C3 rein per CSS `:has()` ein-/ausgeblendet, kein
|
||
JavaScript. `antworten`-JSON nutzt exakt die Fakten-Schlüssel aus
|
||
`rules/*.yaml` (b1-b7, c1-c5, c2_folge, c3_art) — das hat auch die
|
||
OPEN.md-Frage zu den "verboten"-Fragebogen-Feldern final geklärt.)
|
||
3. ~~Ableitungen und harte Filter~~ — **erledigt** (`internal/rules`:
|
||
`EvaluateDatenklasse`/`EvaluateEinstufung`/`DeriveAnforderungen`/
|
||
`IstVerboten`/`FilterWerkzeuge`, vollständig getestet gegen die
|
||
echten `rules/*.yaml`-Dateien.)
|
||
4. ~~Ergebnisdarstellung mit Herleitung~~ — **erledigt** (`POST /antraege`
|
||
berechnet und speichert eine `bewertung` sofort beim Einreichen,
|
||
`GET /antraege/{id}` zeigt Datenklasse, KI-VO-Einstufung,
|
||
Anforderungen, zulässige/ausgeschlossene Werkzeuge — jeweils mit
|
||
Herleitung — sowie RegelwerkVersion/KatalogVersion und den
|
||
Pflicht-Hinweis "Das System entscheidet nicht. Es bereitet vor.".
|
||
`bewertung` ist append-only. Getestet inkl. K.-o.-Prüfung
|
||
(`verboten` überspringt die Werkzeugsuche) und hartem Filter
|
||
gegen den Katalog.)
|
||
5. ~~Entscheidung, Snapshot, Audit-Log~~ — **erledigt** (Ebene 3:
|
||
`GET /faelle` Posteingang, `GET /faelle/{id}` + `POST
|
||
/faelle/{id}/entscheiden`. `verantwortlicher` entscheidet,
|
||
`pruefer` sieht dieselbe Seite ohne Entscheidungsrecht [403 bei
|
||
Entscheidungsversuch]. Begründung ist Pflicht bei Abweichung vom
|
||
abgeleiteten Vorschlag (`vorschlagFuer`), Genehmigung friert den
|
||
vollständigen Werkzeugdatensatz ein und braucht ein zulässiges
|
||
Werkzeug aus der Bewertung, `entscheidung` ist append-only, jede
|
||
Entscheidung erzeugt einen `audit_log`-Eintrag.)
|
||
6. ~~Registereintrag und Export~~ — **erledigt** (`internal/web/register_handlers.go`:
|
||
`GET /registereintraege` zeigt alle Registereinträge des Mandanten,
|
||
`GET /registereintraege/export.{csv,pdf}` exportiert sie. Jede
|
||
Genehmigung — `genehmigt` oder `genehmigt_mit_auflagen` — erzeugt in
|
||
`handleFallEntscheiden` automatisch einen `registereintrag`
|
||
[append-only, Migration 0011]: Zweck [`antrag.titel`], Abteilung,
|
||
Werkzeug, Datenklasse, Einstufung, Auflagen [die Anforderungen der
|
||
Bewertung], Verantwortliche/r, Entscheidungsdatum, Gültigkeit —
|
||
bewusst denormalisiert [eigene Textspalten statt Joins], damit ein
|
||
Eintrag nicht von einem späteren Katalog-/Abteilungs-Wandel abhängt.
|
||
Abgelehnte Entscheidungen und Rückfragen erzeugen keinen Eintrag.
|
||
Der PDF-Export nutzt das bereits vorhandene, bis dahin ungenutzte
|
||
`go-pdf/fpdf` [keine neue Abhängigkeit] mit `UnicodeTranslatorFromDescriptor`
|
||
für deutsche Umlaute.)
|
||
7. ~~Wiedervorlage und Katalog-Benachrichtigung~~ — **erledigt**
|
||
(`GET /wiedervorlage`: abgelaufene/bald ablaufende Genehmigungen und
|
||
Genehmigungen, deren Werkzeug sich seither im Katalog geändert hat
|
||
oder entfernt wurde. In-App-Liste, kein E-Mail-Versand.)
|
||
|
||
Nicht bauen (v1): automatische Genehmigung ohne Mensch, Erkennung
|
||
tatsächlicher Werkzeug-Nutzung, Mitarbeiterüberwachung (nichts, was
|
||
Nutzung einzelner Personen auswertet), konfigurierbare Rollen mit
|
||
eigener Oberfläche, Konzernstrukturen mit Vererbung, mandantenspezifische
|
||
Regelwerke, Workflow-Designer, SAML, Schnittstellen zu Fremdsystemen.
|
||
|
||
---
|
||
|
||
## Go Commands
|
||
|
||
```bash
|
||
export PATH=$PATH:/usr/local/go/bin # Immer setzen!
|
||
|
||
# Entwickeln
|
||
go run ./cmd/deklarix/
|
||
|
||
# Tests
|
||
./scripts/test.sh
|
||
# oder direkt:
|
||
go test -race ./...
|
||
go vet ./...
|
||
|
||
# Build (amd64 + arm64)
|
||
./scripts/build.sh 1.0.0
|
||
|
||
# Build (nur amd64)
|
||
./scripts/build.sh 1.0.0 amd64
|
||
```
|
||
|
||
---
|
||
|
||
## Build & Release-Prozess
|
||
|
||
### Versioning (Semantic Versioning: MAJOR.MINOR.PATCH)
|
||
- **MAJOR** — Breaking changes, API-Inkompatibilitäten
|
||
- **MINOR** — Neue Features, rückwärtskompatibel
|
||
- **PATCH** — Bugfixes
|
||
|
||
### Release-Schritte
|
||
```bash
|
||
# 1. Alle Änderungen committen
|
||
git add -p && git commit -m "feat: ..."
|
||
|
||
# 2. Release-Skript (macht Tests → .deb-Build → Gitea-Upload → Tag → Push)
|
||
./scripts/release.sh 1.2.0
|
||
|
||
# Danach liegt in dist/:
|
||
# deklarix_1.2.0_amd64.deb
|
||
# deklarix_1.2.0_arm64.deb
|
||
```
|
||
|
||
### Was das Release-Skript tut
|
||
1. Prüft: sauberer Git-Status (keine uncommitted changes)
|
||
2. Führt `./scripts/test.sh` aus (vet + race tests + build-check, braucht `DATABASE_URL`)
|
||
3. Baut `.deb`-Pakete für `linux/amd64` und `linux/arm64` (`scripts/build.sh`)
|
||
4. Lädt die Pakete in Giteas Debian-Paketregistrierung hoch (Kanal `testing`,
|
||
Suites `bookworm` + `trixie`) — siehe „Deployment“ unten
|
||
5. Setzt Git-Tag `v<version>` mit Annotierung
|
||
6. Pusht `main` + Tag nach `origin`
|
||
|
||
### Deployment (apt, Infrastruktur-Standard wie bei enconf)
|
||
|
||
Updates laufen ausschließlich über ein apt-Paket, nie über manuelles
|
||
Kopieren eines Binaries. `.deb`-Pakete landen in Giteas eingebauter
|
||
Debian-Paketregistrierung unter der Gitea-Org `projekte` (gleicher
|
||
Mechanismus wie beim Schwesterprojekt enconf-webpanel). Zielserver
|
||
richten sich einmalig ein:
|
||
|
||
```bash
|
||
sudo mkdir -p /etc/apt/keyrings
|
||
sudo curl -fsSL https://git.netcell-it.de/api/packages/projekte/debian/repository.key \
|
||
-o /etc/apt/keyrings/deklarix-gitea.asc
|
||
echo "deb [signed-by=/etc/apt/keyrings/deklarix-gitea.asc] https://git.netcell-it.de/api/packages/projekte/debian $(lsb_release -cs) testing" \
|
||
| sudo tee /etc/apt/sources.list.d/deklarix.list
|
||
sudo apt-get update && sudo apt-get install deklarix
|
||
```
|
||
|
||
Danach: `apt upgrade` für Updates, `systemctl {start,stop,status} deklarix`
|
||
für den Dienst. Konfiguration liegt in `/etc/deklarix/deklarix.env`
|
||
(aus `deklarix.env.example` beim Erstinstall kopiert, `DATABASE_URL` ist
|
||
dort absichtlich auskommentiert — der Dienst startet erst, wenn sie
|
||
gesetzt ist; `postinst` startet ihn sonst bewusst nicht, um keine
|
||
Crash-Restart-Schleife gegen einen Platzhalter-Host zu erzeugen).
|
||
Aktuell gibt es nur den Kanal `testing` (kein `stable` — es gibt noch
|
||
keine Kunden, für die eine Trennung nötig wäre).
|
||
|
||
---
|
||
|
||
## Testing-Pattern & Qualitätssicherung
|
||
|
||
```go
|
||
// Datei: internal/web/health_test.go
|
||
package web_test
|
||
|
||
import (
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"testing"
|
||
)
|
||
|
||
func TestHealth(t *testing.T) {
|
||
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||
w := httptest.NewRecorder()
|
||
HealthHandler(w, req)
|
||
if w.Code != http.StatusOK {
|
||
t.Fatalf("expected 200, got %d", w.Code)
|
||
}
|
||
}
|
||
```
|
||
|
||
- Tests liegen neben dem Code: `paket/foo_test.go`
|
||
- Package: `package foo_test` (Black-Box-Test) oder `package foo` (White-Box)
|
||
- Race-Detector immer an: `go test -race ./...`
|
||
- Tabellenbasierte Tests für mehrere Inputs
|
||
|
||
**DATABASE_URL und Testsystem.** `internal/store` hat Integrationstests
|
||
gegen echtes Postgres (u. a. die Append-only-Garantie). Ohne
|
||
`DATABASE_URL` überspringt `go test` diese Tests einfach — praktisch für
|
||
schnelle lokale Iteration ohne lokales Postgres. `./scripts/test.sh`
|
||
(und damit `release.sh`) ist strenger: es bricht ohne `DATABASE_URL` mit
|
||
Fehler ab, statt die Append-only-Garantie still zu überspringen. Es
|
||
zieht dafür aber **kein** eigenes Postgres hoch (kein Docker-Zwang auf
|
||
dem lokalen Dev-Rechner) — das ist Aufgabe des dedizierten Testsystems,
|
||
dort muss `DATABASE_URL` vorkonfiguriert sein.
|
||
|
||
---
|
||
|
||
## Leitplanken, die dauerhaft gelten
|
||
|
||
- **Keine Rechtsdienstleistung.** Deklarix bereitet eine Einstufung vor,
|
||
entscheidet nicht. Keine Formulierung darf eine individuelle
|
||
rechtliche Bewertung suggerieren oder Erfolgsaussichten behaupten.
|
||
- **Keine erfundenen Einstufungen.** Jede Ableitungsregel steht in einer
|
||
Regel-YAML und wurde dort von Hand geprüft; offene fachliche Fragen
|
||
stehen in `rules/OPEN.md`, werden nicht geraten.
|
||
- **Werkzeugkatalog-Zusicherungen müssen belegbar sein.** `quelle` und
|
||
`letzte_pruefung` sind Pflichtfelder, keine Ausnahme.
|
||
- **Keine Mitarbeiterüberwachung.** Nichts im Produkt darf auswerten,
|
||
welche einzelne Person welches Werkzeug wie oft nutzt — das
|
||
widerspricht dem Vertrauensmodell, auf dem die freiwillige
|
||
Antragstellung beruht.
|
||
- **DSGVO und Hosting in der EU.**
|
||
|
||
---
|
||
|
||
## Wichtige Hinweise
|
||
|
||
### Go PATH
|
||
```bash
|
||
# Immer setzen — ist nicht im Standard-PATH des Servers
|
||
export PATH=$PATH:/usr/local/go/bin
|
||
```
|
||
|
||
### Git Push
|
||
```bash
|
||
git push origin main
|
||
# Remote: https://git.netcell-it.de/projekte/deklarix.git
|
||
```
|
||
|
||
### Server-Prozess
|
||
```bash
|
||
# Dienst läuft über systemd (installiert per apt, siehe Deployment oben)
|
||
sudo systemctl start deklarix
|
||
sudo systemctl status deklarix
|
||
|
||
# Config: /etc/deklarix/deklarix.env (DATABASE_URL, PORT, RULES_DIR)
|
||
|
||
# Logs prüfen
|
||
journalctl -u deklarix -f
|
||
```
|
||
|
||
### Bekannte Server
|
||
|
||
| Rolle | Host | Zugang |
|
||
|---|---|---|
|
||
| Testsystem + `app.deklarix.de` (DB-Integrationstests, apt-Verifikation, nginx-Reverse-Proxy + Let's-Encrypt-TLS auf den lokalen Dienst) | 89.163.205.4 | SSH `noroot`, sudo passwortlos |
|
||
| Installer-Endpoint (`get.deklarix.com`) | 89.163.205.110 | noch nicht eingerichtet |
|
||
| Marketing-Site (`deklarix.de`/`.com`) | 89.163.205.80 | noch nicht eingerichtet |
|
||
|
||
---
|
||
|
||
## Arbeitsweise für Claude Code
|
||
|
||
- Kleine Commits, ein Thema pro Commit
|
||
- Erst Test, dann Implementierung
|
||
- Keine neue Abhängigkeit ohne Rückfrage
|
||
- Fehler nicht verschlucken, keine stillen Fallbacks
|
||
- Bei fachlicher Unsicherheit: nicht raten, sondern als offene Frage in
|
||
`rules/OPEN.md` notieren
|
||
|
||
## Vor Änderungen
|
||
|
||
1. `go vet ./...` — keine Fehler
|
||
2. `./scripts/test.sh` — alle Tests grün
|
||
3. Bestehenden Code lesen — nicht raten
|
||
|
||
## Nach Änderungen
|
||
|
||
1. `./scripts/test.sh` → 0 Fehler
|
||
2. `./scripts/build.sh <version>` → erfolgreich
|
||
3. Commit mit semantischer Message: `feat:`, `fix:`, `refactor:`, `docs:`
|
||
4. Bei Release: `./scripts/release.sh <version>`
|
||
|
||
---
|
||
|
||
## Offene Punkte
|
||
|
||
- ~~Rangfolge der Datenklassen~~ (`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.
|