Filipe: "ich will dass du die vier klassen ueberall mit aufgaben perfektionnierst ... ich will dass die 4 kategorien ueberall perfekt angepasst sind, mit perfekt kategorien und aufgaben." WAS GEFEHLT HAT, NACHGEMESSEN Die Tabelle `aufgaben` kannte weder Bereich noch Stufe, und Aufgabenvorlagen gab es keine. Die Checkliste sagt, WAS sitzen muss -- sie sagt nicht, was jemand als Naechstes TUT. Zwischen "Ton ist der Grund Nummer eins" und einem Abend, an dem der Ton wirklich besser wird, liegt eine Aufgabe mit Frist und Verantwortlichem. Die musste bisher jemand von Hand tippen. WAS JETZT DASTEHT live 12 Aufgaben · 3 je Stufe content 12 Aufgaben · 3 je Stufe technik 12 Aufgaben · 3 je Stufe community 12 Aufgaben · 3 je Stufe zusammen 48 Drei je Feld und nicht zehn: Wer zehn Aufgaben bekommt, macht keine. Drei sind ein Abend, eine Woche, ein Monat -- und die Fristen sind entsprechend gestaffelt. Jede Aufgabe bringt ihre Frist als `tage` mit; eine Aufgabe ohne Frist ist ein Wunsch. Die Sachaussagen stammen aus derselben Recherche wie die Checklistenpunkte: Lautheit minus 20 bis minus 24 LUFS, Tonspur 160 kbit/s, Bitrate hoechstens 75 Prozent des Uploads, Ankuendigung 30 bis 60 Minuten vorher, Verweildauer ueber 15 Minuten, Eskalation in vier Stufen, Intro-Retention ab 70 Prozent. DER WEG BIS ZUR ECHTEN AUFGABE `/workspace/api/vorlagen` liefert sie aus, `/uebernehmen` legt sie an -- als Zeile in `aufgaben`, nicht als weiterer Eintrag in `eintraege`. Eine Aufgabe hat einen Verantwortlichen, eine Frist und einen Status, der sich bewegt; das ist etwas anderes als eine Notiz. Zwei Orte fuer dieselbe Sache waeren der Anfang davon, dass sie auseinanderlaufen. Ohne Creator wird abgelehnt statt herrenlos angelegt. Ein Creator legt sie immer fuer SICH an, auch wenn er jemand anderen angibt. AUF DER SEITE Ein Block ueber dem Aufgabenbrett: zwei Knopfreihen (Bereich, Stufe), drei Karten mit Titel, Erklaerung, Frist und einem Knopf. Die Knopfreihen benutzen `.stufenleiter` aus der Checkliste -- dieselbe Einteilung, dieselbe Darstellung. Eine zweite Optik fuer dieselben vier Bereiche waeren zwei Sachen zum Lernen statt einer. ZWEI EIGENE FEHLER, BEIDE VON PRUEFUNGEN GEFUNDEN 1. Ich hatte verlangt, dass die Frist mit der Stufe waechst. Die Pruefung schlug fehl: Profi 6,7 Tage gegen Fortgeschritten 7,4. Beim Nachsehen war die ERWARTUNG falsch. Eine Profi-Aufgabe ist meist eine Messung -- anspruchsvoll, aber an einem Abend erledigt; eine Fortgeschrittenen-Aufgabe wie "Saeulen anlegen und drei Wochen halten" braucht zwangslaeufig Wochen. Die Stufe sagt, was man KOENNEN muss, nicht wie lange es dauert. Geprueft wird jetzt, was wirklich gilt: keine Anfaengeraufgabe ueber fuenf Tage, keine Meisteraufgabe unter einer Woche. 2. Der Block war zu durchsichtig. pruef-buehne mass den Untergrund als rgb(84, 69, 70) -- das Buehnenfoto -- und die Ueberschrift kam auf 3,61:1 statt 4,5:1. Ein durchsichtiger Kasten hat keinen Untergrund, sondern den, der gerade dahinterliegt. Jetzt eine deckende Flaeche mit Weichzeichner: 5,21:1. GEPRUEFT Neu: server/pruef-aufgaben-vorlagen.mjs, 30 Pruefungen -- Vollstaendigkeit, der Weg bis zur Aufgabe auf dem Brett des Creators, und sechs Gegenproben (unbekannte Stufe, unbekannter Bereich, Nummer ohne Vorlage, ohne Creator, fremder Creator, und ein Creator, der jemand anderen angibt). Dazu ein Browserteil, der klickt statt nur zu zaehlen. Ausserdem gruen: pruef-aufgabenbrett, pruef-buehne, pruef-css-klassen, pruef-handy. Co-Authored-By: Claude Opus 5 <[email protected]>
278 lines
15 KiB
JavaScript
278 lines
15 KiB
JavaScript
/* Prueft die Aufgaben-Vorlagen: vier Bereiche, vier Stufen, und den
|
|
Weg von der Vorlage zur echten Aufgabe.
|
|
|
|
WARUM DAS EINE EIGENE PRUEFUNG IST. Die Vorlagen sind Daten -- dass
|
|
sie vollstaendig sind, laesst sich in Sekunden zaehlen. Interessant
|
|
ist der Weg DANACH: Aus einer Vorlage muss eine Zeile in `aufgaben`
|
|
werden, mit Creator, Frist und Status, und sie muss in der
|
|
Aufgabenliste des Creators wirklich auftauchen. Zwischen "die Vorlage
|
|
gibt es" und "der Creator sieht die Aufgabe" liegen vier Schichten,
|
|
und jede kann still danebengehen.
|
|
|
|
DREI SORTEN GEGENPROBE, alle drei noetig:
|
|
- eine unbekannte Stufe MUSS abgelehnt werden,
|
|
- eine Uebernahme ohne Creator MUSS abgelehnt werden (eine Aufgabe
|
|
ohne Adressaten ist ein Zettel),
|
|
- ein Betreuer darf sie NICHT fuer einen fremden Creator anlegen. */
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-aufv-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
process.env.PORT = "4211";
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
|
|
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
|
|
|
const express = (await import("express")).default;
|
|
const ec = express.response.cookie;
|
|
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
await import("./index.js");
|
|
notbremse(240_000, "pruef-aufgaben-vorlagen");
|
|
await new Promise((r) => setTimeout(r, 700));
|
|
const BASIS = "http://127.0.0.1:4211";
|
|
|
|
let fehler = 0;
|
|
const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes } = await import("node:crypto");
|
|
for (let i = 0; i < 60; i++) {
|
|
try { const p = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); }
|
|
} catch { await new Promise((r) => setTimeout(r, 250)); }
|
|
}
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const jetzt = new Date().toISOString();
|
|
function anlegen(name, rolle, code) {
|
|
const salt = randomBytes(16).toString("hex");
|
|
const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
|
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
|
|
.run(name, rolle, hash, salt, 32768, jetzt);
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
}
|
|
anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
|
const idScout = anlegen("Patrick", "scout", "CODE-SCOU-0001");
|
|
const idTili = anlegen("Tili", "creator", "CODE-ANNA-0001");
|
|
const idFremd = anlegen("Fremd", "creator", "CODE-FREM-0001");
|
|
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idTili, idScout, jetzt);
|
|
d.close();
|
|
|
|
const keks = async (rolle, code) => {
|
|
const a = await fetch(BASIS + "/workspace/api/anmelden", {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ rolle, code }) });
|
|
return (a.headers.getSetCookie?.() || []).map((k) => k.split(";")[0]).join("; ");
|
|
};
|
|
const kScout = await keks("scout", "CODE-SCOU-0001");
|
|
const kTili = await keks("creator", "CODE-ANNA-0001");
|
|
const ruf = (pfad, k, o = {}) => fetch(BASIS + pfad, { ...o, headers: { Cookie: k, ...(o.headers || {}) } });
|
|
const senden = (pfad, k, art, koerper) => ruf(pfad, k, {
|
|
method: art, headers: { "Content-Type": "application/json" }, body: JSON.stringify(koerper || {}) });
|
|
|
|
/* ---------- 1. Die Vorlagen sind vollstaendig ---------------------------- */
|
|
console.log("\n=== Vier Bereiche, vier Stufen, drei Aufgaben");
|
|
const alles = await (await ruf("/workspace/api/vorlagen", kScout)).json();
|
|
const V = alles.aufgaben;
|
|
ok(!!V, "die Aufgabenvorlagen werden ausgeliefert");
|
|
const BEREICHE = ["live", "content", "technik", "community"];
|
|
const STUFEN = ["anfaenger", "fort", "profi", "meister"];
|
|
let gesamt = 0;
|
|
for (const b of BEREICHE) {
|
|
const fehlend = STUFEN.filter((st) => !(V[b] || {})[st] || !V[b][st].length);
|
|
const n = STUFEN.reduce((s, st) => s + ((V[b] || {})[st] || []).length, 0);
|
|
gesamt += n;
|
|
ok(fehlend.length === 0 && n >= 8,
|
|
`${b}: ${n} Aufgaben über ${STUFEN.length - fehlend.length} von 4 Stufen`
|
|
+ (fehlend.length ? ` — fehlt: ${fehlend.join(", ")}` : ""));
|
|
}
|
|
ok(gesamt === 48, `zusammen ${gesamt} Aufgaben (erwartet 48)`);
|
|
|
|
/* Jede Aufgabe braucht Titel, Text, Frist und einen eindeutigen
|
|
Schluessel -- eine Aufgabe ohne Frist ist ein Wunsch. */
|
|
{
|
|
const flach = BEREICHE.flatMap((b) => STUFEN.flatMap((st) => (V[b][st] || [])));
|
|
const unvollstaendig = flach.filter((v) => !v.titel || !v.text || !v.tage || !v.schluessel);
|
|
ok(unvollstaendig.length === 0,
|
|
`jede Aufgabe hat Titel, Text, Frist und Schlüssel (${flach.length - unvollstaendig.length} von ${flach.length})`);
|
|
const schluessel = flach.map((v) => v.schluessel);
|
|
const doppelt = schluessel.filter((x, i) => schluessel.indexOf(x) !== i);
|
|
ok(doppelt.length === 0, `alle Schlüssel eindeutig${doppelt.length ? ": " + doppelt.join(", ") : ""}`);
|
|
/* DIE FRIST WAECHST NICHT DURCHGEHEND MIT DER STUFE -- und das ist
|
|
kein Mangel.
|
|
|
|
Der erste Anlauf verlangte anfaenger < fort < profi < meister und
|
|
schlug fehl: Profi kam auf 6,7 Tage, Fortgeschritten auf 7,4. Beim
|
|
Nachsehen war die Erwartung falsch, nicht die Zahl. Eine
|
|
Profi-Aufgabe ist meist eine MESSUNG -- "Upload messen und Bitrate
|
|
setzen", "Lautheit auf minus 20 bis minus 24 bringen": anspruchsvoll,
|
|
aber an einem Abend erledigt. Eine Fortgeschrittenen-Aufgabe wie
|
|
"Content-Saeulen anlegen und drei Wochen halten" braucht
|
|
zwangslaeufig Wochen, weil sie aus Wiederholung besteht.
|
|
|
|
Die Stufe sagt, was man KOENNEN muss, nicht wie lange es dauert.
|
|
|
|
Was dagegen wirklich gelten muss: Eine Anfaengeraufgabe ist an einem
|
|
Abend zu schaffen (sonst faengt niemand an), und eine Meisteraufgabe
|
|
ist es nie (sonst ist sie keine). */
|
|
const schnitt = (st) => {
|
|
const l = BEREICHE.flatMap((b) => V[b][st] || []);
|
|
return l.reduce((s, v) => s + v.tage, 0) / l.length;
|
|
};
|
|
const [sa, sf, sp, sm] = STUFEN.map(schnitt);
|
|
const anfListe = BEREICHE.flatMap((b) => V[b].anfaenger || []);
|
|
const meiListe = BEREICHE.flatMap((b) => V[b].meister || []);
|
|
const anfZuLang = anfListe.filter((v) => v.tage > 5);
|
|
const meiZuKurz = meiListe.filter((v) => v.tage < 7);
|
|
ok(anfZuLang.length === 0,
|
|
`jede Anfängeraufgabe ist in höchstens fünf Tagen zu schaffen`
|
|
+ (anfZuLang.length ? ` — zu lang: ${anfZuLang.map((v) => v.schluessel).join(", ")}` : ""));
|
|
ok(meiZuKurz.length === 0,
|
|
`keine Meisteraufgabe unter einer Woche`
|
|
+ (meiZuKurz.length ? ` — zu kurz: ${meiZuKurz.map((v) => v.schluessel).join(", ")}` : ""));
|
|
ok(sa < sm,
|
|
`Anfänger deutlich kürzer als Meister (${sa.toFixed(1)} gegen ${sm.toFixed(1)} Tage;`
|
|
+ ` fort ${sf.toFixed(1)}, profi ${sp.toFixed(1)} — dazwischen gilt keine Reihenfolge)`);
|
|
/* GEGENPROBE: Eine erfundene Anfaengeraufgabe mit 30 Tagen MUSS
|
|
auffallen -- sonst prueft die Regel oben nichts. */
|
|
ok([...anfListe, { schluessel: "test", tage: 30 }].filter((v) => v.tage > 5).length === 1,
|
|
`Gegenprobe: eine Anfängeraufgabe mit 30 Tagen fällt auf`);
|
|
}
|
|
|
|
/* ---------- 2. Aus der Vorlage wird eine echte Aufgabe -------------------- */
|
|
console.log("\n=== Von der Vorlage zur Aufgabe");
|
|
{
|
|
const vorher = (await (await ruf("/workspace/api/aufgaben", kTili)).json()).aufgaben?.length ?? 0;
|
|
const a = await senden("/workspace/api/vorlagen/uebernehmen", kScout, "POST",
|
|
{ art: "aufgabe", bereich: "technik", stufe: "anfaenger", nr: 0, creator_id: idTili });
|
|
ok(a.ok, `der Scout kann eine Aufgabe für seinen Creator übernehmen (${a.status})`);
|
|
|
|
const liste = (await (await ruf("/workspace/api/aufgaben", kTili)).json()).aufgaben || [];
|
|
ok(liste.length === vorher + 1, `sie steht in der Liste des Creators (${vorher} → ${liste.length})`);
|
|
const neu = liste.find((x) => x.titel === V.technik.anfaenger[0].titel);
|
|
ok(!!neu, `mit dem richtigen Titel ("${V.technik.anfaenger[0].titel}")`);
|
|
ok(!!neu && neu.status === "offen", `als offen angelegt (${neu?.status})`);
|
|
ok(!!neu && !!neu.frist, `und mit einer Frist (${neu?.frist})`);
|
|
/* Die Frist muss in der Zukunft liegen -- und zwar nach ORTSZEIT
|
|
gerechnet, nicht nach UTC. */
|
|
if (neu?.frist) {
|
|
const heute = new Date();
|
|
const h = `${heute.getFullYear()}-${String(heute.getMonth() + 1).padStart(2, "0")}`
|
|
+ `-${String(heute.getDate()).padStart(2, "0")}`;
|
|
ok(neu.frist > h, `die Frist liegt in der Zukunft (heute ${h}, Frist ${neu.frist})`);
|
|
}
|
|
}
|
|
|
|
/* Alle drei einer Stufe auf einmal. */
|
|
{
|
|
const vorher = ((await (await ruf("/workspace/api/aufgaben", kTili)).json()).aufgaben || []).length;
|
|
const a = await senden("/workspace/api/vorlagen/uebernehmen", kScout, "POST",
|
|
{ art: "aufgaben_alle", bereich: "community", stufe: "meister", creator_id: idTili });
|
|
const nachher = ((await (await ruf("/workspace/api/aufgaben", kTili)).json()).aufgaben || []).length;
|
|
ok(a.ok && nachher === vorher + 3,
|
|
`alle drei einer Stufe auf einmal (${vorher} → ${nachher})`);
|
|
}
|
|
|
|
/* ---------- 3. Gegenproben ------------------------------------------------ */
|
|
console.log("\n=== Gegenproben");
|
|
{
|
|
const a = await senden("/workspace/api/vorlagen/uebernehmen", kScout, "POST",
|
|
{ art: "aufgabe", bereich: "technik", stufe: "gibt-es-nicht", nr: 0, creator_id: idTili });
|
|
ok(a.status === 400, `eine unbekannte Stufe wird abgelehnt (${a.status})`);
|
|
|
|
const b = await senden("/workspace/api/vorlagen/uebernehmen", kScout, "POST",
|
|
{ art: "aufgabe", bereich: "erfunden", stufe: "anfaenger", nr: 0, creator_id: idTili });
|
|
ok(b.status === 400, `ein unbekannter Bereich wird abgelehnt (${b.status})`);
|
|
|
|
const c = await senden("/workspace/api/vorlagen/uebernehmen", kScout, "POST",
|
|
{ art: "aufgabe", bereich: "technik", stufe: "anfaenger", nr: 99, creator_id: idTili });
|
|
ok(c.status === 400, `eine Nummer, die es nicht gibt, wird abgelehnt (${c.status})`);
|
|
|
|
const e = await senden("/workspace/api/vorlagen/uebernehmen", kScout, "POST",
|
|
{ art: "aufgabe", bereich: "technik", stufe: "anfaenger", nr: 0 });
|
|
ok(e.status === 400, `ohne Creator wird abgelehnt — eine Aufgabe ohne Adressaten ist ein Zettel (${e.status})`);
|
|
|
|
const f = await senden("/workspace/api/vorlagen/uebernehmen", kScout, "POST",
|
|
{ art: "aufgabe", bereich: "technik", stufe: "anfaenger", nr: 0, creator_id: idFremd });
|
|
ok(f.status === 404, `für einen fremden Creator wird abgelehnt (${f.status})`);
|
|
|
|
/* Und der Creator selbst legt sie fuer sich an -- ohne creator_id,
|
|
der Server setzt sie. Sonst koennte er sie jemand anderem geben. */
|
|
const g = await senden("/workspace/api/vorlagen/uebernehmen", kTili, "POST",
|
|
{ art: "aufgabe", bereich: "live", stufe: "anfaenger", nr: 0, creator_id: idFremd });
|
|
const beiFremd = ((await (await ruf("/workspace/api/aufgaben", kTili)).json()).aufgaben || [])
|
|
.filter((x) => x.titel === V.live.anfaenger[0].titel).length;
|
|
ok(g.ok && beiFremd === 1,
|
|
`ein Creator legt sie für SICH an, auch wenn er jemand anderen angibt (${g.status})`);
|
|
}
|
|
|
|
/* ---------- 4. Im Browser: sieht man es, und geht es? -------------------- */
|
|
console.log("\n=== Auf der Aufgabenseite");
|
|
{
|
|
const { chromium } = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await chromium.launch();
|
|
const kontext = await browser.newContext({ viewport: { width: 1200, height: 1400 } });
|
|
const seite = await kontext.newPage();
|
|
const konsole = [];
|
|
seite.on("pageerror", (f) => konsole.push(f.message));
|
|
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
|
await seite.click('.rolle[data-rolle="creator"]');
|
|
await seite.fill("#code", "CODE-ANNA-0001");
|
|
await seite.click("button[type=submit]");
|
|
await seite.waitForURL("**/start.html", { timeout: 20000 });
|
|
await seite.goto(BASIS + "/workspace/aufgaben.html", { waitUntil: "domcontentloaded" });
|
|
await seite.waitForTimeout(2500);
|
|
|
|
const d = await seite.evaluate(() => {
|
|
const b = document.getElementById("vorlagenbrett");
|
|
return {
|
|
sichtbar: !!b && !b.hidden,
|
|
reihen: document.querySelectorAll("#vorlagenbrett .stufenleiter").length,
|
|
karten: document.querySelectorAll("#vorlagenbrett .vorlagenbrett__karte").length,
|
|
knoepfe: document.querySelectorAll("#vorlagenbrett .vorlagenbrett__fuss .knopf").length,
|
|
/* Gestaltet oder nackt? Eine Karte ohne Kante ist keine Karte --
|
|
derselbe Fehler wie damals bei der Checkliste, wo das CSS in
|
|
der falschen Datei lag und die Pruefung trotzdem gruen war,
|
|
weil sie nur Elemente gezaehlt hat. */
|
|
gestaltet: (() => {
|
|
const k = document.querySelector("#vorlagenbrett .vorlagenbrett__karte");
|
|
if (!k) return null;
|
|
const st = getComputedStyle(k);
|
|
return parseFloat(st.borderTopWidth) >= 1 && parseFloat(st.paddingLeft) >= 8;
|
|
})(),
|
|
ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
|
};
|
|
});
|
|
ok(d.sichtbar, "der Block ist da");
|
|
ok(d.reihen === 2, `zwei Knopfreihen: Bereich und Stufe (${d.reihen})`);
|
|
ok(d.karten === 3, `drei Aufgaben je Feld (${d.karten})`);
|
|
ok(d.knoepfe === 3, `jede hat einen Übernehmen-Knopf (${d.knoepfe})`);
|
|
ok(d.gestaltet === true, "die Karten sind gestaltet, nicht nackt");
|
|
ok(d.ueber <= 0, `nichts steht seitlich über (${d.ueber}px)`);
|
|
|
|
/* Der Bereich laesst sich wechseln, und die Karten wechseln mit. */
|
|
const vorher = await seite.textContent("#vorlagenbrett .vorlagenbrett__titel");
|
|
await seite.click("#vorlagenbrett .stufenleiter >> nth=0 >> .stufenleiter__knopf >> nth=2");
|
|
await seite.waitForTimeout(500);
|
|
const nachher = await seite.textContent("#vorlagenbrett .vorlagenbrett__titel");
|
|
ok(vorher !== nachher, `ein anderer Bereich zeigt andere Aufgaben ("${vorher}" → "${nachher}")`);
|
|
|
|
/* Und der Weg bis zum Brett. */
|
|
const zaehle = () => seite.evaluate(() => document.querySelectorAll("#brett .karte").length);
|
|
const brettVorher = await zaehle();
|
|
await seite.click("#vorlagenbrett .vorlagenbrett__fuss .knopf");
|
|
await seite.waitForTimeout(2500);
|
|
const brettNachher = await zaehle();
|
|
ok(brettNachher === brettVorher + 1,
|
|
`Übernehmen legt eine Aufgabe auf dem Brett an (${brettVorher} → ${brettNachher})`);
|
|
ok(konsole.length === 0, `keine Fehler in der Konsole${konsole.length ? ": " + konsole[0] : ""}`);
|
|
await browser.close();
|
|
}
|
|
|
|
console.log(fehler ? `\n${fehler} Prüfung(en) fehlgeschlagen.` : "\nAlles in Ordnung.");
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
process.exit(fehler ? 1 : 0);
|