Compare commits

...

2 Commits

Author SHA1 Message Date
noroot
9d2cb79424 fix: Startseite zeigt echte Übersicht statt Phase-1-Platzhalter
handleIndex war seit dem Produktwechsel unverändert ein Platzhalter aus
Schritt 1 ("Antrag stellen"/"Meine Anträge" — beides bereits in der
Seitenleiste vorhanden, die Startseite selbst zeigte nichts Eigenes).
Zeigt jetzt die Zahl eigener offener Anträge, und für die Fachebene
zusätzlich die Zahl offener Fälle im Posteingang; Betreiber sehen
einen Link zur Plattform statt leerer Antrags-Kacheln, die für sie
ohnehin nicht gelten. Bewusst ohne Wiedervorlage-Zahl — deren
Berechnung vergleicht jeden Genehmigungs-Snapshot gegen den aktuellen
Katalogstand und wäre für eine bei jedem Login geladene Startseite zu
teuer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 23:10:42 +02:00
noroot
4dd9bd88cb docs: fachliche Annahmen im Regelwerk bestätigt
Rangfolge der Datenklassen, auftragsdaten bei kein_training_auf_eingabe,
Vollständigkeit der drei "verboten"-Varianten und der intern-Fallback
wurden vom Produktverantwortlichen bestätigt (rules/OPEN.md). Weiterhin
offen: konkrete Löschfristen in Tagen, harte Filterung von
menschliche_aufsicht/kennzeichnungspflicht/dsfa_erforderlich.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 22:47:07 +02:00
5 changed files with 151 additions and 36 deletions

View File

@@ -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)**

View File

@@ -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)
} }

View 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())
}
}

View File

@@ -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>

View File

@@ -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?