From d04cd896d8a89db27e65c6a23739fca002a69d15 Mon Sep 17 00:00:00 2001 From: Lars Weiser Date: Tue, 28 Apr 2026 16:06:53 +0200 Subject: [PATCH 1/4] docs(baseline): Datenschutz raw-event clause amendment (G B.3.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lands the legal-text amendment required before the Phase B.3 Datenschutz pages ship. No code, no config — text-only edits to BASELINE_COPY.md §9.4 (DE) and §10.4 (EN). Origin: pre-Phase-B.3 hand-verification of Cloudflare facts (run 2026-04-28) found that Cloudflare does not publish a numerical hour-level retention figure for Web Analytics raw events. The Web Analytics overview only commits to edge pre-processing; the "4 hours" figure circulating in adjacent Cloudflare writing is for edge access logs, a different product surface. Filling {{ cwa_retention_raw_hours }} with any specific Y therefore makes a commitment Cloudflare has not made publicly. Path α (decided 2026-04-28): drop the Y-hour claim, preserve the disclosure that raw events are pre-processed at the Cloudflare edge, plus a negative commitment that no persistent storage of raw events takes place under our control. Per project rule §11.4 the amendment text was drafted by an isolated reviewer cycle (sub-agent, 2026-04-28); the orchestrator session committed verbatim. §9.4 / §10.4 — replaced one semicolon-bridged retention sentence with two period-separated sentences. Aggregated-data retention clause unchanged (still placeholder-driven, value 6 to land via data file). Verified-date clause unchanged. Surrounding paragraph (beacon host, cookie-free, fingerprint-free, aggregate-metrics list, identification statement, legal basis) unchanged. The {{ cwa_retention_raw_hours }} placeholder is no longer referenced anywhere in BASELINE_COPY and will not appear in the src/data/cloudflare-facts.json data file shipped with B.3. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/BASELINE_COPY.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/BASELINE_COPY.md b/docs/BASELINE_COPY.md index b9235a5..d0e25c5 100644 --- a/docs/BASELINE_COPY.md +++ b/docs/BASELINE_COPY.md @@ -269,7 +269,7 @@ Weitere Informationen zum Datenschutz bei Cloudflare: https://www.cloudflare.com 4. Cloudflare-Dienste: Web Analytics und Turnstile -Zur Reichweitenmessung wird Cloudflare Web Analytics eingesetzt. Hierzu wird beim Aufruf der Website ein Skript (Beacon) vom Cloudflare-Host static.cloudflareinsights.com nachgeladen. Dieser Dienst arbeitet cookiefrei und ohne Fingerprinting. Es werden ausschließlich aggregierte Nutzungsstatistiken erhoben (Seitenaufrufe, Referrer, ungefähre geografische Herkunft auf Länderebene). Eine Identifizierung einzelner Besucher ist weder vorgesehen noch möglich. Aggregierte Auswertungsdaten werden für {{ cwa_retention_aggregates_months }} Monate gespeichert; einzelne Rohereignisse werden innerhalb von {{ cwa_retention_raw_hours }} Stunden aggregiert und anschließend gelöscht. Diese Angaben werden regelmäßig anhand der öffentlichen Dokumentation von Cloudflare geprüft (zuletzt geprüft am {{ cloudflare_facts_verified_date }}). +Zur Reichweitenmessung wird Cloudflare Web Analytics eingesetzt. Hierzu wird beim Aufruf der Website ein Skript (Beacon) vom Cloudflare-Host static.cloudflareinsights.com nachgeladen. Dieser Dienst arbeitet cookiefrei und ohne Fingerprinting. Es werden ausschließlich aggregierte Nutzungsstatistiken erhoben (Seitenaufrufe, Referrer, ungefähre geografische Herkunft auf Länderebene). Eine Identifizierung einzelner Besucher ist weder vorgesehen noch möglich. Aggregierte Auswertungsdaten werden für {{ cwa_retention_aggregates_months }} Monate gespeichert. Einzelne Rohereignisse werden bereits an der Cloudflare-Edge vorverarbeitet; eine dauerhafte Speicherung roher Einzelereignisse durch uns findet nicht statt. Diese Angaben werden regelmäßig anhand der öffentlichen Dokumentation von Cloudflare geprüft (zuletzt geprüft am {{ cloudflare_facts_verified_date }}). Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse an einer datenschutzfreundlichen Reichweitenmessung). @@ -376,7 +376,7 @@ Cloudflare's privacy policy: https://www.cloudflare.com/privacypolicy/ 4. Cloudflare Services: Web Analytics and Turnstile -Cloudflare Web Analytics is used to measure site usage. To do this, a script (beacon) is loaded from the Cloudflare host static.cloudflareinsights.com when a page is opened. This service operates without cookies and without fingerprinting. Only aggregate usage statistics are collected (page views, referrer, approximate country-level geolocation). Identification of individual visitors is neither intended nor possible. Aggregated analytics data is retained for {{ cwa_retention_aggregates_months }} months; individual raw events are aggregated and then deleted within {{ cwa_retention_raw_hours }} hours. These figures are verified periodically against Cloudflare's public documentation (last verified on {{ cloudflare_facts_verified_date }}). +Cloudflare Web Analytics is used to measure site usage. To do this, a script (beacon) is loaded from the Cloudflare host static.cloudflareinsights.com when a page is opened. This service operates without cookies and without fingerprinting. Only aggregate usage statistics are collected (page views, referrer, approximate country-level geolocation). Identification of individual visitors is neither intended nor possible. Aggregated analytics data is retained for {{ cwa_retention_aggregates_months }} months. Individual raw events are pre-processed at Cloudflare's edge; no persistent storage of raw individual events takes place under our control. These figures are verified periodically against Cloudflare's public documentation (last verified on {{ cloudflare_facts_verified_date }}). Legal basis: Art. 6 (1) (f) GDPR (legitimate interest in privacy-friendly analytics). From 66dc0140f2a3ce830f9ffd95ac1c825805e4e438 Mon Sep 17 00:00:00 2001 From: Lars Weiser Date: Tue, 28 Apr 2026 16:34:38 +0200 Subject: [PATCH 2/4] feat(b3): cloudflare-facts data file + 90-day build-guard (G B.3.2.a) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds src/data/cloudflare-facts.json with hand-verified initial values from the 2026-04-28 verification cycle (DPF Active per participant/5666; CWA aggregated retention 6 months per Web Analytics FAQ; raw-event retention placeholder removed by G B.3.1 amendment). scripts/check-cloudflare-facts-freshness.mjs runs ahead of astro build in the build script chain. Fails the build if verifiedDate is older than 90 days — manual-era 'no silent failures' mechanism per backlog item #8. Tighter than the post-verifier 120-day threshold because manual processes need stronger forcing functions than automated ones. Phase D Cloudflare-fact-verifier (backlog #8) will overwrite the JSON file via automated workflow; the consumer pattern lands here so D is purely 'automate the data source' work. Co-Authored-By: Claude Opus 4.7 (1M context) --- package.json | 2 +- scripts/check-cloudflare-facts-freshness.mjs | 62 ++++++++++++++++++++ src/data/cloudflare-facts.json | 13 ++++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100755 scripts/check-cloudflare-facts-freshness.mjs create mode 100644 src/data/cloudflare-facts.json diff --git a/package.json b/package.json index 3b7f3a1..2d74fc0 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "dev": "astro dev", "start": "astro dev", - "build": "node scripts/build-headers.mjs && astro build && node scripts/extract-tokens.mjs", + "build": "node scripts/check-cloudflare-facts-freshness.mjs && node scripts/build-headers.mjs && astro build && node scripts/extract-tokens.mjs", "preview": "astro preview", "astro": "astro", "check": "astro check" diff --git a/scripts/check-cloudflare-facts-freshness.mjs b/scripts/check-cloudflare-facts-freshness.mjs new file mode 100755 index 0000000..dc7121d --- /dev/null +++ b/scripts/check-cloudflare-facts-freshness.mjs @@ -0,0 +1,62 @@ +#!/usr/bin/env node +/** + * Build-time freshness guard for src/data/cloudflare-facts.json. + * + * Manual-era "no silent failures" mechanism per backlog item #8. + * Fails the build if `verifiedDate` is older than MAX_AGE_DAYS, forcing + * either a fresh hand-verification or the Phase D automated verifier + * before another deploy can ship. + * + * MAX_AGE_DAYS=90 matches the future verifier's verification cadence. + * Tighter than the post-verifier 120-day threshold (which factors in a + * grace window on top of the cadence) — manual processes need stronger + * forcing functions than automated ones. + * + * Exit codes: + * 0 — verifiedDate is at most MAX_AGE_DAYS old. + * 1 — verifiedDate is missing, malformed, or older than MAX_AGE_DAYS. + */ + +import { readFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; + +const MAX_AGE_DAYS = 90; +const DATA_PATH = resolve('src/data/cloudflare-facts.json'); + +function fail(message) { + process.stderr.write(`[cloudflare-facts] ${message}\n`); + process.exit(1); +} + +const raw = await readFile(DATA_PATH, 'utf8').catch((err) => + fail(`cannot read ${DATA_PATH}: ${err.message}`), +); + +let data; +try { + data = JSON.parse(raw); +} catch (err) { + fail(`invalid JSON in ${DATA_PATH}: ${err.message}`); +} + +const verifiedDate = data?.verifiedDate; +if (typeof verifiedDate !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(verifiedDate)) { + fail(`verifiedDate missing or not ISO YYYY-MM-DD: ${JSON.stringify(verifiedDate)}`); +} + +const verifiedMs = Date.parse(`${verifiedDate}T00:00:00Z`); +if (Number.isNaN(verifiedMs)) { + fail(`verifiedDate not a parseable date: ${verifiedDate}`); +} + +const ageDays = Math.floor((Date.now() - verifiedMs) / (24 * 60 * 60 * 1000)); +if (ageDays > MAX_AGE_DAYS) { + fail( + `verifiedDate ${verifiedDate} is ${ageDays} days old (max ${MAX_AGE_DAYS}). ` + + `Re-verify Cloudflare facts at ${data.dpf?.sourceUrl ?? ''} ` + + `and ${data.cwa?.sourceUrl ?? ''}, then update src/data/cloudflare-facts.json. ` + + `Or land Phase D Cloudflare-fact-verifier (backlog #8).`, + ); +} + +process.stdout.write(`[cloudflare-facts] verifiedDate ${verifiedDate} OK (${ageDays} days old)\n`); diff --git a/src/data/cloudflare-facts.json b/src/data/cloudflare-facts.json new file mode 100644 index 0000000..63dad94 --- /dev/null +++ b/src/data/cloudflare-facts.json @@ -0,0 +1,13 @@ +{ + "verifiedDate": "2026-04-28", + "dpf": { + "status": "active", + "sourceUrl": "https://www.dataprivacyframework.gov/participant/5666", + "originalCertificationDate": "2016-11-09", + "nextCertificationDue": "2026-09-23" + }, + "cwa": { + "retentionAggregatesMonths": 6, + "sourceUrl": "https://developers.cloudflare.com/web-analytics/faq/" + } +} From 108a5eabeb7a2e1bce72191391cf35543ed993a5 Mon Sep 17 00:00:00 2001 From: Lars Weiser Date: Tue, 28 Apr 2026 16:52:47 +0200 Subject: [PATCH 3/4] feat(privacy): Datenschutz + Privacy pages (G B.3.2.b) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DE /datenschutz (legally authoritative) + EN /en/privacy (courtesy translation). Copy held inline (per OQ-1) and verbatim from docs/BASELINE_COPY.md §9 / §10 — eleven numbered sections each, with a two-column TOC desktop / single-column mobile per design bundle (07-datenschutz.html). Stand / Last updated hardcoded as 'April 2026' per plan §4.B.3 / D22 (bumped only on substantive changes). Two build-time substitutions per page from src/data/cloudflare-facts.json via src/lib/cloudflare-facts.ts: the aggregated-data retention figure ({{ cwa_retention_aggregates_months }} → 6) and the verified-date ({{ cloudflare_facts_verified_date }} → '28. April 2026' / 'April 28, 2026' formatted via Intl.DateTimeFormat per page locale). SiteFooter gains the Datenschutz / Privacy entry between Impressum and GitHub. footer.privacy i18n keys, counterpart map (/datenschutz ↔ /en/privacy), and hreflang were already provisioned in earlier phases. Text-color verification (G0 item 2.3): bundle's datenschutz.css uses standard tokens (--color-text, --color-heading, --color-text-muted, --color-link). Volcanic Stone is AAA on every surface per brief §4.8. Implementation matches. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/SiteFooter.astro | 7 +- src/lib/cloudflare-facts.ts | 39 +++ src/pages/datenschutz.astro | 521 +++++++++++++++++++++++++++++++ src/pages/en/privacy.astro | 526 ++++++++++++++++++++++++++++++++ 4 files changed, 1090 insertions(+), 3 deletions(-) create mode 100644 src/lib/cloudflare-facts.ts create mode 100644 src/pages/datenschutz.astro create mode 100644 src/pages/en/privacy.astro diff --git a/src/components/SiteFooter.astro b/src/components/SiteFooter.astro index 6c41a2b..1496a7e 100644 --- a/src/components/SiteFooter.astro +++ b/src/components/SiteFooter.astro @@ -2,9 +2,8 @@ /** * Site footer — small brand mark, tiny nav, copyright. * - * Nav lists locale-aware About, Contact, Impressum, and an external - * GitHub link. Products and Datenschutz entries arrive with their - * respective Pass 2 phases. + * Nav lists locale-aware About, Contact, Impressum, Datenschutz, and an + * external GitHub link. Products entry arrives with Phase C. */ import BrandLogo from './BrandLogo.astro'; @@ -18,6 +17,7 @@ const t = getUiStrings(locale).footer; const aboutHref = locale === 'de' ? '/ueber' : '/en/about'; const contactHref = locale === 'de' ? '/kontakt' : '/en/contact'; const legalHref = locale === 'de' ? '/impressum' : '/en/legal'; +const privacyHref = locale === 'de' ? '/datenschutz' : '/en/privacy'; ---