feat: Add Compliance Wiki as internal admin knowledge base

Migration 040 with wiki_categories + wiki_articles tables, 10 seed
articles across 8 categories (DSGVO, Art. 9, AVV, HinSchG etc.).
Read-only FastAPI API, Next.js proxy, and two-column frontend with
full-text search.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-09 20:01:27 +01:00
parent 3d22935065
commit 11d4c2fd36
8 changed files with 1379 additions and 1 deletions

View File

@@ -32,6 +32,7 @@ from .incident_routes import router as incident_router
from .change_request_routes import router as change_request_router
from .generation_routes import router as generation_router
from .project_routes import router as project_router
from .wiki_routes import router as wiki_router
# Include sub-routers
router.include_router(audit_router)
@@ -65,6 +66,7 @@ router.include_router(incident_router)
router.include_router(change_request_router)
router.include_router(generation_router)
router.include_router(project_router)
router.include_router(wiki_router)
__all__ = [
"router",
@@ -98,4 +100,5 @@ __all__ = [
"change_request_router",
"generation_router",
"project_router",
"wiki_router",
]

View File

@@ -0,0 +1,218 @@
"""
FastAPI routes for Compliance Wiki (read-only knowledge base).
Endpoints:
- GET /v1/wiki/categories → All categories with article counts
- GET /v1/wiki/articles → All articles (optional category filter)
- GET /v1/wiki/articles/{id} → Single article
- GET /v1/wiki/search → Full-text search (PostgreSQL tsvector)
"""
import logging
from typing import Optional
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel
from sqlalchemy import text
from database import SessionLocal
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/v1/wiki", tags=["wiki"])
# =============================================================================
# RESPONSE MODELS
# =============================================================================
class WikiCategoryResponse(BaseModel):
id: str
name: str
description: str
icon: str
sort_order: int
article_count: int
class WikiArticleResponse(BaseModel):
id: str
category_id: str
category_name: str
title: str
summary: str
content: str
legal_refs: list[str]
tags: list[str]
relevance: str
source_urls: list[str]
version: int
updated_at: str
class WikiSearchResultResponse(BaseModel):
id: str
title: str
summary: str
category_name: str
relevance: str
highlight: str
# =============================================================================
# HELPERS
# =============================================================================
def _article_row_to_response(row) -> dict:
"""Convert a DB row to WikiArticleResponse dict."""
return {
"id": row.id,
"category_id": row.category_id,
"category_name": getattr(row, "category_name", ""),
"title": row.title,
"summary": row.summary,
"content": row.content,
"legal_refs": list(row.legal_refs) if row.legal_refs else [],
"tags": list(row.tags) if row.tags else [],
"relevance": row.relevance or "info",
"source_urls": list(row.source_urls) if row.source_urls else [],
"version": row.version or 1,
"updated_at": row.updated_at.isoformat() if row.updated_at else "",
}
# =============================================================================
# ENDPOINTS
# =============================================================================
@router.get("/categories")
async def list_categories():
"""List all wiki categories with article counts."""
db = SessionLocal()
try:
result = db.execute(text("""
SELECT c.id, c.name, c.description, c.icon, c.sort_order,
COUNT(a.id) AS article_count
FROM compliance_wiki_categories c
LEFT JOIN compliance_wiki_articles a ON a.category_id = c.id
GROUP BY c.id, c.name, c.description, c.icon, c.sort_order
ORDER BY c.sort_order
"""))
rows = result.fetchall()
return {
"categories": [
{
"id": row.id,
"name": row.name,
"description": row.description or "",
"icon": row.icon or "",
"sort_order": row.sort_order or 0,
"article_count": row.article_count or 0,
}
for row in rows
]
}
finally:
db.close()
@router.get("/articles")
async def list_articles(
category_id: Optional[str] = Query(None, description="Filter by category"),
):
"""List all wiki articles, optionally filtered by category."""
db = SessionLocal()
try:
if category_id:
result = db.execute(text("""
SELECT a.*, c.name AS category_name
FROM compliance_wiki_articles a
JOIN compliance_wiki_categories c ON c.id = a.category_id
WHERE a.category_id = :category_id
ORDER BY
CASE a.relevance
WHEN 'critical' THEN 0
WHEN 'important' THEN 1
ELSE 2
END,
a.title
"""), {"category_id": category_id})
else:
result = db.execute(text("""
SELECT a.*, c.name AS category_name
FROM compliance_wiki_articles a
JOIN compliance_wiki_categories c ON c.id = a.category_id
ORDER BY c.sort_order,
CASE a.relevance
WHEN 'critical' THEN 0
WHEN 'important' THEN 1
ELSE 2
END,
a.title
"""))
rows = result.fetchall()
return {
"articles": [_article_row_to_response(row) for row in rows],
"total": len(rows),
}
finally:
db.close()
@router.get("/articles/{article_id}")
async def get_article(article_id: str):
"""Get a single wiki article by ID."""
db = SessionLocal()
try:
result = db.execute(text("""
SELECT a.*, c.name AS category_name
FROM compliance_wiki_articles a
JOIN compliance_wiki_categories c ON c.id = a.category_id
WHERE a.id = :article_id
"""), {"article_id": article_id})
row = result.fetchone()
if not row:
raise HTTPException(status_code=404, detail="Article not found")
return _article_row_to_response(row)
finally:
db.close()
@router.get("/search")
async def search_wiki(
q: str = Query(..., min_length=2, description="Search query"),
):
"""Full-text search across wiki articles using PostgreSQL tsvector."""
db = SessionLocal()
try:
result = db.execute(text("""
SELECT a.id, a.title, a.summary, a.relevance,
c.name AS category_name,
ts_headline('german', a.content, plainto_tsquery('german', :query),
'MaxWords=40, MinWords=20, StartSel=**, StopSel=**') AS highlight
FROM compliance_wiki_articles a
JOIN compliance_wiki_categories c ON c.id = a.category_id
WHERE to_tsvector('german', a.title || ' ' || a.summary || ' ' || a.content)
@@ plainto_tsquery('german', :query)
ORDER BY
ts_rank(to_tsvector('german', a.title || ' ' || a.summary || ' ' || a.content),
plainto_tsquery('german', :query)) DESC
LIMIT 20
"""), {"query": q})
rows = result.fetchall()
return {
"results": [
{
"id": row.id,
"title": row.title,
"summary": row.summary,
"category_name": row.category_name,
"relevance": row.relevance or "info",
"highlight": row.highlight or "",
}
for row in rows
],
"total": len(rows),
"query": q,
}
finally:
db.close()

View File

@@ -0,0 +1,465 @@
-- Migration 040: Compliance Wiki (Strukturierte Wissensbasis)
-- Interne Admin-Wissensbasis fuer DSGVO/Compliance-Fachwissen.
-- System-Eintraege (read-only), kein tenant_id — globale Daten.
-- =============================================================================
-- 1. Wiki-Kategorien
-- =============================================================================
CREATE TABLE IF NOT EXISTS compliance_wiki_categories (
id VARCHAR(100) PRIMARY KEY,
name VARCHAR(300) NOT NULL,
description TEXT DEFAULT '',
icon VARCHAR(50) DEFAULT '',
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- =============================================================================
-- 2. Wiki-Artikel
-- =============================================================================
CREATE TABLE IF NOT EXISTS compliance_wiki_articles (
id VARCHAR(100) PRIMARY KEY,
category_id VARCHAR(100) NOT NULL REFERENCES compliance_wiki_categories(id),
title VARCHAR(500) NOT NULL,
summary TEXT NOT NULL,
content TEXT NOT NULL,
legal_refs TEXT[] DEFAULT '{}',
tags TEXT[] DEFAULT '{}',
relevance VARCHAR(20) DEFAULT 'info',
source_urls TEXT[] DEFAULT '{}',
version INTEGER DEFAULT 1,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_wiki_articles_category ON compliance_wiki_articles(category_id);
CREATE INDEX IF NOT EXISTS idx_wiki_articles_tags ON compliance_wiki_articles USING GIN(tags);
CREATE INDEX IF NOT EXISTS idx_wiki_articles_search ON compliance_wiki_articles
USING GIN(to_tsvector('german', title || ' ' || summary || ' ' || content));
-- =============================================================================
-- 3. Seed-Daten: Kategorien
-- =============================================================================
INSERT INTO compliance_wiki_categories (id, name, description, icon, sort_order) VALUES
('datenkategorien', 'Datenkategorien & Abgrenzung', 'Welche personenbezogenen Daten gibt es und wie grenzt man sie voneinander ab?', 'Database', 10),
('dsgvo-grundlagen', 'DSGVO-Grundlagen', 'Grundlegende Konzepte der Datenschutz-Grundverordnung', 'Shield', 20),
('art9-besondere', 'Besondere Kategorien (Art. 9)', 'Besonders schuetzenswerte Daten nach Art. 9 DSGVO', 'AlertTriangle', 30),
('rechtsgrundlagen', 'Rechtsgrundlagen', 'Die sechs Rechtsgrundlagen fuer die Datenverarbeitung', 'Scale', 40),
('avv-dienstleister', 'Auftragsverarbeitung (AVV)', 'Regeln fuer externe Dienstleister, die Daten verarbeiten', 'Handshake', 50),
('arbeitsrecht', 'Arbeitsrecht & Compliance', 'Datenschutz im Arbeitsverhaeltnis', 'Briefcase', 60),
('hinschg', 'Hinweisgeberschutz (HinSchG)', 'Pflichten zum Schutz von Hinweisgebern', 'MessageCircle', 70),
('branchenspezifisch', 'Branchenspezifisches', 'Besonderheiten einzelner Branchen', 'Building2', 80)
ON CONFLICT (id) DO NOTHING;
-- =============================================================================
-- 4. Seed-Daten: Artikel
-- =============================================================================
-- 1. Gesundheitsdaten — Abgrenzung
INSERT INTO compliance_wiki_articles (id, category_id, title, summary, content, legal_refs, tags, relevance, source_urls) VALUES
('gesundheitsdaten-abgrenzung', 'datenkategorien',
'Gesundheitsdaten — Was zaehlt dazu und was nicht?',
'Nicht alles, was mit Gesundheit zu tun hat, ist automatisch ein Gesundheitsdatum im Sinne der DSGVO. Die Abgrenzung ist in der Praxis wichtig.',
'## Ueberblick
Der Name der Krankenkasse (z.B. "AOK Bayern", "TK") ist **kein Gesundheitsdatum** nach Art. 9 DSGVO. Er verraet nichts ueber den Gesundheitszustand einer Person — jeder Arbeitnehmer hat eine Krankenkasse, unabhaengig davon ob er gesund oder krank ist.
## Was SIND Gesundheitsdaten?
- Diagnosen, Krankheitsbilder, Befunde
- Krankmeldungen (AU-Bescheinigungen) mit Diagnose
- Schwerbehindertenausweis / Grad der Behinderung
- Medikamenteneinnahme
- Ergebnisse von Eignungsuntersuchungen
## Was sind KEINE Gesundheitsdaten?
- Name der Krankenkasse (reine Verwaltungsinformation)
- Anzahl Krankheitstage (ohne Diagnose)
- Versichertennummer
- Beitragssatz
## Warum ist das wichtig?
Gesundheitsdaten unterliegen dem besonderen Schutz nach Art. 9 DSGVO. Fuer ihre Verarbeitung braucht man eine **ausdrueckliche Rechtsgrundlage** (z.B. § 26 Abs. 3 BDSG im Beschaeftigungsverhaeltnis). Verwaltungsdaten wie der Krankenkassenname fallen unter die normalen Regeln.
## Praxis-Tipp
Wenn Sie im VVT oder in der DSFA Datenkategorien zuordnen: Pruefen Sie genau, ob ein Datum tatsaechlich Rueckschluesse auf den Gesundheitszustand zulaesst. Nur dann ist es ein Art.-9-Datum.',
ARRAY['Art. 9 DSGVO', '§ 26 Abs. 3 BDSG', 'ErwGr. 35 DSGVO'],
ARRAY['gesundheit', 'art9', 'abgrenzung', 'krankenkasse'],
'critical',
ARRAY['https://www.bfdi.bund.de', 'EuGH C-184/20'])
ON CONFLICT (id) DO NOTHING;
-- 2. Beschaeftigtendaten — Umfang
INSERT INTO compliance_wiki_articles (id, category_id, title, summary, content, legal_refs, tags, relevance, source_urls) VALUES
('beschaeftigtendaten-umfang', 'datenkategorien',
'Beschaeftigtendaten — Was gehoert alles dazu?',
'Beschaeftigtendaten umfassen weit mehr als Name und Adresse. Hier eine Uebersicht der typischen Datenkategorien im Arbeitsverhaeltnis.',
'## Ueberblick
Im Arbeitsverhaeltnis fallen viele verschiedene personenbezogene Daten an. Sie alle unterliegen dem Beschaeftigtendatenschutz nach § 26 BDSG.
## Typische Beschaeftigtendaten
### Stammdaten
- Name, Adresse, Geburtsdatum
- Steuer-ID, Sozialversicherungsnummer
- Bankverbindung (fuer Gehaltsauszahlung)
### Vertragsdaten
- Arbeitsvertrag, Stellenbeschreibung
- Gehalt, Zulagen, Bonusvereinbarungen
- Arbeitszeit, Urlaubsanspruch
### Verwaltungsdaten
- Krankenkassenname, Beitragssatz
- Steuerklasse, Kinderfreibetraege
- Kirchensteuermerkmal
### Leistungsdaten
- Beurteilungen, Zielvereinbarungen
- Fortbildungsnachweise, Zertifikate
- Abmahnungen, Zwischenzeugnisse
## Abgrenzung zu Art.-9-Daten
Das **Kirchensteuermerkmal** verraet die Religionszugehoerigkeit und ist damit ein Art.-9-Datum. Die Steuerklasse hingegen ist ein normales Verwaltungsdatum.
## Praxis-Tipp
Erfassen Sie im VVT die Beschaeftigtendaten moeglichst nach Kategorien getrennt (Stammdaten, Vertragsdaten etc.). Das erleichtert spaeter die Zuordnung von Loeschfristen und Zugriffsrechten.',
ARRAY['§ 26 BDSG', 'Art. 6 Abs. 1b DSGVO', 'Art. 88 DSGVO'],
ARRAY['beschaeftigte', 'personal', 'stammdaten', 'lohnabrechnung'],
'important',
ARRAY[])
ON CONFLICT (id) DO NOTHING;
-- 3. Arbeitszeiterfassung — Pflicht
INSERT INTO compliance_wiki_articles (id, category_id, title, summary, content, legal_refs, tags, relevance, source_urls) VALUES
('arbeitszeiterfassung-pflicht', 'arbeitsrecht',
'Arbeitszeiterfassung — Wer muss was erfassen?',
'Seit dem BAG-Beschluss 2022 besteht in Deutschland eine Pflicht zur systematischen Arbeitszeiterfassung. Das betrifft fast alle Unternehmen.',
'## Ueberblick
Das Bundesarbeitsgericht hat im September 2022 entschieden, dass Arbeitgeber die Arbeitszeiten ihrer Mitarbeiter systematisch erfassen muessen. Diese Pflicht ergibt sich aus dem Arbeitsschutzgesetz.
## Was muss erfasst werden?
- **Beginn** und **Ende** der taeglichen Arbeitszeit
- **Dauer** der Arbeitszeit
- **Ueberstunden** und Mehrarbeit
- Einhaltung der **Ruhezeiten** (mind. 11 Stunden)
- Einhaltung der **Pausenregelungen**
## Wer ist betroffen?
Grundsaetzlich alle Arbeitgeber — unabhaengig von der Unternehmensgroesse. Ausnahmen gibt es nur in sehr engen Grenzen (z.B. leitende Angestellte nach § 18 ArbZG).
## Datenschutz-Aspekte
Die Arbeitszeitdaten sind **personenbezogene Daten**. Die Rechtsgrundlage fuer die Erfassung ist die **rechtliche Verpflichtung** (Art. 6 Abs. 1c DSGVO i.V.m. § 3 ArbZG).
Wichtig: Die Daten duerfen **nicht** fuer andere Zwecke verwendet werden (z.B. Leistungskontrolle), es sei denn, es gibt dafuer eine eigene Rechtsgrundlage.
## Aufbewahrungsfrist
Arbeitszeitaufzeichnungen muessen mindestens **2 Jahre** aufbewahrt werden (§ 16 Abs. 2 ArbZG).
## Praxis-Tipp
Setzen Sie im VVT eine eigene Verarbeitungstaetigkeit "Arbeitszeiterfassung" auf und ordnen Sie die passende Rechtsgrundlage (Art. 6 Abs. 1c) zu.',
ARRAY['§ 3 ArbZG', '§ 16 Abs. 2 ArbZG', 'Art. 6 Abs. 1c DSGVO', 'BAG 1 ABR 22/21'],
ARRAY['arbeitszeit', 'zeiterfassung', 'bag', 'pflicht'],
'critical',
ARRAY['BAG 1 ABR 22/21 (13.09.2022)', 'EuGH C-55/18 (CCOO)'])
ON CONFLICT (id) DO NOTHING;
-- 4. HinSchG — Grundlagen
INSERT INTO compliance_wiki_articles (id, category_id, title, summary, content, legal_refs, tags, relevance, source_urls) VALUES
('hinschg-grundlagen', 'hinschg',
'Hinweisgeberschutzgesetz — Ab wann gilt was?',
'Seit Dezember 2023 muessen alle Unternehmen ab 50 Mitarbeitern eine interne Meldestelle einrichten. Das hat auch datenschutzrechtliche Auswirkungen.',
'## Ueberblick
Das Hinweisgeberschutzgesetz (HinSchG) schuetzt Personen, die auf Missstaende in Unternehmen hinweisen ("Whistleblower"). Seit dem 17. Dezember 2023 gilt die volle Pflicht fuer Unternehmen ab 50 Beschaeftigten.
## Kernpflichten
### Interne Meldestelle einrichten
- Kann eine **interne Person** oder ein **externer Dienstleister** sein
- Meldungen muessen **muendlich, schriftlich und persoenlich** moeglich sein
- Eingangsbestaetigung innerhalb von **7 Tagen**
- Rueckmeldung an den Hinweisgeber innerhalb von **3 Monaten**
### Vertraulichkeitsgebot (§ 8 HinSchG)
- Die **Identitaet des Hinweisgebers** darf nur den zustaendigen Personen bekannt sein
- Verstoss ist bussgeld­bewehrt (bis 50.000 EUR)
## Welche Daten fallen an?
- Identitaet des Hinweisgebers (besonders schuetzenswert!)
- Beschuldigte Personen
- Zeugen und weitere Beteiligte
- Inhalt der Meldung (kann sensible Daten enthalten)
- Kommunikationsverlauf
## Datenschutz-Anforderungen
- **Eigene Verarbeitungstaetigkeit** im VVT anlegen
- Rechtsgrundlage: Art. 6 Abs. 1c DSGVO (rechtliche Verpflichtung)
- **Zugriffsbeschraenkung:** Nur die benannte Meldestelle darf auf die Daten zugreifen
- **Loeschfrist:** 3 Jahre nach Abschluss des Verfahrens (§ 11 Abs. 5 HinSchG)
- Bei Art.-9-Daten in Meldungen: besondere Schutzmassnahmen erforderlich
## Praxis-Tipp
Pruefen Sie bei externen Meldestellen-Anbietern, ob ein **AVV** erforderlich ist. In den meisten Faellen ja — der Anbieter verarbeitet personenbezogene Daten in Ihrem Auftrag.',
ARRAY['§ 8 HinSchG', '§ 11 Abs. 5 HinSchG', '§ 12 HinSchG', 'Art. 6 Abs. 1c DSGVO'],
ARRAY['hinweisgeberschutz', 'whistleblower', 'meldestelle', 'vertraulichkeit'],
'critical',
ARRAY[])
ON CONFLICT (id) DO NOTHING;
-- 5. AVV — Website-Betrieb
INSERT INTO compliance_wiki_articles (id, category_id, title, summary, content, legal_refs, tags, relevance, source_urls) VALUES
('avv-website-betrieb', 'avv-dienstleister',
'AVV beim Website-Betrieb — Wer braucht einen Vertrag?',
'Beim Betrieb einer Website sind fast immer externe Dienstleister beteiligt. Fuer die meisten davon brauchen Sie einen Auftragsverarbeitungsvertrag.',
'## Ueberblick
Sobald ein externer Dienstleister in Ihrem Auftrag personenbezogene Daten verarbeitet, brauchen Sie einen **Auftragsverarbeitungsvertrag (AVV)** nach Art. 28 DSGVO.
## Typische AVV-Pflichten beim Website-Betrieb
| Dienstleister | AVV noetig? | Grund |
|--------------|-------------|-------|
| Hosting-Anbieter | Ja | Zugriff auf Server-Logs mit IP-Adressen |
| Newsletter-Tool | Ja | Verarbeitet E-Mail-Adressen |
| Analytics (Matomo gehostet) | Ja | Verarbeitet Nutzungsdaten |
| Cookie-Consent-Tool | Kommt drauf an | Nur wenn Daten beim Anbieter liegen |
| CDN (Cloudflare etc.) | Ja | IP-Adressen werden verarbeitet |
| Externer IT-Support | Ja | Potentieller Zugriff auf alle Daten |
## Was muss im AVV stehen?
- **Gegenstand und Dauer** der Verarbeitung
- **Art und Zweck** der Verarbeitung
- **Art der personenbezogenen Daten** (IP-Adressen, E-Mails etc.)
- **Kategorien betroffener Personen** (Website-Besucher, Newsletter-Abonnenten)
- **Technisch-organisatorische Massnahmen (TOMs)** des Dienstleisters
- **Unterauftragsverarbeiter** — muessen genehmigt werden
## Cookies & Analytics
Auch wenn Sie einen AVV mit dem Analytics-Anbieter haben: Die **datenschutzrechtliche Verantwortung** bleibt bei Ihnen! Sie muessen sicherstellen, dass eine gueltige Einwilligung vorliegt (§ 25 TDDDG).
## Praxis-Tipp
Fuehren Sie eine **Liste aller Dienstleister** mit Website-Bezug und pruefen Sie fuer jeden, ob ein AVV vorliegt. Viele Anbieter bieten Standard-AVVs zum Download an.',
ARRAY['Art. 28 DSGVO', '§ 25 TDDDG', 'Art. 32 DSGVO'],
ARRAY['avv', 'website', 'hosting', 'analytics', 'dienstleister'],
'important',
ARRAY[])
ON CONFLICT (id) DO NOTHING;
-- 6. AVV — Lohnbuchhaltung
INSERT INTO compliance_wiki_articles (id, category_id, title, summary, content, legal_refs, tags, relevance, source_urls) VALUES
('avv-lohnbuchhaltung', 'avv-dienstleister',
'AVV bei externer Lohnbuchhaltung',
'Wer die Lohnabrechnung an einen externen Dienstleister auslagert, braucht zwingend einen Auftragsverarbeitungsvertrag — denn es werden sensible Beschaeftigtendaten uebermittelt.',
'## Ueberblick
Die externe Lohnbuchhaltung ist einer der haeufigsten Faelle von Auftragsverarbeitung. Der Dienstleister erhaelt umfangreiche personenbezogene Daten Ihrer Beschaeftigten.
## Welche Daten werden uebermittelt?
- Name, Adresse, Geburtsdatum
- Sozialversicherungsnummer
- Steuer-ID, Steuerklasse
- Krankenkasse, Beitragssaetze
- Gehalt, Zulagen, Praemien
- Arbeitszeiten, Fehlzeiten
- Ggf. Kirchensteuermerkmal (Art.-9-Datum!)
- Ggf. Pfaendungsdaten
## AVV-Pflicht
Ein AVV nach Art. 28 DSGVO ist **zwingend erforderlich**. Der Dienstleister handelt weisungsgebunden in Ihrem Auftrag.
## Besondere Schutzmassnahmen
Da potenziell Art.-9-Daten betroffen sind (Kirchensteuermerkmal → Religion), sollten folgende TOMs beim Dienstleister nachgewiesen werden:
- **Verschluesselung** der Datenuebertragung
- **Zugriffsbeschraenkung** auf die Lohndaten
- **Protokollierung** aller Zugriffe
- **Regelmaessige Audits** des Dienstleisters
## Praxis-Tipp
Pruefen Sie, ob der Lohnbuchhaltungs-Dienstleister seinerseits **Unterauftragsverarbeiter** einsetzt (z.B. Cloud-Hosting, DATEV). Diese muessen im AVV aufgefuehrt sein.',
ARRAY['Art. 28 DSGVO', '§ 26 BDSG', 'Art. 9 DSGVO', 'Art. 32 DSGVO'],
ARRAY['avv', 'lohnbuchhaltung', 'personal', 'beschaeftigte'],
'important',
ARRAY[])
ON CONFLICT (id) DO NOTHING;
-- 7. Religion bei Bewerbungen
INSERT INTO compliance_wiki_articles (id, category_id, title, summary, content, legal_refs, tags, relevance, source_urls) VALUES
('religion-bewerbung', 'art9-besondere',
'Religion im Bewerbungsverfahren — Was darf gefragt werden?',
'Die Religionszugehoerigkeit ist ein besonders geschuetztes Datum nach Art. 9 DSGVO. Im Bewerbungsverfahren gelten strenge Regeln.',
'## Ueberblick
Die Religionszugehoerigkeit faellt unter die **besonderen Kategorien** personenbezogener Daten (Art. 9 DSGVO). Im Bewerbungsverfahren darf grundsaetzlich **nicht** danach gefragt werden.
## Frageverbote
Das **Allgemeine Gleichbehandlungsgesetz (AGG)** verbietet die Benachteiligung wegen der Religion. Daraus folgt:
- **Keine Frage** nach der Religionszugehoerigkeit im Bewerbungsgespraech
- **Kein Feld** "Religion" im Bewerbungsformular
- **Keine Rueckschluesse** aus dem Lebenslauf ziehen (z.B. Mitgliedschaft in religioesen Organisationen)
## Ausnahmen
Eine Ausnahme gilt fuer **Tendenzbetriebe** (z.B. kirchliche Einrichtungen). Hier kann die Religionszugehoerigkeit eine wesentliche und gerechtfertigte berufliche Anforderung sein — allerdings mit Einschraenkungen nach der EuGH-Rechtsprechung.
## Wann Religion doch relevant wird
Spaetestens bei der **Lohnabrechnung** wird die Religionszugehoerigkeit relevant, weil das Kirchensteuermerkmal uebermittelt werden muss. Dies ist dann durch **§ 26 Abs. 3 BDSG** gedeckt.
## Praxis-Tipp
Gestalten Sie Bewerbungsformulare so, dass keine besonderen Kategorien abgefragt werden. Fuehren Sie im VVT die Verarbeitung "Bewerbermanagement" mit den richtigen Datenkategorien — und listen Sie Religion dort **nicht** auf.',
ARRAY['Art. 9 DSGVO', '§ 1 AGG', '§ 26 Abs. 3 BDSG', 'Art. 4 Nr. 13 DSGVO'],
ARRAY['religion', 'bewerbung', 'art9', 'agg', 'diskriminierung'],
'important',
ARRAY['EuGH C-414/16 (Egenberger)'])
ON CONFLICT (id) DO NOTHING;
-- 8. Kontaktdaten von Ansprechpartnern
INSERT INTO compliance_wiki_articles (id, category_id, title, summary, content, legal_refs, tags, relevance, source_urls) VALUES
('kontaktdaten-ansprechpartner', 'datenkategorien',
'Kontaktdaten von Kunden- und Lieferanten-Ansprechpartnern',
'Auch die Kontaktdaten von Ansprechpartnern bei Geschaeftspartnern sind personenbezogene Daten und muessen datenschutzkonform verarbeitet werden.',
'## Ueberblick
In jedem CRM-System, jeder SAP-Kontaktpflege und jedem E-Mail-Verteiler werden personenbezogene Daten von **Ansprechpartnern** bei Kunden und Lieferanten gespeichert. Diese Daten unterliegen der DSGVO.
## Typische Daten
- Name, Vorname, Titel
- Geschaeftliche E-Mail-Adresse
- Geschaeftliche Telefonnummer
- Position / Abteilung
- Ggf. Foto (z.B. in Kontaktdatenbanken)
## Rechtsgrundlage
Die uebliche Rechtsgrundlage ist das **berechtigte Interesse** (Art. 6 Abs. 1f DSGVO). Die Geschaeftsbeziehung macht es erforderlich, Ansprechpartner zu kennen und zu kontaktieren.
## Informationspflicht
Auch Ansprechpartner bei Geschaeftspartnern muessen ueber die Datenverarbeitung informiert werden (Art. 13/14 DSGVO). In der Praxis geschieht das oft ueber:
- Einen Datenschutzhinweis in der E-Mail-Signatur
- Einen Link zur Datenschutzerklaerung in der Auftragsbestaetigung
- Einen separaten Datenschutzhinweis bei Vertragsabschluss
## Praxis-Tipp
Fuehren Sie im VVT eine Verarbeitungstaetigkeit "Kunden-/Lieferantenmanagement" mit der Datenkategorie "Geschaeftliche Kontaktdaten" auf. Die Rechtsgrundlage ist in der Regel Art. 6 Abs. 1f DSGVO.',
ARRAY['Art. 6 Abs. 1f DSGVO', 'Art. 13 DSGVO', 'Art. 14 DSGVO'],
ARRAY['kontaktdaten', 'crm', 'kunden', 'lieferanten', 'b2b'],
'info',
ARRAY[])
ON CONFLICT (id) DO NOTHING;
-- 9. Gemeinsame Verantwortlichkeit
INSERT INTO compliance_wiki_articles (id, category_id, title, summary, content, legal_refs, tags, relevance, source_urls) VALUES
('gemeinsame-verantwortlichkeit', 'dsgvo-grundlagen',
'Gemeinsame Verantwortlichkeit vs. Auftragsverarbeitung',
'Die Abgrenzung zwischen Art. 26 (gemeinsame Verantwortlichkeit) und Art. 28 (Auftragsverarbeitung) ist in der Praxis oft schwierig, aber entscheidend fuer die richtige vertragliche Gestaltung.',
'## Ueberblick
Wenn zwei oder mehr Stellen gemeinsam ueber **Zwecke und Mittel** der Datenverarbeitung entscheiden, liegt eine **gemeinsame Verantwortlichkeit** nach Art. 26 DSGVO vor. Das ist etwas anderes als eine Auftragsverarbeitung (Art. 28), bei der ein Dienstleister weisungsgebunden handelt.
## Auftragsverarbeitung (Art. 28)
Der Auftragsverarbeiter:
- Handelt **weisungsgebunden**
- Entscheidet **nicht** ueber Zweck und Mittel
- Verarbeitet Daten **nur im Auftrag** des Verantwortlichen
**Beispiele:** Hosting-Anbieter, externe Lohnbuchhaltung, Cloud-Speicher
## Gemeinsame Verantwortlichkeit (Art. 26)
Beide Parteien:
- Entscheiden **gemeinsam** ueber Zwecke und/oder Mittel
- Haben **eigene Interessen** an der Verarbeitung
- Muessen eine **Vereinbarung** ueber ihre jeweiligen Pflichten treffen
**Beispiele:**
- Facebook-Fanpage (EuGH Wirtschaftsakademie)
- Gemeinsame Kundendatenbank zweier Unternehmen
- Konzernweites HR-System mit gemeinsamer Steuerung
## Wann wird aus AVV eine gemeinsame Verantwortlichkeit?
Sobald der "Auftragsverarbeiter" beginnt, Daten fuer **eigene Zwecke** zu nutzen (z.B. eigene Analysen, Produktverbesserung mit Kundendaten), verschiebt sich die Rolle Richtung gemeinsame Verantwortlichkeit.
## Praxis-Tipp
Pruefen Sie bei jedem Dienstleister: Hat er ein **eigenes Interesse** an den Daten? Nutzt er sie fuer **eigene Zwecke**? Wenn ja, brauchen Sie eine Art.-26-Vereinbarung statt eines AVV.',
ARRAY['Art. 26 DSGVO', 'Art. 28 DSGVO', 'Art. 4 Nr. 7 DSGVO'],
ARRAY['art26', 'art28', 'avv', 'verantwortlichkeit', 'joint-controller'],
'important',
ARRAY['EuGH C-210/16 (Wirtschaftsakademie)', 'EuGH C-40/17 (Fashion ID)'])
ON CONFLICT (id) DO NOTHING;
-- 10. Qualifikationsdaten
INSERT INTO compliance_wiki_articles (id, category_id, title, summary, content, legal_refs, tags, relevance, source_urls) VALUES
('qualifikationsdaten', 'datenkategorien',
'Qualifikationsdaten — Fortbildungen, Zertifikate, Schulungsnachweise',
'Qualifikationsdaten gehoeren zu den Beschaeftigtendaten und unterliegen besonderen Aufbewahrungsregeln.',
'## Ueberblick
Qualifikationsdaten dokumentieren die beruflichen Faehigkeiten und Weiterbildungen von Beschaeftigten. Sie sind personenbezogene Daten und gehoeren zu den Beschaeftigtendaten.
## Was sind Qualifikationsdaten?
- Abschlusszeugnisse und Studiennachweise
- Berufliche Zertifizierungen (z.B. ISO-Auditor, Datenschutzbeauftragter)
- Teilnahmenachweise fuer Fortbildungen
- Schulungsnachweise (z.B. Arbeitssicherheit, Datenschutz)
- Fuehrerscheine / Fahrerlaubnisse (bei Relevanz fuer den Job)
- Sprachkenntnisse, IT-Kenntnisse
## Rechtsgrundlage
- **Waehrend des Arbeitsverhaeltnisses:** § 26 BDSG (Durchfuehrung des Beschaeftigungsverhaeltnisses)
- **Bei Pflichtschulungen:** Art. 6 Abs. 1c DSGVO (z.B. Arbeitssicherheitsunterweisungen)
## Aufbewahrungsfristen
| Datum | Frist | Grund |
|-------|-------|-------|
| Unterweisungsnachweise (Arbeitssicherheit) | Dauer des Arbeitsverhaeltnisses | ArbSchG |
| Fortbildungsnachweise | 3 Jahre nach Ende des AV | Nachweis der Personalentwicklung |
| Pflichtschulungen (z.B. Datenschutz) | 3 Jahre nach Durchfuehrung | Nachweispflicht |
| Fuehrerscheinkopien | Regelmaessige Ueberpruefung | UVV |
## Praxis-Tipp
Fuehren Sie Qualifikationsdaten als eigene Datenkategorie im VVT. Achten Sie auf die **Zweckbindung**: Schulungsnachweise zum Datenschutz duerfen nicht fuer die Leistungsbewertung herangezogen werden.',
ARRAY['§ 26 BDSG', 'Art. 6 Abs. 1c DSGVO', 'Art. 17 DSGVO'],
ARRAY['qualifikation', 'fortbildung', 'schulung', 'zertifikate', 'personal'],
'info',
ARRAY[])
ON CONFLICT (id) DO NOTHING;