Gesundheits-Check: ein Befehl prueft alles, was schon mal still kaputt war

Der Nutzer fragte zu Recht, warum staendig neue Probleme auftauchen. Antwort:
Sie tauchen nicht auf, wir schauen erst jetzt hin — und es gab bisher keine
einzige automatische Pruefung. Das Warteliste-Formular war SECHS WOCHEN
kaputt, ohne dass etwas Alarm geschlagen haette.

tools/pruefung.js prueft in ~30 Sekunden zehn Bereiche, jeder davon war
schon einmal ein echter Fehler:

 1 abgeschnittene Zeilen '[...]'      (Werkzeug kuerzte auf 200 Zeichen)
 2 Warteliste-Formular vollstaendig   (war 186 statt 248 Zeichen -> tot)
 3 strukturierte Daten gueltig        (Google meldete 'Fehlerhafter Aufbau')
 4 verlinkte Dateien existieren       (manifest.json lieferte HTML)
 5 Preis-IDs Worker == Seiten         (Kauf scheitert sonst stumm)
 6 alle Sitemap-Adressen liefern 200
 7 Kauf erzeugt echte cs_live_-Seite + fremde Origins abgewiesen
 8 Webhook-Secret gesetzt             (sonst keine Wiederherstellungs-Mail)
 9 KI antwortet                       (Schluessel war zweimal ungueltig)
10 Turnstile eingebunden              (sonst Missbrauch auf eigene Kosten)

Gegengeprueft: Mit absichtlich eingebauten Fehlern (abgeschnittene Zeile,
gekuerzte Formular-Adresse, kaputtes JSON-LD) schlaegt sie bei allen dreien
an und liefert Rueckgabewert 1.

Aufruf: npm run pruefung

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
JUSNandClaude Opus 5 committed 2026-09-21 14:42:09 +02:00
1 parent 8e4ee58055
commit e5915f3cc2
2 files changed
+188 -1

No files matched your search

+2 -1
View File
@@ -43,7 +43,8 @@
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"test": "jest"
"test": "jest",
"pruefung": "node tools/pruefung.js"
},
"jest": {
"preset": "jest-expo"
+186
View File
@@ -0,0 +1,186 @@
/**
* Gesundheits-Check für BehördenKlar.
*
* node tools/pruefung.js
*
* Prüft in EINEM Durchlauf alles, was in der Vergangenheit schon einmal
* still kaputt war — ohne dass es jemand gemerkt hat:
*
* - Warteliste-Formular (war 6 Wochen kaputt: Adresse abgeschnitten)
* - abgeschnittene Zeilen "[...]" (ein Werkzeug kürzte auf 200 Zeichen)
* - ungültige strukturierte Daten (Google meldete "Fehlerhafter Aufbau")
* - tote Verweise auf Dateien, die es nicht gibt (manifest.json lieferte HTML)
* - Bezahlung, KI-Analyse und Webhook — die drei Dinge, an denen Geld hängt
*
* Rückgabewert 0 = alles in Ordnung, 1 = mindestens ein Problem.
* Nach jedem Deploy ausführen. Dauert etwa 30 Sekunden.
*/
const { execFileSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const WEB = path.join(__dirname, '..', 'webseite');
const BASIS = 'https://behoerdenklar.de';
const WORKER = 'https://behoerdenklar-proxy.behoerdenbrief.workers.dev';
const BREVO_LAENGE = 248; // vollständige Formular-Adresse
let fehler = 0;
let warnungen = 0;
function ok(text) { console.log(' \x1b[32m✓\x1b[0m ' + text); }
function schlecht(text) { console.log(' \x1b[31m✗ ' + text + '\x1b[0m'); fehler++; }
function warnung(text) { console.log(' \x1b[33m!\x1b[0m ' + text); warnungen++; }
function titel(text) { console.log('\n\x1b[1m' + text + '\x1b[0m'); }
/** Holt eine Adresse. curl statt fetch, damit es überall gleich läuft. */
function hole(url, optionen = []) {
try {
return execFileSync('curl', ['-s', '--max-time', '30', ...optionen, url],
{ encoding: 'utf8', maxBuffer: 20 * 1024 * 1024 });
} catch { return ''; }
}
function status(url) {
try {
return execFileSync('curl', ['-s', '-o', '/dev/null', '-w', '%{http_code}', '--max-time', '30', url],
{ encoding: 'utf8' }).trim();
} catch { return '000'; }
}
function htmlDateien() {
const raus = [];
(function lauf(ordner) {
for (const eintrag of fs.readdirSync(ordner, { withFileTypes: true })) {
const p = path.join(ordner, eintrag.name);
if (eintrag.isDirectory()) lauf(p);
else if (eintrag.name.endsWith('.html')) raus.push(p);
}
})(WEB);
return raus;
}
// ─────────────────────────────────────────────────────────────
titel('1. Abgeschnittene Texte');
{
const betroffen = htmlDateien().filter((p) => fs.readFileSync(p, 'utf8').includes('[...]'));
if (betroffen.length) betroffen.forEach((p) => schlecht('abgeschnitten: ' + path.relative(WEB, p)));
else ok('keine Datei enthält "[...]"');
}
titel('2. Warteliste-Formular');
{
let geprueft = 0;
for (const p of htmlDateien()) {
const s = fs.readFileSync(p, 'utf8');
const t = s.match(/action="(https:\/\/86550585[^"]*)"/);
if (!t) continue;
geprueft++;
if (t[1].length !== BREVO_LAENGE) {
schlecht(`${path.relative(WEB, p)}: Adresse ${t[1].length} statt ${BREVO_LAENGE} Zeichen — Anmeldungen gehen ins Leere!`);
}
}
if (geprueft && !fehler) ok(`${geprueft} Formulare, alle Adressen vollständig`);
if (!geprueft) warnung('kein Warteliste-Formular gefunden');
}
titel('3. Strukturierte Daten (Google)');
{
let anzahl = 0, kaputt = 0;
for (const p of htmlDateien()) {
const s = fs.readFileSync(p, 'utf8');
for (const m of s.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)) {
anzahl++;
try { JSON.parse(m[1]); }
catch (e) { kaputt++; schlecht(`${path.relative(WEB, p)}: ${e.message.slice(0, 60)}`); }
}
}
if (!kaputt) ok(`${anzahl} Blöcke, alle gültig`);
}
titel('4. Verwiesene Dateien vorhanden');
{
const fehlend = new Set();
for (const p of htmlDateien()) {
const s = fs.readFileSync(p, 'utf8');
for (const m of s.matchAll(/(?:href|src)="(\/[^"#?]*\.(?:png|jpg|svg|json|ico|webmanifest))"/g)) {
const ziel = path.join(WEB, m[1]);
if (!fs.existsSync(ziel)) fehlend.add(m[1]);
}
}
if (fehlend.size) [...fehlend].forEach((f) => schlecht(`verlinkt, existiert aber nicht: ${f}`));
else ok('alle verlinkten Dateien vorhanden');
}
titel('5. Preise stimmen zwischen Worker und Seiten überein');
{
const worker = fs.readFileSync(path.join(__dirname, '..', 'proxy', 'src', 'index.ts'), 'utf8');
const imWorker = new Set([...worker.matchAll(/'(price_[A-Za-z0-9]+)'/g)].map((m) => m[1]));
const aufSeiten = new Set();
for (const p of htmlDateien()) {
for (const m of fs.readFileSync(p, 'utf8').matchAll(/(price_[A-Za-z0-9]+)/g)) aufSeiten.add(m[1]);
}
const unbekannt = [...aufSeiten].filter((x) => !imWorker.has(x));
if (unbekannt.length) unbekannt.forEach((x) => schlecht(`Seite nutzt ${x}, Worker kennt ihn nicht — Kauf würde scheitern`));
else ok(`${aufSeiten.size} Preis-IDs, alle dem Worker bekannt`);
}
titel('6. Alle Seiten der Sitemap erreichbar');
{
const sm = fs.readFileSync(path.join(WEB, 'sitemap.xml'), 'utf8');
const urls = [...sm.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]);
const tot = urls.filter((u) => status(u) !== '200');
if (tot.length) tot.forEach((u) => schlecht('nicht erreichbar: ' + u));
else ok(`${urls.length} Adressen, alle liefern 200`);
}
titel('7. Bezahlung (echtes Geld)');
{
const worker = fs.readFileSync(path.join(__dirname, '..', 'proxy', 'src', 'index.ts'), 'utf8');
const preise = [...worker.matchAll(/'(price_[A-Za-z0-9]+)'/g)].map((m) => m[1]);
for (const preis of preise) {
const a = hole(`${WORKER}/kaufen`, ['-X', 'POST', '-H', 'origin: ' + BASIS,
'-H', 'content-type: application/json', '-d', JSON.stringify({ priceId: preis })]);
if (a.includes('cs_live_')) ok(`${preis.slice(0, 20)}… öffnet echte Bezahlseite`);
else if (a.includes('cs_test_')) warnung(`${preis.slice(0, 20)}… ist im TESTMODUS — es fließt kein echtes Geld`);
else schlecht(`${preis.slice(0, 20)}… KAUF GEHT NICHT: ${a.slice(0, 90)}`);
}
// Fremde Seiten dürfen nicht kaufen können
const fremd = hole(`${WORKER}/kaufen`, ['-X', 'POST', '-H', 'origin: https://fremde-seite.de',
'-H', 'content-type: application/json', '-d', JSON.stringify({ priceId: preise[0] })]);
if (fremd.includes('"origin"')) ok('fremde Webseiten werden abgewiesen');
else schlecht('SICHERHEIT: fremde Webseiten können Bezahlseiten erzeugen');
}
titel('8. Webhook (Wiederherstellungs-Mail)');
{
const a = hole(`${WORKER}/stripe-webhook`, ['-X', 'POST',
'-H', 'stripe-signature: t=1,v1=deadbeef', '-H', 'content-type: application/json', '-d', '{}']);
if (a.includes('Signatur ungültig')) ok('Secret gesetzt, Signaturprüfung greift');
else if (a.includes('nicht konfiguriert')) schlecht('STRIPE_WEBHOOK_SECRET fehlt — keine Wiederherstellungs-Mail!');
else if (a.includes('Signatur fehlt')) ok('Signaturprüfung greift');
else schlecht('Webhook antwortet unerwartet: ' + a.slice(0, 70));
}
titel('9. KI-Analyse');
{
const a = hole(`${WORKER}/`, ['-X', 'POST', '-H', 'content-type: application/json',
'-H', 'x-geraete-id: g_pruefunglauf0000000000ab', '-d',
JSON.stringify({ model: 'claude-haiku-4-5', max_tokens: 8, messages: [{ role: 'user', content: 'Sag nur: OK' }] })]);
if (a.includes('"text"')) ok('KI antwortet');
else if (a.includes('authentication_error')) schlecht('ANTHROPIC-SCHLÜSSEL UNGÜLTIG — keine Analyse möglich!');
else if (a.includes('credit balance')) schlecht('ANTHROPIC-GUTHABEN LEER — bitte aufladen');
else schlecht('KI antwortet nicht: ' + a.slice(0, 90));
}
titel('10. Bot-Schutz auf der Demo-Seite');
{
const s = hole(BASIS + '/demo');
if (s.includes('cf-turnstile') && s.includes('data-sitekey="0x')) ok('Turnstile eingebunden');
else schlecht('Turnstile fehlt — Missbrauch auf deine Kosten möglich');
}
// ─────────────────────────────────────────────────────────────
console.log('\n' + '─'.repeat(56));
if (fehler === 0 && warnungen === 0) console.log('\x1b[32m\x1b[1mAlles in Ordnung.\x1b[0m');
else if (fehler === 0) console.log(`\x1b[33m${warnungen} Hinweis(e), keine Fehler.\x1b[0m`);
else console.log(`\x1b[31m\x1b[1m${fehler} Problem(e)\x1b[0m` + (warnungen ? `, ${warnungen} Hinweis(e)` : ''));
console.log('─'.repeat(56));
process.exit(fehler ? 1 : 0);