generated from knso/webklar-preview-template
Demo-Seite: - Ampel in Kalendertagen, naechstes kommendes Datum, "Termin am"/"Frist bis" - nie "Deutsch | Deutsch"; Beraterin bekommt immer Deutsch (d=1), das Handy erkennt seine Sprache selbst und speichert die PC-Vorgabe nicht dauerhaft - Code-Feld ausserhalb von #eingabe; tolerante Code-Eingabe; Ablaufdatum im Banner; leeres/abgelaufenes Einrichtungs-Kontingent ohne Kaufangebot, mit Telefonnummer; nach Aufladen reicht ein Neuladen - Handy-Modus: endet nach dem ersten Brief, uebersteht ein Neuladen (sessionStorage), keine Sackgasse ohne Zugang, ehrlicher Hinweis + "Noch einmal senden", wenn das Ergebnis den PC nicht erreicht - PC wartet geduldig, solange verbunden; meldet langsame Analysen, fehlende Verbindung, fehlenden Zugang; Warnung beim Verlassen - Einwilligung pro Brief, nennt Gesundheit/Herkunft/Aufenthalt - localStorage nur ueber abgesicherten Helfer; Dateifeld wird geleert - RTL (bdi, dir=auto), Sprachliste mit deutschem Zusatz, groesserer QR Worker: KI-Aufruf und Router-catch liefern immer JSON mit CORS; Logs ohne Briefinhalt; [observability]; Nutzungszaehler je Analyse (Datum, Sprache, 390 Tage); normCode + 30 Fehlversuche; Uebergabe-TTL gleitend. Werkzeug: Auswertung pro Woche/Sprache, Eingabepruefung, --bis in der Hilfe. Datenschutz: SCC statt DPF, "in der Regel 30 Tage", Server-Protokolle, Nutzungszaehler. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
148 lines
6.2 KiB
JavaScript
148 lines
6.2 KiB
JavaScript
/**
|
|
* Einrichtungs-Codes für Beratungsstellen anlegen und verwalten.
|
|
*
|
|
* node tools/einrichtungs-code.js neu "MBE Donnersberg" MBE --briefe 100 --bis 2026-12-18
|
|
* node tools/einrichtungs-code.js neu "MBE Donnersberg" MBE --briefe 100 --monate 1
|
|
* node tools/einrichtungs-code.js stand MBE-K7QX (mit Nutzung nach Woche und Sprache)
|
|
* node tools/einrichtungs-code.js aufladen MBE-K7QX 100
|
|
* node tools/einrichtungs-code.js verlaengern MBE-K7QX 2027-06-30
|
|
*
|
|
* Ein Code ist ein Guthaben wie beim Kauf, das allen gehört, die den Code
|
|
* kennen. Die Ratsuchenden zahlen nichts, die Einrichtung zahlt (oder testet
|
|
* gratis). Gilt bis zum Ablaufdatum oder bis die Briefe aufgebraucht sind.
|
|
*
|
|
* Braucht nur den wrangler-Login (npx wrangler login), kein Secret.
|
|
* Link für Handzettel/QR-Code: https://behoerdenklar.de/demo?code=<CODE>
|
|
*/
|
|
const { execFileSync } = require('child_process');
|
|
const crypto = require('crypto');
|
|
const path = require('path');
|
|
|
|
const PROXY = path.join(__dirname, '..', 'proxy');
|
|
const KV_ID = '7577adef8ceb4ed6bfaeef81a1d496af'; // RATE_LIMIT aus proxy/wrangler.toml
|
|
// ohne 0/O, 1/I/L: am Telefon und auf Papier nicht zu verwechseln
|
|
const ZEICHEN = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
|
|
|
|
function kv(...args) {
|
|
return execFileSync('npx', ['wrangler', 'kv', 'key', ...args, '--namespace-id', KV_ID, '--remote'],
|
|
{ cwd: PROXY, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
}
|
|
function lies(schluessel) {
|
|
try { return kv('get', schluessel, '--text'); } catch { return null; }
|
|
}
|
|
function schreib(schluessel, wert) { kv('put', schluessel, wert); }
|
|
|
|
function option(name, standard) {
|
|
const i = process.argv.indexOf('--' + name);
|
|
return i > 0 ? process.argv[i + 1] : standard;
|
|
}
|
|
function abbruch(text) { console.error('\x1b[31m' + text + '\x1b[0m'); process.exit(1); }
|
|
|
|
function zufall(laenge) {
|
|
let s = '';
|
|
for (const b of crypto.randomBytes(laenge)) s += ZEICHEN[b % ZEICHEN.length];
|
|
return s;
|
|
}
|
|
|
|
function plusMonate(monate) {
|
|
const d = new Date();
|
|
d.setMonth(d.getMonth() + monate);
|
|
return d.toISOString().slice(0, 10);
|
|
}
|
|
|
|
/** Wie im Worker: „mbe k7qx" → „MBE-K7QX". */
|
|
function normCode(c) {
|
|
const roh = String(c || '').toUpperCase().replace(/[^A-Z0-9]/g, '');
|
|
return roh.length > 4 ? `${roh.slice(0, -4)}-${roh.slice(-4)}` : roh;
|
|
}
|
|
|
|
/** Nutzung je Analyse (vom Worker: nutzung:<walletId>:<datum>:<sprache>:<zufall>). */
|
|
function nutzung(walletId) {
|
|
let liste = [];
|
|
try {
|
|
liste = JSON.parse(kv('list', '--prefix', `nutzung:${walletId}:`));
|
|
} catch { return null; }
|
|
return liste.map((e) => {
|
|
const [, , datum, sprache] = e.name.split(':');
|
|
return { datum, sprache };
|
|
});
|
|
}
|
|
|
|
function ladeCode(code) {
|
|
code = normCode(code);
|
|
const walletId = lies(`code:${code}`);
|
|
if (!walletId) abbruch(`Code ${code} gibt es nicht.`);
|
|
const einrichtung = JSON.parse(lies(`einrichtung:${walletId}`) || '{}');
|
|
const rest = parseInt(lies(`wallet:${walletId}`) || '0', 10);
|
|
return { code, walletId, einrichtung, rest };
|
|
}
|
|
|
|
function zeige({ code, einrichtung, rest, walletId }) {
|
|
const heute = new Date().toISOString().slice(0, 10);
|
|
const n = walletId ? nutzung(walletId) : null;
|
|
let auswertung = '';
|
|
if (n && n.length) {
|
|
const proWoche = {}, proSprache = {};
|
|
for (const { datum, sprache } of n) {
|
|
const d = new Date(`${datum}T00:00:00Z`);
|
|
const montag = new Date(d.getTime() - ((d.getUTCDay() + 6) % 7) * 86400000).toISOString().slice(0, 10);
|
|
proWoche[montag] = (proWoche[montag] || 0) + 1;
|
|
proSprache[sprache] = (proSprache[sprache] || 0) + 1;
|
|
}
|
|
auswertung = `
|
|
Genutzt: ${n.length} Briefe
|
|
Pro Woche: ${Object.keys(proWoche).sort().map((w) => `ab ${w}: ${proWoche[w]}`).join(' · ')}
|
|
Sprachen: ${Object.entries(proSprache).sort((a, b) => b[1] - a[1]).map(([s, z]) => `${s} ${z}`).join(' · ')}`;
|
|
} else if (n) {
|
|
auswertung = '\n Genutzt: noch keine Briefe';
|
|
}
|
|
console.log(`
|
|
Einrichtung: ${einrichtung.name}
|
|
Code: \x1b[1m${code}\x1b[0m
|
|
Übrig: ${rest} Briefe
|
|
Gültig bis: ${einrichtung.ablauf}${einrichtung.ablauf < heute ? ' \x1b[31m(abgelaufen)\x1b[0m' : ''}
|
|
Link: https://behoerdenklar.de/demo?code=${code}${auswertung}
|
|
`);
|
|
}
|
|
|
|
const [befehl, a, b] = process.argv.slice(2);
|
|
|
|
if (befehl === 'neu') {
|
|
const name = a;
|
|
const praefix = String(b || '').toUpperCase();
|
|
if (!name || !/^[A-Z]{2,12}$/.test(praefix)) {
|
|
abbruch('So geht es: node tools/einrichtungs-code.js neu "MBE Donnersberg" MBE --briefe 100 --monate 1');
|
|
}
|
|
const briefe = parseInt(option('briefe', '100'), 10);
|
|
const monate = parseInt(option('monate', '1'), 10);
|
|
const ablauf = option('bis', Number.isFinite(monate) && monate > 0 ? plusMonate(monate) : '');
|
|
if (!(briefe > 0)) abbruch('--briefe braucht eine Zahl größer 0, z. B. --briefe 100');
|
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(ablauf)) abbruch('--bis braucht ein Datum wie 2026-12-18 (oder --monate 1)');
|
|
const code = `${praefix}-${zufall(4)}`;
|
|
if (lies(`code:${code}`)) abbruch('Zufällig schon vergeben — einfach nochmal ausführen.');
|
|
const walletId = crypto.randomUUID();
|
|
schreib(`wallet:${walletId}`, String(briefe));
|
|
schreib(`einrichtung:${walletId}`, JSON.stringify({ name, ablauf }));
|
|
schreib(`code:${code}`, walletId);
|
|
console.log('\n \x1b[32m✓ Code angelegt\x1b[0m');
|
|
zeige({ code, einrichtung: { name, ablauf }, rest: briefe, walletId });
|
|
} else if (befehl === 'stand') {
|
|
zeige(ladeCode(a));
|
|
} else if (befehl === 'aufladen') {
|
|
const c = ladeCode(a);
|
|
const dazu = parseInt(b, 10);
|
|
if (!(dazu > 0)) abbruch('So geht es: node tools/einrichtungs-code.js aufladen MBE-K7QX 100');
|
|
schreib(`wallet:${c.walletId}`, String(c.rest + dazu));
|
|
console.log(`\n \x1b[32m✓ ${dazu} Briefe dazu\x1b[0m`);
|
|
zeige({ ...c, rest: c.rest + dazu });
|
|
} else if (befehl === 'verlaengern') {
|
|
const c = ladeCode(a);
|
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(b || '')) abbruch('So geht es: node tools/einrichtungs-code.js verlaengern MBE-K7QX 2027-06-30');
|
|
const einrichtung = { ...c.einrichtung, ablauf: b };
|
|
schreib(`einrichtung:${c.walletId}`, JSON.stringify(einrichtung));
|
|
console.log('\n \x1b[32m✓ verlängert\x1b[0m');
|
|
zeige({ ...c, einrichtung });
|
|
} else {
|
|
console.log(require('fs').readFileSync(__filename, 'utf8').split('*/')[0].replace(/^\/\*\*?|^ \* ?/gm, ''));
|
|
}
|