Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d2cb79424 | ||
|
|
4dd9bd88cb |
26
CLAUDE.md
26
CLAUDE.md
@@ -734,16 +734,24 @@ journalctl -u deklarix -f
|
|||||||
|
|
||||||
## Offene Punkte
|
## Offene Punkte
|
||||||
|
|
||||||
- **Rangfolge der Datenklassen und Grenzfälle im Anforderungsprofil**
|
- ~~Rangfolge der Datenklassen~~ (`rules/OPEN.md`, Punkt 1) — **am
|
||||||
(siehe `rules/OPEN.md`, Punkte 1/2/5/6) — mit dem/der fachlich
|
2026-08-29 vom Produktverantwortlichen bestätigt**, keine Änderung
|
||||||
Verantwortlichen (z. B. Datenschutzbeauftragte/r) bestätigen. Schritt
|
nötig.
|
||||||
3 ist trotzdem schon umgesetzt (auf Basis dieser dokumentierten
|
- ~~Anforderung `kein_training_auf_eingabe` und `auftragsdaten`~~
|
||||||
Annahmen) — nicht auf die Klärung gewartet, um nicht blockiert zu
|
(`rules/OPEN.md`, Punkt 2) — **bestätigt**, `auftragsdaten` bleibt
|
||||||
bleiben, aber die Ableitung kann sich noch ändern.
|
eingeschlossen.
|
||||||
- ~~Genaue Fragebogen-Felder für die drei "verboten"-Varianten~~ —
|
- ~~Genaue Fragebogen-Felder für die drei "verboten"-Varianten~~ —
|
||||||
**erledigt in Schritt 2**, C3 hat jetzt eine Folgefrage `c3_art`.
|
**erledigt in Schritt 2**, C3 hat jetzt eine Folgefrage `c3_art`. Ob
|
||||||
Weiterhin offen: ob die drei Varianten fachlich vollständig sind
|
die drei Varianten fachlich vollständig sind, wurde am 2026-08-29
|
||||||
(siehe `rules/OPEN.md`, Punkt 3).
|
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
|
- **Löschfristen je Datenklasse** (konkrete Tageswerte für
|
||||||
`loeschfrist_max_tage`) — noch nicht fachlich festgelegt.
|
`loeschfrist_max_tage`) — noch nicht fachlich festgelegt.
|
||||||
- **Mandantenisolation auf Datenbankebene (Postgres Row-Level Security)**
|
- **Mandantenisolation auf Datenbankebene (Postgres Row-Level Security)**
|
||||||
|
|||||||
@@ -11,15 +11,49 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type indexData struct {
|
type indexData struct {
|
||||||
Title string
|
Title string
|
||||||
Nav navData
|
Nav navData
|
||||||
|
OffeneAntraege int
|
||||||
|
ZeigePosteingang bool
|
||||||
|
OffenerPosteingang int
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleIndex ist die Startseite nach der Anmeldung. Platzhalter für
|
// handleIndex ist die Startseite nach der Anmeldung — ein kurzer
|
||||||
// Phase 1 (Datenmodell/Regelwerk/Katalogstruktur) — der geführte
|
// Überblick statt reiner Navigations-Duplikate (die Links stehen
|
||||||
// Fragebogen (Ebene 2, "Antrag stellen") ist Phase 2 der Baureihenfolge.
|
// 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) {
|
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 {
|
if err := s.templates.ExecuteTemplate(w, "index", data); err != nil {
|
||||||
http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError)
|
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}}
|
{{template "nav" .Nav}}
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<h1>Willkommen bei Deklarix</h1>
|
<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">
|
<ul class="admin-kacheln">
|
||||||
<li><a href="/antraege/neu">Antrag stellen</a></li>
|
<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>
|
</ul>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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
|
und harte Filter) mit dem fachlich Verantwortlichen (z. B. der/dem
|
||||||
betrieblichen Datenschutzbeauftragten) klären.
|
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
|
Die Spezifikation sagt "höchste zutreffende Stufe gewinnt", gibt aber
|
||||||
keine expliziten Rangzahlen vor. `rules/datenklasse.yaml` nimmt aktuell
|
keine expliziten Rangzahlen vor. `rules/datenklasse.yaml` nimmt diese
|
||||||
diese Reihenfolge an (niedrigster zu höchstem Rang):
|
Reihenfolge an (niedrigster zu höchstem Rang) — **vom Produktverantwortlichen
|
||||||
|
bestätigt, keine Änderung nötig:**
|
||||||
|
|
||||||
1. `oeffentlich`
|
1. `oeffentlich`
|
||||||
2. `intern`
|
2. `intern`
|
||||||
@@ -18,22 +19,22 @@ diese Reihenfolge an (niedrigster zu höchstem Rang):
|
|||||||
5. `berufsgeheimnis`
|
5. `berufsgeheimnis`
|
||||||
6. `besondere_kategorie`
|
6. `besondere_kategorie`
|
||||||
|
|
||||||
Begründung der Annahme: DSGVO Art. 9 (besondere Kategorien) gilt
|
Begründung: DSGVO Art. 9 (besondere Kategorien) gilt allgemein als
|
||||||
allgemein als striktester Datenschutz-Tatbestand, § 203 StGB
|
striktester Datenschutz-Tatbestand, § 203 StGB (Berufsgeheimnis) hat
|
||||||
(Berufsgeheimnis) hat eigene strafrechtliche Relevanz — beide vor
|
eigene strafrechtliche Relevanz — beide vor "normalen" personenbezogenen
|
||||||
"normalen" personenbezogenen Daten eingeordnet. `auftragsdaten` unter
|
Daten eingeordnet. `auftragsdaten` unter `personenbezogen` einsortiert,
|
||||||
`personenbezogen` einsortiert, weil vertragliche Geheimhaltung in der
|
weil vertragliche Geheimhaltung in der Regel schwächer sanktioniert ist
|
||||||
Regel schwächer sanktioniert ist als DSGVO-Bußgelder. **Nicht
|
als DSGVO-Bußgelder.
|
||||||
bestätigt.**
|
|
||||||
|
|
||||||
## 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" —
|
Die Spezifikation formuliert "aus intern, personenbezogen und höher" —
|
||||||
das lässt offen, ob `auftragsdaten` (zwischen `intern` und
|
das lässt offen, ob `auftragsdaten` (zwischen `intern` und
|
||||||
`personenbezogen` einsortiert, siehe Punkt 1) eingeschlossen sein soll.
|
`personenbezogen` einsortiert, siehe Punkt 1) eingeschlossen sein soll.
|
||||||
`rules/anforderungen.yaml` schließt `auftragsdaten` aktuell explizit
|
`rules/anforderungen.yaml` schließt `auftragsdaten` ein (Kundendaten
|
||||||
ein (Kundendaten sollten aus denselben Gründen wie Geschäftsgeheimnisse
|
sollten aus denselben Gründen wie Geschäftsgeheimnisse nicht zum
|
||||||
nicht zum Training verwendet werden) — **Annahme, nicht bestätigt.**
|
Training verwendet werden) — **vom Produktverantwortlichen bestätigt,
|
||||||
|
keine Änderung nötig.**
|
||||||
|
|
||||||
## 3. Genaue Fragebogen-Fakten für "verboten" (KI-VO Art. 5) — ERLEDIGT
|
## 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`,
|
`emotionserkennung_arbeitsplatz`, `social_scoring`,
|
||||||
`biometrische_kategorisierung` und `keine` — exakt die Werte, die
|
`biometrische_kategorisierung` und `keine` — exakt die Werte, die
|
||||||
`rules/kivo_einstufung.yaml` bereits erwartete. Feldnamen sind damit
|
`rules/kivo_einstufung.yaml` bereits erwartete. Feldnamen sind damit
|
||||||
final, nicht mehr Platzhalter. **Weiterhin offen:** ob diese drei
|
final, nicht mehr Platzhalter. Ob diese drei Varianten fachlich
|
||||||
Varianten fachlich vollständig sind (deckt das wirklich alle "verboten"-
|
vollständig sind, wurde am 2026-08-29 vom Produktverantwortlichen
|
||||||
Fälle aus Art. 5 KI-VO ab?) — das war nie Teil dieser Klärung.
|
**bestätigt (keine Ergänzung nötig).**
|
||||||
|
|
||||||
## 4. `loeschfrist_max_tage` — konkrete Fristen je Datenklasse
|
## 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,
|
(jedes Werkzeug gilt hier als "erfüllt") — sobald Fristen feststehen,
|
||||||
muss die Filterfunktion entsprechend erweitert werden.
|
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
|
Wenn ein Antrag bei allen B-Fragen "nein" beantwortet — auch bei B7
|
||||||
("nur allgemein zugängliche/erfundene Inhalte") — trifft laut Tabelle
|
("nur allgemein zugängliche/erfundene Inhalte") — trifft laut Tabelle
|
||||||
keine Stufe zu (ein eigentlich widersprüchlicher Zustand: irgendeine
|
keine Stufe zu (ein eigentlich widersprüchlicher Zustand: irgendeine
|
||||||
Kategorie sollte immer zutreffen). `internal/rules.EvaluateDatenklasse`
|
Kategorie sollte immer zutreffen). `internal/rules.EvaluateDatenklasse`
|
||||||
nimmt in diesem Fall konservativ `intern` an, nicht `oeffentlich` — im
|
nimmt in diesem Fall konservativ `intern` an, nicht `oeffentlich` — im
|
||||||
Zweifel mehr Schutzanforderungen, nicht weniger. **Nicht fachlich
|
Zweifel mehr Schutzanforderungen, nicht weniger. **Vom
|
||||||
bestätigt**, nur eine sichere Standardannahme.
|
Produktverantwortlichen bestätigt, keine Änderung nötig.**
|
||||||
|
|
||||||
## 6. Welche Anforderungen werden hart gegen ein Werkzeug gefiltert?
|
## 6. Welche Anforderungen werden hart gegen ein Werkzeug gefiltert?
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user