Files
Webklar-Kundenbereich/scripts/datenschutz-hochladen.mjs
T
Claude 0fbeadd0c1 Rechtliches: Skript zum Hochladen der Datenschutzerklärung als neue Version
Der Ablauf des Vorbilds (hochladen) ist jetzt eine exportierte, parametrisierte Funktion; datenschutz-hochladen.mjs ruft sie mit Karte datenschutz, PDF und HTML auf und bricht ab, wenn der Stand in der HTML nicht der Freigabe entspricht.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit 06676c459d0ac4f534aed7d616e2c0d40ac97b97)
2026-10-22 13:03:30 +00:00

104 lines
5.5 KiB
JavaScript

#!/usr/bin/env node
// Lädt die Datenschutzerklärung (PDF und HTML) nach der Freigabe als neue Version in WOMS „Rechtliches“ hoch (Karte datenschutz, Bucket
// legal-files, Collection legalVersions): neue Fassung "current", die bisherige gültige wird "superseded" mit validTo. Nur mit
// --freigabe=JJJJ-MM-TT; dieser Tag muss der „Stand TT.MM.JJJJ“ in public/datenschutz.html entsprechen. Schon hochgeladene Stände (gleiche source,
// also gleicher Commit) werden übersprungen. Den Ablauf teilt sich das Skript mit rechtliches-hochladen.mjs (Funktion hochladen).
//
// node scripts/datenschutz-hochladen.mjs --probe nur lesen: Schlüssel, Bucket, Karte datenschutz und ihre Versionen
// node scripts/datenschutz-hochladen.mjs --freigabe=JJJJ-MM-TT lädt die Datenschutzerklärung hoch (validFrom = Freigabedatum); ohne Karte,
// ohne beide Dateien, mit nicht committetem Stand oder mit abweichendem
// „Stand“ Abbruch vor dem Schreiben
//
// Schlüssel: kurzlebiger Appwrite-Schlüssel mit files.read/files.write/documents.read/documents.write in /root/.appwrite-rechtliches.key (chmod 600).
// Der Import dieser Datei liest weder den Schlüssel noch nutzt er das Netz; beides geschieht nur in main (deps.schluessel, deps.fetch).
// Dieses Skript wird nur geschrieben und mit Nachbau-deps getestet, nie von einem Agenten ausgeführt (auch nicht mit --probe).
import { readFileSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { BUCKET, KEY_DATEI, alleDokumente, client, ddmmjjjj, hochladen, istKalendertag, versionen } from './rechtliches-hochladen.mjs'
const DOKUMENT_KEY = 'datenschutz'
/** [PDF, HTML]: in dieser Reihenfolge hochgeladen. */
const DATEIEN = Object.freeze(['docs/WEBklar-Datenschutzerklaerung.pdf', 'public/datenschutz.html'])
const QUELLE = 'public/datenschutz.html'
/** Für Kunden sichtbar (WOMS „Rechtliches“): darum weder „Workers“ noch „Hustler“. */
const AENDERUNG = 'Abschnitt 11 Interessenten (Ansprache per E-Mail)'
const USAGE = 'Aufruf: node scripts/datenschutz-hochladen.mjs --probe | --freigabe=JJJJ-MM-TT'
/**
* Kommandozeile prüfen: --probe allein oder genau eine --freigabe=JJJJ-MM-TT (ein echter Kalendertag).
* @returns {{ modus: 'probe'|'hochladen', freigabe: string|null }}
*/
export function parseArgs(argv) {
const fremde = argv.filter((a) => a !== '--probe' && !a.startsWith('--freigabe='))
if (fremde.length > 0) throw new Error(`${USAGE}\nUnbekanntes Argument: ${fremde[0].slice(0, 40)}`)
const probe = argv.filter((a) => a === '--probe')
const freigaben = argv.filter((a) => a.startsWith('--freigabe='))
if (probe.length > 0) {
if (probe.length > 1 || freigaben.length > 0) throw new Error(`${USAGE}\n--probe steht allein.`)
return { modus: 'probe', freigabe: null }
}
if (freigaben.length !== 1) throw new Error(`${USAGE}\nFreigabe fehlt: genau eine --freigabe=JJJJ-MM-TT ist beim Hochladen Pflicht.`)
const freigabe = freigaben[0].slice('--freigabe='.length)
if (!istKalendertag(freigabe)) throw new Error(`Freigabe ungültig (erwartet JJJJ-MM-TT, ein echter Kalendertag): ${freigabe.slice(0, 20)}`)
return { modus: 'hochladen', freigabe }
}
/** Der Tag hinter „Stand“ in der Kopfzeile der HTML (TT.MM.JJJJ), sonst null. */
const standIn = (html) => /<p class="meta">\s*Stand (\d{2}\.\d{2}\.\d{4})\s*<\/p>/.exec(html)?.[1] ?? null
/** Einstieg: deps = { fetch, schluessel, log, git, dateien } wie bei rechtliches-hochladen.mjs. @returns {Promise<number>} Exit-Code (0 oder 1) */
export async function main(argv, deps = {}) {
const log = deps.log ?? console.log
let args
try {
args = parseArgs(argv)
} catch (err) {
log(err.message)
return 1
}
try {
const fetchFn = deps.fetch ?? globalThis.fetch
const api = client(fetchFn, (deps.schluessel ?? (() => readFileSync(KEY_DATEI, 'utf8').trim()))())
if (args.modus === 'probe') return await probe(api, log)
const { freigabe } = args
return await hochladen(api, log, {
documentKey: DOKUMENT_KEY,
dateien: DATEIEN,
quelle: QUELLE,
changeNote: `Fassung vom ${ddmmjjjj(freigabe)}: ${AENDERUNG}`,
freigabe,
kartenHinweis: '(die Karte gehört zu WOMS „Rechtliches“; dieses Skript legt keine Karten an)',
pruefeInhalt: (lies) => {
const stand = standIn(lies(QUELLE).toString('utf8'))
if (!stand) return `Abbruch: „Stand TT.MM.JJJJ“ fehlt in ${QUELLE}`
if (stand !== ddmmjjjj(freigabe)) return `Abbruch: Stand ${stand} in ${QUELLE} passt nicht zur Freigabe ${ddmmjjjj(freigabe)}`
return null
},
}, deps)
} catch (err) {
log(`Fehler: ${err.message}`)
return 1
}
}
/** --probe: nur lesen. Eine fehlende Karte ist ein Stopp. */
async function probe(api, log) {
await api('GET', `/storage/buckets/${BUCKET}/files`)
const karten = await alleDokumente(api, 'legalDocuments')
log(`${DOKUMENT_KEY}: ${(await versionen(api, DOKUMENT_KEY)).map((v) => `${v.version}:${v.status}`).join(' ') || '—'}`)
if (!karten.some((k) => k.key === DOKUMENT_KEY)) {
log(`STOPP: Karte fehlt: ${DOKUMENT_KEY}`)
return 1
}
log(`Schlüssel ok (files.read + documents.read), Karte ${DOKUMENT_KEY} vorhanden.`)
return 0
}
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
main(process.argv.slice(2)).then((code) => {
process.exitCode = code
})
}