Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d2cb79424 | ||
|
|
4dd9bd88cb | ||
|
|
96ac5d6b59 |
26
CLAUDE.md
26
CLAUDE.md
@@ -734,16 +734,24 @@ journalctl -u deklarix -f
|
||||
|
||||
## Offene Punkte
|
||||
|
||||
- **Rangfolge der Datenklassen und Grenzfälle im Anforderungsprofil**
|
||||
(siehe `rules/OPEN.md`, Punkte 1/2/5/6) — mit dem/der fachlich
|
||||
Verantwortlichen (z. B. Datenschutzbeauftragte/r) bestätigen. Schritt
|
||||
3 ist trotzdem schon umgesetzt (auf Basis dieser dokumentierten
|
||||
Annahmen) — nicht auf die Klärung gewartet, um nicht blockiert zu
|
||||
bleiben, aber die Ableitung kann sich noch ändern.
|
||||
- ~~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`.
|
||||
Weiterhin offen: ob die drei Varianten fachlich vollständig sind
|
||||
(siehe `rules/OPEN.md`, Punkt 3).
|
||||
**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)**
|
||||
|
||||
@@ -360,7 +360,6 @@ func (s *Server) handleAntragList(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
type anforderungView struct {
|
||||
ID string
|
||||
Beschreibung string
|
||||
Herleitung string
|
||||
}
|
||||
@@ -387,7 +386,6 @@ type bewertungView struct {
|
||||
type antragDetailData struct {
|
||||
Title string
|
||||
Nav navData
|
||||
ID string
|
||||
Titel string
|
||||
Beschreibung string
|
||||
Ergebnis string
|
||||
@@ -415,7 +413,7 @@ func (s *Server) handleAntragDetail(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
data := antragDetailData{
|
||||
Title: "Antrag", Nav: navFor(r), ID: antrag.ID, Titel: antrag.Titel,
|
||||
Title: "Antrag", Nav: navFor(r), Titel: antrag.Titel,
|
||||
Beschreibung: antrag.Beschreibung, Ergebnis: antrag.Ergebnis, Haeufigkeit: antrag.Haeufigkeit,
|
||||
Status: antrag.Status, CreatedAt: antrag.CreatedAt.Format("02.01.2006 15:04"), Antworten: antwortenAnzeige(antworten),
|
||||
}
|
||||
@@ -442,7 +440,7 @@ func (s *Server) toBewertungView(ctx context.Context, b store.Bewertung) *bewert
|
||||
CreatedAt: b.CreatedAt.Format("02.01.2006 15:04"),
|
||||
}
|
||||
for _, a := range b.Anforderungen {
|
||||
v.Anforderungen = append(v.Anforderungen, anforderungView{ID: a.ID, Beschreibung: a.Beschreibung, Herleitung: a.Herleitung})
|
||||
v.Anforderungen = append(v.Anforderungen, anforderungView{Beschreibung: a.Beschreibung, Herleitung: a.Herleitung})
|
||||
}
|
||||
for _, id := range b.ZulaessigeWerkzeuge {
|
||||
v.ZulaessigeWerkzeuge = append(v.ZulaessigeWerkzeuge, s.werkzeugName(ctx, id))
|
||||
|
||||
@@ -80,7 +80,6 @@ type betreiberUserView struct {
|
||||
type betreiberAccountDetailData struct {
|
||||
Title string
|
||||
Nav navData
|
||||
AccountID string
|
||||
Name string
|
||||
Users []betreiberUserView
|
||||
CreatedAt string
|
||||
@@ -101,7 +100,7 @@ func (s *Server) handleBetreiberAccountDetail(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
data := betreiberAccountDetailData{
|
||||
Title: "Account", Nav: navFor(r), AccountID: acc.ID, Name: acc.Name,
|
||||
Title: "Account", Nav: navFor(r), Name: acc.Name,
|
||||
CreatedAt: acc.CreatedAt.Format("02.01.2006 15:04"),
|
||||
}
|
||||
for _, u := range users {
|
||||
|
||||
@@ -11,15 +11,49 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
type indexData struct {
|
||||
Title string
|
||||
Nav navData
|
||||
Title string
|
||||
Nav navData
|
||||
OffeneAntraege int
|
||||
ZeigePosteingang bool
|
||||
OffenerPosteingang int
|
||||
}
|
||||
|
||||
// handleIndex ist die Startseite nach der Anmeldung. Platzhalter für
|
||||
// Phase 1 (Datenmodell/Regelwerk/Katalogstruktur) — der geführte
|
||||
// Fragebogen (Ebene 2, "Antrag stellen") ist Phase 2 der Baureihenfolge.
|
||||
// handleIndex ist die Startseite nach der Anmeldung — ein kurzer
|
||||
// Überblick statt reiner Navigations-Duplikate (die Links stehen
|
||||
// eh schon in der Seitenleiste): eigene offene Anträge, und für die
|
||||
// Fachebene zusätzlich die Zahl offener Fälle im Posteingang. Bewusst
|
||||
// ohne die Wiedervorlage-Zahl hier — deren Berechnung vergleicht jeden
|
||||
// aktiven Genehmigungs-Snapshot gegen den aktuellen Katalogstand
|
||||
// (siehe handleWiedervorlageListe) und wäre für eine Startseite, die
|
||||
// bei jedem Login geladen wird, zu teuer.
|
||||
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||
data := indexData{Title: "Start", Nav: navFor(r)}
|
||||
user := currentUser(r)
|
||||
nav := navFor(r)
|
||||
data := indexData{Title: "Start", Nav: nav}
|
||||
|
||||
if !nav.IsBetreiber {
|
||||
eigene, err := s.store.ListAntraegeForUser(r.Context(), user.ID)
|
||||
if err == nil {
|
||||
for _, a := range eigene {
|
||||
if a.Status == "eingereicht" {
|
||||
data.OffeneAntraege++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if nav.IsFachebene {
|
||||
data.ZeigePosteingang = true
|
||||
alle, err := s.store.ListAntraegeForAccount(r.Context(), user.AccountID)
|
||||
if err == nil {
|
||||
for _, a := range alle {
|
||||
if a.Status == "eingereicht" {
|
||||
data.OffenerPosteingang++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.templates.ExecuteTemplate(w, "index", data); err != nil {
|
||||
http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
63
internal/web/handlers_test.go
Normal file
63
internal/web/handlers_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package web_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIndexZeigtEigeneOffeneAntraege(t *testing.T) {
|
||||
s, _, cookie := newAuthedTestServer(t)
|
||||
postForm(t, s, cookie, "/antraege", fullAntragForm())
|
||||
|
||||
resp := getWithCookie(t, s, cookie, "/")
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String())
|
||||
}
|
||||
if !strings.Contains(resp.Body.String(), `href="/antraege">Meine offenen Anträge <span class="status">1</span>`) {
|
||||
t.Errorf("expected 1 offenen Antrag auf der Startseite, got: %s", resp.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexZeigtPosteingangNurFuerFachebene(t *testing.T) {
|
||||
fs := newFakeStore()
|
||||
s := newServer(t, fs)
|
||||
_, verantwortlicherCookie := seedFallImAccount(t, fs, s, "verantwortlicher")
|
||||
|
||||
resp := getWithCookie(t, s, verantwortlicherCookie, "/")
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String())
|
||||
}
|
||||
if !strings.Contains(resp.Body.String(), `href="/faelle">Posteingang <span class="status">1</span>`) {
|
||||
t.Errorf("expected 1 offenen Fall im Posteingang auf der Startseite, got: %s", resp.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexZeigtKeinenPosteingangFuerMitarbeiter(t *testing.T) {
|
||||
s, _, cookie := newAuthedTestServer(t)
|
||||
|
||||
resp := getWithCookie(t, s, cookie, "/")
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String())
|
||||
}
|
||||
if strings.Contains(resp.Body.String(), "Posteingang") {
|
||||
t.Errorf("expected no Posteingang tile for mitarbeiter, got: %s", resp.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexZeigtPlattformLinkFuerBetreiber(t *testing.T) {
|
||||
fs := newFakeStore()
|
||||
s := newServer(t, fs)
|
||||
betreiberCookie := seedAccountWithRole(t, fs, "Deklarix Betreiber", "betreiber@example.com", "betreiber")
|
||||
|
||||
resp := getWithCookie(t, s, betreiberCookie, "/")
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String())
|
||||
}
|
||||
if !strings.Contains(resp.Body.String(), `href="/betreiber">Zur Plattform`) {
|
||||
t.Errorf("expected a Plattform link for betreiber, got: %s", resp.Body.String())
|
||||
}
|
||||
if strings.Contains(resp.Body.String(), "Antrag stellen") {
|
||||
t.Errorf("expected no antrag tiles for betreiber, got: %s", resp.Body.String())
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,19 @@
|
||||
{{template "nav" .Nav}}
|
||||
<div class="page">
|
||||
<h1>Willkommen bei Deklarix</h1>
|
||||
{{if .Nav.IsBetreiber}}
|
||||
<ul class="admin-kacheln">
|
||||
<li><a href="/betreiber">Zur Plattform</a></li>
|
||||
</ul>
|
||||
{{else}}
|
||||
<ul class="admin-kacheln">
|
||||
<li><a href="/antraege/neu">Antrag stellen</a></li>
|
||||
<li><a href="/antraege">Meine Anträge</a></li>
|
||||
<li><a href="/antraege">Meine offenen Anträge <span class="status">{{.OffeneAntraege}}</span></a></li>
|
||||
{{if .ZeigePosteingang}}
|
||||
<li><a href="/faelle">Posteingang <span class="status">{{.OffenerPosteingang}}</span></a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,12 @@ bewusst nicht geraten, sondern hier notiert — vor Phase 3 (Ableitungen
|
||||
und harte Filter) mit dem fachlich Verantwortlichen (z. B. der/dem
|
||||
betrieblichen Datenschutzbeauftragten) klären.
|
||||
|
||||
## 1. Rangfolge der Datenklassen
|
||||
## 1. Rangfolge der Datenklassen — BESTÄTIGT (2026-08-29)
|
||||
|
||||
Die Spezifikation sagt "höchste zutreffende Stufe gewinnt", gibt aber
|
||||
keine expliziten Rangzahlen vor. `rules/datenklasse.yaml` nimmt aktuell
|
||||
diese Reihenfolge an (niedrigster zu höchstem Rang):
|
||||
keine expliziten Rangzahlen vor. `rules/datenklasse.yaml` nimmt diese
|
||||
Reihenfolge an (niedrigster zu höchstem Rang) — **vom Produktverantwortlichen
|
||||
bestätigt, keine Änderung nötig:**
|
||||
|
||||
1. `oeffentlich`
|
||||
2. `intern`
|
||||
@@ -18,22 +19,22 @@ diese Reihenfolge an (niedrigster zu höchstem Rang):
|
||||
5. `berufsgeheimnis`
|
||||
6. `besondere_kategorie`
|
||||
|
||||
Begründung der Annahme: DSGVO Art. 9 (besondere Kategorien) gilt
|
||||
allgemein als striktester Datenschutz-Tatbestand, § 203 StGB
|
||||
(Berufsgeheimnis) hat eigene strafrechtliche Relevanz — beide vor
|
||||
"normalen" personenbezogenen Daten eingeordnet. `auftragsdaten` unter
|
||||
`personenbezogen` einsortiert, weil vertragliche Geheimhaltung in der
|
||||
Regel schwächer sanktioniert ist als DSGVO-Bußgelder. **Nicht
|
||||
bestätigt.**
|
||||
Begründung: DSGVO Art. 9 (besondere Kategorien) gilt allgemein als
|
||||
striktester Datenschutz-Tatbestand, § 203 StGB (Berufsgeheimnis) hat
|
||||
eigene strafrechtliche Relevanz — beide vor "normalen" personenbezogenen
|
||||
Daten eingeordnet. `auftragsdaten` unter `personenbezogen` einsortiert,
|
||||
weil vertragliche Geheimhaltung in der Regel schwächer sanktioniert ist
|
||||
als DSGVO-Bußgelder.
|
||||
|
||||
## 2. Anforderung `kein_training_auf_eingabe` und `auftragsdaten`
|
||||
## 2. Anforderung `kein_training_auf_eingabe` und `auftragsdaten` — BESTÄTIGT (2026-08-29)
|
||||
|
||||
Die Spezifikation formuliert "aus intern, personenbezogen und höher" —
|
||||
das lässt offen, ob `auftragsdaten` (zwischen `intern` und
|
||||
`personenbezogen` einsortiert, siehe Punkt 1) eingeschlossen sein soll.
|
||||
`rules/anforderungen.yaml` schließt `auftragsdaten` aktuell explizit
|
||||
ein (Kundendaten sollten aus denselben Gründen wie Geschäftsgeheimnisse
|
||||
nicht zum Training verwendet werden) — **Annahme, nicht bestätigt.**
|
||||
`rules/anforderungen.yaml` schließt `auftragsdaten` ein (Kundendaten
|
||||
sollten aus denselben Gründen wie Geschäftsgeheimnisse nicht zum
|
||||
Training verwendet werden) — **vom Produktverantwortlichen bestätigt,
|
||||
keine Änderung nötig.**
|
||||
|
||||
## 3. Genaue Fragebogen-Fakten für "verboten" (KI-VO Art. 5) — ERLEDIGT
|
||||
|
||||
@@ -43,9 +44,9 @@ jetzt eine Folgefrage `c3_art` mit den Werten
|
||||
`emotionserkennung_arbeitsplatz`, `social_scoring`,
|
||||
`biometrische_kategorisierung` und `keine` — exakt die Werte, die
|
||||
`rules/kivo_einstufung.yaml` bereits erwartete. Feldnamen sind damit
|
||||
final, nicht mehr Platzhalter. **Weiterhin offen:** ob diese drei
|
||||
Varianten fachlich vollständig sind (deckt das wirklich alle "verboten"-
|
||||
Fälle aus Art. 5 KI-VO ab?) — das war nie Teil dieser Klärung.
|
||||
final, nicht mehr Platzhalter. Ob diese drei Varianten fachlich
|
||||
vollständig sind, wurde am 2026-08-29 vom Produktverantwortlichen
|
||||
**bestätigt (keine Ergänzung nötig).**
|
||||
|
||||
## 4. `loeschfrist_max_tage` — konkrete Fristen je Datenklasse
|
||||
|
||||
@@ -60,15 +61,15 @@ wird diese Anforderung aktuell NICHT hart gegen Werkzeuge gefiltert
|
||||
(jedes Werkzeug gilt hier als "erfüllt") — sobald Fristen feststehen,
|
||||
muss die Filterfunktion entsprechend erweitert werden.
|
||||
|
||||
## 5. Fallback, wenn keine Datenklasse zutrifft
|
||||
## 5. Fallback, wenn keine Datenklasse zutrifft — BESTÄTIGT (2026-08-29)
|
||||
|
||||
Wenn ein Antrag bei allen B-Fragen "nein" beantwortet — auch bei B7
|
||||
("nur allgemein zugängliche/erfundene Inhalte") — trifft laut Tabelle
|
||||
keine Stufe zu (ein eigentlich widersprüchlicher Zustand: irgendeine
|
||||
Kategorie sollte immer zutreffen). `internal/rules.EvaluateDatenklasse`
|
||||
nimmt in diesem Fall konservativ `intern` an, nicht `oeffentlich` — im
|
||||
Zweifel mehr Schutzanforderungen, nicht weniger. **Nicht fachlich
|
||||
bestätigt**, nur eine sichere Standardannahme.
|
||||
Zweifel mehr Schutzanforderungen, nicht weniger. **Vom
|
||||
Produktverantwortlichen bestätigt, keine Änderung nötig.**
|
||||
|
||||
## 6. Welche Anforderungen werden hart gegen ein Werkzeug gefiltert?
|
||||
|
||||
|
||||
Reference in New Issue
Block a user