feat: Werkzeugkatalog nennt tatsächliche Verarbeitungsländer statt EU/USA/gemischt-Eimer, plus DPF-Feld

Die USA sind DSGVO-rechtlich bereits ein Drittland wie jedes andere -
der alte verarbeitungsort-Wertebereich (EU/USA/gemischt/on-prem)
verschleierte das. Ersetzt durch verarbeitungslaender TEXT[] mit
echter Weltländerliste im Formular (Mehrfachauswahl, kein JS nötig).
Der harte Filter eu_verarbeitung verlangt jetzt, dass ALLE genannten
Länder EU/EWR sind. Neues Feld dpf_zertifiziert macht die EU-US Data
Privacy Framework-Zertifizierung strukturiert statt nur Fließtext.
Öffnet nebenbei den Weg für Katalogeinträge außerhalb EU/USA (z. B.
DeepSeek), die vorher am alten Wertebereich scheiterten.

Zusätzlich Typografie-Nachbesserung nach Nutzer-Feedback: Formular-
Abschnittsüberschriften (<legend>) saßen zu dicht am Kartenrand, weil
<legend> das Padding des umschließenden <fieldset> ignoriert - jetzt
mit eigenem Padding und größerer Schrift, h2/h3 einheitlich gesetzt.
This commit is contained in:
noroot
2026-08-31 13:52:00 +02:00
parent 2e459f1dd1
commit 59e1bcc8f7
18 changed files with 425 additions and 192 deletions

View File

@@ -0,0 +1,20 @@
ALTER TABLE werkzeug ADD COLUMN verarbeitungsort TEXT;
-- Ein Rückbau von Freitext-Ländern auf den alten Vier-Werte-Eimer ist
-- verlustbehaftet (welches Land genau "gemischt" war, geht verloren) —
-- bestmögliche Näherung statt Datenverlust durch NULL.
UPDATE werkzeug SET verarbeitungsort =
CASE
WHEN verarbeitungslaender = ARRAY['On-Premise (selbst gehostet)'] THEN 'on-prem'
WHEN verarbeitungslaender = ARRAY['USA'] THEN 'USA'
WHEN array_length(verarbeitungslaender, 1) IS NULL OR array_length(verarbeitungslaender, 1) = 0 THEN 'gemischt'
WHEN 'USA' = ANY(verarbeitungslaender) THEN 'gemischt'
ELSE 'EU'
END;
ALTER TABLE werkzeug ALTER COLUMN verarbeitungsort SET NOT NULL;
ALTER TABLE werkzeug ADD CONSTRAINT werkzeug_verarbeitungsort_check
CHECK (verarbeitungsort IN ('EU', 'USA', 'gemischt', 'on-prem'));
ALTER TABLE werkzeug DROP COLUMN verarbeitungslaender;
ALTER TABLE werkzeug DROP COLUMN dpf_zertifiziert;

View File

@@ -0,0 +1,29 @@
-- "EU"/"USA"/"gemischt"/"on-prem" als grobe Eimer verschleierten, dass
-- auch die USA bereits ein Drittland sind (DSGVO Art. 44 ff. kennt nur
-- EU/EWR vs. Drittland) und dass "gemischt" nichts über die tatsächlich
-- beteiligten Länder aussagt (z. B. Irland+USA bei Microsoft 365
-- Copilot vs. Frankreich bei Mistral) — beides macht einen Unterschied
-- für die Einschätzung, nicht nur für die Anzeige. Ersetzt durch eine
-- Liste tatsächlicher Länder (oder einer Region, wenn der Anbieter
-- selbst keinen einzelnen Staat benennt, z. B. "EU-Region" bei
-- OpenAI/Microsoft).
ALTER TABLE werkzeug ADD COLUMN verarbeitungslaender TEXT[] NOT NULL DEFAULT '{}';
ALTER TABLE werkzeug DROP CONSTRAINT werkzeug_verarbeitungsort_check;
UPDATE werkzeug SET verarbeitungslaender =
CASE verarbeitungsort
WHEN 'EU' THEN ARRAY['EU-Region (Land nicht migriert, bitte prüfen)']
WHEN 'USA' THEN ARRAY['USA']
WHEN 'on-prem' THEN ARRAY['On-Premise (selbst gehostet)']
ELSE ARRAY['nicht spezifiziert (bitte prüfen)']
END;
ALTER TABLE werkzeug DROP COLUMN verarbeitungsort;
-- Ob ein US-Anbieter unter dem EU-US Data Privacy Framework zertifiziert
-- ist (schmalere, für Schrems-Nachfolgeverfahren anfällige Rechtsgrundlage)
-- oder sich rein auf Standardvertragsklauseln stützt, stand bisher nur als
-- Fließtext in "einschraenkungen" — jetzt strukturiert und damit künftig
-- filterbar.
ALTER TABLE werkzeug ADD COLUMN dpf_zertifiziert BOOLEAN NOT NULL DEFAULT false;

View File

@@ -16,15 +16,27 @@ import (
// LetztePruefung und Quelle sind Pflicht — jede Zusicherung im Katalog
// muss belegbar sein.
type Werkzeug struct {
ID string
AccountID *string
Name string
Anbieter string
Verarbeitungsort string
AVVVerfuegbar bool
AVVURL string
TrainingOptOut bool
TrainingStandard bool
ID string
AccountID *string
Name string
Anbieter string
// Verarbeitungslaender nennt die tatsächlichen Länder (oder, wenn
// der Anbieter selbst keinen einzelnen Staat zusichert, eine Region
// wie "EU-Region") statt eines groben EU/USA/gemischt-Eimers — die
// USA sind DSGVO-rechtlich bereits ein Drittland wie jedes andere,
// "gemischt" verschleierte, welche Länder konkret beteiligt sind.
Verarbeitungslaender []string
AVVVerfuegbar bool
AVVURL string
TrainingOptOut bool
TrainingStandard bool
// DPFZertifiziert: EU-US Data Privacy Framework — schmalere,
// gerichtlich schon zweimal gekippte Rechtsgrundlage (Safe Harbor,
// Privacy Shield) für Transfers in die USA, gilt nur für zertifizierte
// Unternehmen. false bedeutet nicht "nicht zertifiziert" im Sinne von
// widerlegt, sondern "nicht als Zusicherung belegt" — wie bei den
// übrigen Booleans hier.
DPFZertifiziert bool
AufbewahrungTage *int // nil = vom Anbieter nicht beziffert, NICHT gleichbedeutend mit 0 Tagen
Zertifizierungen []string
Subprozessoren []string
@@ -40,25 +52,26 @@ type Werkzeug struct {
// Create/Update — bei 15 Feldern lesbarer als eine positionale
// Parameterliste.
type WerkzeugInput struct {
AccountID *string
Name string
Anbieter string
Verarbeitungsort string
AVVVerfuegbar bool
AVVURL string
TrainingOptOut bool
TrainingStandard bool
AufbewahrungTage *int
Zertifizierungen []string
Subprozessoren []string
GeeigneteZwecke []string
Einschraenkungen []string
LetztePruefung time.Time
Quelle string
AccountID *string
Name string
Anbieter string
Verarbeitungslaender []string
AVVVerfuegbar bool
AVVURL string
TrainingOptOut bool
TrainingStandard bool
DPFZertifiziert bool
AufbewahrungTage *int
Zertifizierungen []string
Subprozessoren []string
GeeigneteZwecke []string
Einschraenkungen []string
LetztePruefung time.Time
Quelle string
}
const werkzeugColumns = `id, account_id, name, anbieter, verarbeitungsort, avv_verfuegbar, avv_url,
training_opt_out, training_standard, aufbewahrung_tage, zertifizierungen, subprozessoren,
const werkzeugColumns = `id, account_id, name, anbieter, verarbeitungslaender, avv_verfuegbar, avv_url,
training_opt_out, training_standard, dpf_zertifiziert, aufbewahrung_tage, zertifizierungen, subprozessoren,
geeignete_zwecke, einschraenkungen, letzte_pruefung, quelle, created_at, updated_at`
func scanWerkzeug(row interface {
@@ -66,8 +79,8 @@ func scanWerkzeug(row interface {
}) (Werkzeug, error) {
var w Werkzeug
err := row.Scan(
&w.ID, &w.AccountID, &w.Name, &w.Anbieter, &w.Verarbeitungsort, &w.AVVVerfuegbar, &w.AVVURL,
&w.TrainingOptOut, &w.TrainingStandard, &w.AufbewahrungTage, &w.Zertifizierungen, &w.Subprozessoren,
&w.ID, &w.AccountID, &w.Name, &w.Anbieter, &w.Verarbeitungslaender, &w.AVVVerfuegbar, &w.AVVURL,
&w.TrainingOptOut, &w.TrainingStandard, &w.DPFZertifiziert, &w.AufbewahrungTage, &w.Zertifizierungen, &w.Subprozessoren,
&w.GeeigneteZwecke, &w.Einschraenkungen, &w.LetztePruefung, &w.Quelle, &w.CreatedAt, &w.UpdatedAt,
)
return w, err
@@ -79,6 +92,9 @@ func scanWerkzeug(row interface {
// einer wenig hilfreichen Constraint-Fehlermeldung abgelehnt (derselbe
// Fall wie früher bei finding.sources).
func normalizeWerkzeugSlices(in *WerkzeugInput) {
if in.Verarbeitungslaender == nil {
in.Verarbeitungslaender = []string{}
}
if in.Zertifizierungen == nil {
in.Zertifizierungen = []string{}
}
@@ -98,13 +114,13 @@ func (s *Store) CreateWerkzeug(ctx context.Context, in WerkzeugInput) (Werkzeug,
normalizeWerkzeugSlices(&in)
row := s.Pool.QueryRow(ctx, `
INSERT INTO werkzeug (
account_id, name, anbieter, verarbeitungsort, avv_verfuegbar, avv_url,
training_opt_out, training_standard, aufbewahrung_tage, zertifizierungen, subprozessoren,
account_id, name, anbieter, verarbeitungslaender, avv_verfuegbar, avv_url,
training_opt_out, training_standard, dpf_zertifiziert, aufbewahrung_tage, zertifizierungen, subprozessoren,
geeignete_zwecke, einschraenkungen, letzte_pruefung, quelle
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
RETURNING `+werkzeugColumns,
in.AccountID, in.Name, in.Anbieter, in.Verarbeitungsort, in.AVVVerfuegbar, in.AVVURL,
in.TrainingOptOut, in.TrainingStandard, in.AufbewahrungTage, in.Zertifizierungen, in.Subprozessoren,
in.AccountID, in.Name, in.Anbieter, in.Verarbeitungslaender, in.AVVVerfuegbar, in.AVVURL,
in.TrainingOptOut, in.TrainingStandard, in.DPFZertifiziert, in.AufbewahrungTage, in.Zertifizierungen, in.Subprozessoren,
in.GeeigneteZwecke, in.Einschraenkungen, in.LetztePruefung, in.Quelle,
)
w, err := scanWerkzeug(row)
@@ -123,14 +139,14 @@ func (s *Store) UpdateWerkzeug(ctx context.Context, id string, in WerkzeugInput)
normalizeWerkzeugSlices(&in)
row := s.Pool.QueryRow(ctx, `
UPDATE werkzeug SET
name = $2, anbieter = $3, verarbeitungsort = $4, avv_verfuegbar = $5, avv_url = $6,
training_opt_out = $7, training_standard = $8, aufbewahrung_tage = $9,
zertifizierungen = $10, subprozessoren = $11, geeignete_zwecke = $12, einschraenkungen = $13,
letzte_pruefung = $14, quelle = $15, updated_at = now()
name = $2, anbieter = $3, verarbeitungslaender = $4, avv_verfuegbar = $5, avv_url = $6,
training_opt_out = $7, training_standard = $8, dpf_zertifiziert = $9, aufbewahrung_tage = $10,
zertifizierungen = $11, subprozessoren = $12, geeignete_zwecke = $13, einschraenkungen = $14,
letzte_pruefung = $15, quelle = $16, updated_at = now()
WHERE id = $1
RETURNING `+werkzeugColumns,
id, in.Name, in.Anbieter, in.Verarbeitungsort, in.AVVVerfuegbar, in.AVVURL,
in.TrainingOptOut, in.TrainingStandard, in.AufbewahrungTage, in.Zertifizierungen, in.Subprozessoren,
id, in.Name, in.Anbieter, in.Verarbeitungslaender, in.AVVVerfuegbar, in.AVVURL,
in.TrainingOptOut, in.TrainingStandard, in.DPFZertifiziert, in.AufbewahrungTage, in.Zertifizierungen, in.Subprozessoren,
in.GeeigneteZwecke, in.Einschraenkungen, in.LetztePruefung, in.Quelle,
)
w, err := scanWerkzeug(row)

View File

@@ -13,21 +13,21 @@ func intPtr(v int) *int { return &v }
func testWerkzeugInput(accountID *string, name string) store.WerkzeugInput {
return store.WerkzeugInput{
AccountID: accountID,
Name: name,
Anbieter: "Beispiel-Anbieter GmbH",
Verarbeitungsort: "EU",
AVVVerfuegbar: true,
AVVURL: "https://beispiel.example/avv",
TrainingOptOut: true,
TrainingStandard: true,
AufbewahrungTage: intPtr(30),
Zertifizierungen: []string{"ISO 27001"},
Subprozessoren: []string{"Beispiel-Subprozessor Inc."},
GeeigneteZwecke: []string{"Textgenerierung"},
Einschraenkungen: nil,
LetztePruefung: time.Now().Add(-24 * time.Hour).Truncate(time.Millisecond),
Quelle: "https://beispiel.example/beleg",
AccountID: accountID,
Name: name,
Anbieter: "Beispiel-Anbieter GmbH",
Verarbeitungslaender: []string{"Deutschland"},
AVVVerfuegbar: true,
AVVURL: "https://beispiel.example/avv",
TrainingOptOut: true,
TrainingStandard: true,
AufbewahrungTage: intPtr(30),
Zertifizierungen: []string{"ISO 27001"},
Subprozessoren: []string{"Beispiel-Subprozessor Inc."},
GeeigneteZwecke: []string{"Textgenerierung"},
Einschraenkungen: nil,
LetztePruefung: time.Now().Add(-24 * time.Hour).Truncate(time.Millisecond),
Quelle: "https://beispiel.example/beleg",
}
}