Files
Webklar-Kundenbereich/scripts/umschalten-nachpruefung.mjs
T
ClaudeandClaude Opus 5.5 17317ada05 Kundenansicht neu P10: Korrekturen aus der Pruefung (Task 4)
Zeile /auftrag.html in neu und alt (Übersicht N40), Test mit der echten Weiche bleibt auch nach dem Festschreiben (Task 14) grün

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 12:07:57 +00:00

155 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Kundenansicht neu (P10 Umschalten): prueft per HTTP, ob die oeffentlichen Seiten, das Manifest und die Abo-Bedingungen
* so ausgeliefert werden, wie der Schalter KUNDENANSICHT_NEU es verlangt: "alt" (off und pilot) oder "neu" (on).
* Nur lesend: je Pruefung ein GET ohne Cookies und ohne Umleitungen, kein Appwrite, kein Schreiben, keine Namen
* oder Kundendaten in der Ausgabe (nur Adressen der Seiten und Gruende).
* Usage:
* node scripts/umschalten-nachpruefung.mjs https://project.webklar.com --erwartet=alt
* node scripts/umschalten-nachpruefung.mjs https://project.webklar.com --erwartet=neu
* Exit 0 = alle Zeilen gruen, 1 = mindestens eine Zeile rot, 2 = falscher Aufruf.
*/
import { pathToFileURL } from 'node:url'
const ZEITLIMIT_MS = 15000
const html = (pfad, enthaelt, enthaeltNicht = []) => Object.freeze({ pfad, typ: 'html', enthaelt, enthaeltNicht })
// Manifest: "schluessel=wert" prueft ein Feld, "shortcuts.name=X" einen Shortcut mit diesem Namen
const manifest = (enthaelt) => Object.freeze({ pfad: '/manifest.webmanifest', typ: 'json', enthaelt, enthaeltNicht: [] })
const pdf = Object.freeze({ pfad: '/WEBklar-Abo-Vertragsunterlagen.pdf', typ: 'pdf', enthaelt: ['application/pdf', '%PDF-'], enthaeltNicht: [] })
const NEU_FORMULAR = ['/neu/oeffentlich/oeffentlich.css', '/legal-form.js']
/** Marker je Erwartung; jede Zeile verlangt Status 200. */
export const PRUEFUNGEN = Object.freeze({
neu: Object.freeze([
html('/dashboard.html', ['<meta name="kn-seite" content="neu">'], ['kb-page']),
html('/login.html', ['content="#ffffff"', '/neu/tokens.css'], ['#9d3dfe', '/login.css']),
html('/kuendigen.html', NEU_FORMULAR, ['/legal-form.css']),
html('/widerrufen.html', NEU_FORMULAR, ['/legal-form.css']),
html('/auftrag.html', ['/neu/oeffentlich/oeffentlich.css'], ['/auftrag.css']), // Uebersicht N40
html('/offline.html', ['content="#ffffff"'], ['#9d3dfe', '<link']),
manifest([
'id=/dashboard.html',
'start_url=/dashboard.html',
'scope=/',
'background_color=#ffffff',
'theme_color=#ffffff',
'shortcuts.name=Chat',
'shortcuts.name=Rechnungen',
'shortcuts.name=Verträge',
]),
html('/abo-bedingungen.html', ['Version 6'], ['Version 5 · Stand', 'Entwurf – Prüfung durch Kanzlei offen']),
pdf,
]),
alt: Object.freeze([
html('/dashboard.html', ['class="portal-page dashboard-page kb-page"']),
html('/login.html', ['/login.css', 'content="#09090b"']),
html('/kuendigen.html', ['/legal-form.css']),
html('/widerrufen.html', ['/legal-form.css']),
html('/auftrag.html', ['/auftrag.css']),
html('/offline.html', ['#9d3dfe']),
manifest(['background_color=#000000']),
html('/abo-bedingungen.html', ['Version 5 · Stand']),
pdf,
]),
})
const AUFRUF = 'Aufruf: node scripts/umschalten-nachpruefung.mjs <http(s)://host[:port]> --erwartet=neu|alt'
/** [basis, '--erwartet=neu'|'--erwartet=alt'] -> { basis, erwartet }; sonst Error "Aufruf: ...". */
export function parseArgs(argv) {
const args = Array.isArray(argv) ? argv : []
const optionen = args.filter((a) => typeof a === 'string' && a.startsWith('--'))
const rest = args.filter((a) => !optionen.includes(a))
const erwartet = optionen.length === 1 ? /^--erwartet=(neu|alt)$/.exec(optionen[0])?.[1] : undefined
const basis = rest.length === 1 ? rest[0] : undefined
// nur Schema, Host und optional Port: kein Pfad, keine Zugangsdaten, keine Parameter
if (!erwartet || typeof basis !== 'string' || !/^https?:\/\/([A-Za-z0-9.-]+|\[[0-9A-Fa-f:]+\])(:\d{1,5})?$/.test(basis)) throw new Error(AUFRUF)
return { basis, erwartet }
}
/** Ein Feld des Manifests nach "schluessel=wert" / "shortcuts.name=X" pruefen. */
function manifestHat(daten, marker) {
const gleich = marker.indexOf('=')
const schluessel = marker.slice(0, gleich)
const wert = marker.slice(gleich + 1)
if (schluessel === 'shortcuts.name') return Array.isArray(daten?.shortcuts) && daten.shortcuts.some((s) => s?.name === wert)
return daten?.[schluessel] === wert
}
/** Eine Pruefung auswerten -> { pfad, ok, grund }. */
async function pruefe(basis, pruefung, fetchFn) {
const zeile = (ok, grund = '') => ({ pfad: pruefung.pfad, ok, grund })
let antwort
let text
try {
antwort = await fetchFn(basis + pruefung.pfad, {
redirect: 'manual',
headers: { 'cache-control': 'no-cache' },
signal: AbortSignal.timeout(ZEITLIMIT_MS),
})
text = await antwort.text()
} catch {
return zeile(false, 'Netzfehler')
}
if (antwort.status !== 200) return zeile(false, `Status ${antwort.status} statt 200`)
if (pruefung.typ === 'pdf') {
const art = antwort.headers.get('content-type') || ''
if (!art.includes('application/pdf')) return zeile(false, `Inhaltstyp ${art || 'fehlt'} statt application/pdf`)
return text.startsWith('%PDF-') ? zeile(true) : zeile(false, 'Beginn %PDF- fehlt')
}
if (pruefung.typ === 'json') {
let daten
try {
daten = JSON.parse(text)
} catch {
return zeile(false, 'kein JSON')
}
const fehlt = pruefung.enthaelt.find((m) => !manifestHat(daten, m))
return fehlt ? zeile(false, `fehlt: ${fehlt}`) : zeile(true)
}
const fehlt = pruefung.enthaelt.find((m) => !text.includes(m))
if (fehlt) return zeile(false, `fehlt: ${fehlt}`)
const verboten = pruefung.enthaeltNicht.find((m) => text.includes(m))
return verboten ? zeile(false, `verboten: ${verboten}`) : zeile(true)
}
/**
* Alle Pruefungen der Erwartung ("neu" | "alt") gegen basis ausfuehren; ohne: Adressen, die ausgelassen werden
* (z. B. die Fassung-6-Zeilen, solange sie nicht ausgeliefert werden). -> { ok, zeilen: [{ pfad, ok, grund }] }.
*/
export async function nachpruefen(basis, erwartet, { fetchFn = fetch, ohne = [] } = {}) {
const liste = PRUEFUNGEN[erwartet]
if (!liste) throw new Error(AUFRUF)
const zeilen = []
for (const pruefung of liste) {
if (ohne.includes(pruefung.pfad)) continue
zeilen.push(await pruefe(basis, pruefung, fetchFn))
}
return { ok: zeilen.every((z) => z.ok), zeilen }
}
const invokedDirectly = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href
if (invokedDirectly) {
let args
try {
args = parseArgs(process.argv.slice(2))
} catch (err) {
console.error(err.message)
process.exit(2)
}
nachpruefen(args.basis, args.erwartet)
.then(({ ok, zeilen }) => {
for (const z of zeilen) console.log(z.ok ? `ok ${z.pfad}` : `FEHLER ${z.pfad} ${z.grund}`)
process.exit(ok ? 0 : 1)
})
.catch((err) => {
console.error(`FEHLER ${err.message}`)
process.exit(1)
})
}