All checks were successful
Container-Image bauen und veröffentlichen / build-and-push (push) Successful in 1m54s
Führende Quelle ist jetzt der Excel-Export des Amtes statt der einzelnen Merkblatt-PDFs: 120 Zeilen, davon 4 bisher nicht erfasste Sozialamts- Leistungen. Zuordnung über den Bezeichner, nicht den Titel – die aus PDF gelesenen Titel unterscheiden sich in neun Fällen nur in Leerzeichen um Schrägstriche und hätten Dubletten erzeugt. Zwei Angaben stehen nicht in der Liste und werden ergänzt: der in allen Merkblättern enthaltene Empfängersatz zur Finanzbuchhaltung (ohne ihn hätten 27 Erklärungen gar keine Empfängerangabe) und die Bezeichnung der Verarbeitungstätigkeit aus den Organisationsspalten. Die zehn Bausteine folgen durchgehend den zentralen Standardtexten; gespeichert wird nur die eine echte Abweichung: die gemeinsame Verantwortlichkeit mit dem ZIT SH beim Online-Wohngeldantrag. Alle 121 Erklärungen bleiben unveröffentlicht – drei Standardtexte (Widerspruchsrecht, Drittlandübermittlung, automatisierte Entscheidungen) sind noch nicht gepflegt und ohne sie ist keine vollständig. Die Quellliste selbst bleibt unversioniert: Sie führt Namen von Beschäftigten, die nicht in die Erklärungen übernommen werden. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
107 lines
4.0 KiB
JavaScript
107 lines
4.0 KiB
JavaScript
/**
|
||
* Berichtet, welche Erklärungen die Pflichtangaben aus Art. 13/14 DSGVO noch
|
||
* nicht erfüllen und deshalb nicht veröffentlicht werden können.
|
||
*
|
||
* node scripts/check-completeness.js Zusammenfassung
|
||
* node scripts/check-completeness.js --list jede Erklärung einzeln
|
||
*
|
||
* Spiegelt die Regeln aus lib/policy-completeness.ts.
|
||
*/
|
||
const { PrismaClient } = require("@prisma/client")
|
||
const { readDefaults } = require("./lib/policy-defaults")
|
||
|
||
const prisma = new PrismaClient()
|
||
|
||
/** Angaben, die die Erklärung selbst mitbringen muss. */
|
||
const EIGENE = [
|
||
["title", "Titel", ""],
|
||
["processingDescription", "Bezeichnung der Verarbeitungstätigkeit", ""],
|
||
["processingPurposes", "Zweck der Datenerhebung", "Art. 13 Abs. 1 lit. c"],
|
||
["legalBasis", "Rechtsgrundlage", "Art. 13 Abs. 1 lit. c"],
|
||
["recipients", "Geplante Empfänger", "Art. 13 Abs. 1 lit. e"],
|
||
["storageDuration", "Speicherdauer", "Art. 13 Abs. 2 lit. a"],
|
||
]
|
||
|
||
/** Bausteine, die aus der Erklärung oder dem Standardtext kommen müssen. */
|
||
const BAUSTEINE = [
|
||
["responsible", "Verantwortlicher", "Art. 13 Abs. 1 lit. a"],
|
||
["contactDPO", "Datenschutzbeauftragte/r", "Art. 13 Abs. 1 lit. b"],
|
||
["dataSubjectsRights", "Betroffenen-Rechte", "Art. 13 Abs. 2 lit. b"],
|
||
["complaintRight", "Beschwerderecht", "Art. 13 Abs. 2 lit. d"],
|
||
["withdrawalRight", "Widerrufsrecht", "Art. 13 Abs. 2 lit. c"],
|
||
["objectionRight", "Widerspruchsrecht", "Art. 21 Abs. 4"],
|
||
["thirdCountryTransfer", "Übermittlung in Drittländer", "Art. 13 Abs. 1 lit. f"],
|
||
["automatedDecision", "Automatisierte Entscheidungsfindung", "Art. 13 Abs. 2 lit. f"],
|
||
]
|
||
|
||
const hat = (v) => typeof v === "string" && v.trim().length > 0
|
||
|
||
async function main() {
|
||
const list = process.argv.includes("--list")
|
||
const defaults = await readDefaults(prisma)
|
||
const policies = await prisma.privacyPolicy.findMany({ orderBy: { title: "asc" } })
|
||
|
||
const zaehler = new Map()
|
||
const unvollstaendig = []
|
||
|
||
for (const policy of policies) {
|
||
const fehlt = []
|
||
|
||
for (const [field, label, artikel] of EIGENE) {
|
||
if (!hat(policy[field])) fehlt.push({ label, artikel, quelle: "Erklärung" })
|
||
}
|
||
for (const [field, label, artikel] of BAUSTEINE) {
|
||
if (!hat(policy[field]) && !hat(defaults[field])) {
|
||
fehlt.push({ label, artikel, quelle: "Standardtext" })
|
||
}
|
||
}
|
||
// Art. 14 greift nur bei Erhebung aus anderer Quelle
|
||
if (hat(policy.dataSource) && !hat(policy.dataCategories)) {
|
||
fehlt.push({
|
||
label: "Kategorien der verarbeiteten Daten",
|
||
artikel: "Art. 14 Abs. 1 lit. d",
|
||
quelle: "Erklärung",
|
||
})
|
||
}
|
||
|
||
fehlt.forEach((f) => {
|
||
const key = `${f.label}|${f.artikel}|${f.quelle}`
|
||
zaehler.set(key, (zaehler.get(key) || 0) + 1)
|
||
})
|
||
|
||
if (fehlt.length) unvollstaendig.push({ policy, fehlt })
|
||
}
|
||
|
||
console.log(`Erklärungen gesamt: ${policies.length}`)
|
||
console.log(`Veröffentlichungsreif: ${policies.length - unvollstaendig.length}`)
|
||
console.log(`Unvollständig: ${unvollstaendig.length}`)
|
||
console.log(`Derzeit veröffentlicht: ${policies.filter((p) => p.isPublic).length}`)
|
||
console.log("")
|
||
|
||
console.log("=== Fehlende Angaben nach Häufigkeit ===")
|
||
const sortiert = [...zaehler.entries()].sort((a, b) => b[1] - a[1])
|
||
for (const [key, anzahl] of sortiert) {
|
||
const [label, artikel, quelle] = key.split("|")
|
||
const ort = quelle === "Standardtext" ? " [zentraler Standardtext]" : ""
|
||
console.log(` ${String(anzahl).padStart(4)}× ${label}${artikel ? ` (${artikel})` : ""}${ort}`)
|
||
}
|
||
|
||
if (list) {
|
||
console.log("")
|
||
console.log("=== Erklärungen mit fehlenden Angaben ===")
|
||
for (const { policy, fehlt } of unvollstaendig) {
|
||
const eigene = fehlt.filter((f) => f.quelle === "Erklärung")
|
||
if (!eigene.length) continue
|
||
console.log(` ${policy.title}`)
|
||
eigene.forEach((f) => console.log(` fehlt: ${f.label}${f.artikel ? ` (${f.artikel})` : ""}`))
|
||
}
|
||
}
|
||
}
|
||
|
||
main()
|
||
.catch((e) => {
|
||
console.error(e)
|
||
process.exitCode = 1
|
||
})
|
||
.finally(() => prisma.$disconnect())
|