DIE ZWEI FEHLER AUS DEM LETZTEN LAUF WAREN KEINE FEHLER AM PRODUKT.
FEHL das GIF steht danach wirklich im Verlauf
FEHL und die Tafel geht zu -- man will sehen, wie es ankommt
Gemessen statt geraten: eine Sonde, die jede Anfrage mitschreibt. Die
Antwort stand sofort da, um 04:30 Uhr:
POST /workspace/api/chat/raeume/1/gif
403 {"fehler":"Das Rudel schläft. Ab 06:00 Uhr geht es weiter …",
"nachtruhe":{"zu":true,"ab":0,"bis":6,"minuten":90}}
Die Pruefung klickte „das erste Gespraech in der Liste". Das erste ist
der TREFF, den der Server beim Start selbst anlegt -- und im Treff gilt
die Nachtruhe von 0 bis 6 Uhr. Also: tagsueber gruen, nachts rot, seit
es diese Pruefung gibt. Es ist derselbe Fehler wie am 06.09. im Shop
(„ein Test, der die Wanduhr als Annahme benutzt, misst irgendwann das
Gegenteil") -- dort ein Oeffnungstermin, hier ein Raum mit Nachtruhe.
Warum es nie jemandem auffiel: Fuer Dogfather Universe gibt es keinen
naechtlichen Pruefdienst (nachgesehen: `systemctl list-timers` kennt nur
`vandiy-pruefung` und `runone-pruefung`). Die Pruefung lief immer nur
dann, wenn jemand sie von Hand startete -- und das war bisher nie
nachts.
WAS GEAENDERT IST
1. pruef-gifs legt ein eigenes Gespraech mit Miss an und oeffnet GENAU
DAS. Fuer ein normales Gespraech gibt es keine Nachtruhe; die Messung
gilt jetzt rund um die Uhr. Findet es den Knopf nicht, bricht es laut
ab statt still auf nichts zu klicken.
Gemessen, 04:40 Uhr:
verschicken: {"vorher":0,"nachher":1,"imVerlauf":true,
"adressen":["/workspace/api/chat/anhang/1"],
"tafelZu":true}
17 Pruefungen, 0 Fehler. Und nebenbei belegt die Adresse, was der
Quelltext behauptet: Das GIF wird KOPIERT und haengt danach als
ganz normaler Anhang an der Nachricht.
2. DIE NACHTRUHE IST NICHT UNTER DEN TISCH GEFALLEN -- sie steht jetzt
dort, wo sie hingehoert: in pruef-treffchat (110 -> 114). Dort wird
das Fenster ueber die EINSTELLUNG verschoben, nicht ueber die
Systemuhr, und beide Zustaende kommen in einem Lauf vor.
DENN DER GIF-WEG WAR DORT ALS EINZIGER DER DREI UEBERHAUPT NICHT
GEPRUEFT. Im Quelltext steht neben der Regel woertlich: „Sie nur
beim Text und beim Anhang zu pruefen hiesse, dass man nachts zwar
nicht schreiben, aber ein GIF schicken kann -- der Weg, den jeder
findet, der es einmal versucht." Genau dieser Weg hatte keine
Pruefung. Der Kommentar hat den Fehler beschrieben und nicht
verhindert -- dieselbe Luecke wie beim Spaltenverlust vom 11.09.
Gemessen, beide Zweige in einem Lauf:
Fenster 7–9 Uhr, jetzt 4 -> 404 „Dieses GIF gibt es nicht mehr."
Fenster 4–6 Uhr, jetzt 4 -> 403 „Das Rudel schläft. Ab 06:00 …"
ZWEI ENTSCHEIDUNGEN DABEI, beide mit Grund:
· Gefragt wird als DogFather, nicht als Gast. Die GIF-Kiste gehoert
dem Rudel; ein Gast bekaeme seine 403 von der falschen Schranke
(`nurRudel`) -- gruen, ohne die Nachtruhe je beruehrt zu haben.
· Mit einer Nummer, die es NICHT gibt. Kommt trotzdem die
Nachtruhe-Absage, steht die Schranke VOR dem Nachschlagen.
Stuende sie dahinter, verriete der Server nachts an der Nummer,
welche GIFs es gibt.
AM PRODUKT IST NICHTS GEAENDERT. Nur zwei Pruefdateien -- kein Stempel,
kein Neustart noetig.
GEPRUEFT: pruef-gifs 17 ok (vorher 15 ok / 2 FEHL) ·
pruef-treffchat 114 ok (vorher 110).
Co-Authored-By: Claude Opus 5 <[email protected]>
318 lines
15 KiB
JavaScript
318 lines
15 KiB
JavaScript
/* DIE GIF-KISTE, VON VORN BIS HINTEN.
|
|
Filipe: „mach das mit den gifs auch fertig."
|
|
|
|
ERGEBNIS: SIE IST FERTIG. Alle sechs „Befunde" meines ersten
|
|
Laufs waren Messfehler -- zu frueh gemessen, falscher Kopf,
|
|
falscher Selektor, falsche Adresse, falscher Header, und einmal
|
|
eine Umleitung fuer einen nicht angemeldeten curl-Aufruf.
|
|
|
|
WARUM SIE TROTZDEM BLEIBT: pruef-chat-anhaenge prueft die WEGE
|
|
(hineinlegen, verschicken, Duplikate, Typ am Inhalt). Diese hier
|
|
prueft die OBERFLAECHE -- dass der Knopf fuer Team Dogi erscheint
|
|
und fuer einen Creator nicht, dass die Tafel aufgeht, dass an
|
|
jeder Kachel ein Weg zum Herausnehmen steht. Genau das war
|
|
ungeprueft, und genau dort haette ich beinahe gebaut, was es
|
|
laengst gibt. */
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-gf-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
const { eigenerPort } = await import("./helfer-port.mjs");
|
|
const PORT = await eigenerPort(import.meta, "die GIF-Kisten-Pruefung");
|
|
process.env.PORT = `${PORT}`;
|
|
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(200_000, "pruef-gifs");
|
|
await new Promise((r) => setTimeout(r, 900));
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
|
|
let fehler = 0, geprueft = 0;
|
|
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const jetzt = new Date().toISOString();
|
|
const ks = randomBytes(32).toString("hex");
|
|
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
|
|
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
|
.run("code_kennung_schluessel", ks, jetzt);
|
|
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,"
|
|
+ " code_kennung, aktiv, erstellt) VALUES (?,?,?,?,?,?,1,?)")
|
|
.run(name, rolle, hash, salt, 32768,
|
|
createHmac("sha256", ks).update(code).digest("hex"), jetzt);
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
}
|
|
anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
|
const idMiss = anlegen("Miss", "modi", "CODE-MODI-0001");
|
|
anlegen("Luna", "creator", "CODE-CREA-0001");
|
|
d.close();
|
|
|
|
const { chromium } = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await chromium.launch();
|
|
|
|
/* Ein winziges, gueltiges GIF (1x1). Echte Kopfbytes -- die Erkennung
|
|
liest sie, nicht den Namen. */
|
|
const GIF_B64 = "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
|
|
|
try {
|
|
const kontext = await browser.newContext({ viewport: { width: 1200, height: 900 } });
|
|
const seite = await kontext.newPage();
|
|
const konsole = [];
|
|
seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message));
|
|
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
|
await seite.click('.rolle[data-rolle="admin"]');
|
|
await seite.fill("#code", "CODE-DOGI-0001");
|
|
await seite.click("button[type=submit]");
|
|
await seite.waitForURL("**/start.html", { timeout: 15000 });
|
|
await seite.goto(BASIS + "/workspace/chat.html", { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(2000);
|
|
|
|
/* EINEN RAUM WIRKLICH OEFFNEN UND WARTEN.
|
|
|
|
Beim ersten Lauf stand "knopfSichtbar: false" -- und ich hielt
|
|
das fuer einen Befund. Es war zu frueh gemessen: `darf_gif`
|
|
kommt mit den Raumdaten, und die Zeile, die den Knopf
|
|
freischaltet, laeuft erst danach. `istTeamDogi(admin)` ist
|
|
true, nachgemessen.
|
|
|
|
Gewartet wird auf den ZUSTAND, nicht auf eine Zahl: bis der
|
|
Knopf sichtbar ist, laengstens acht Sekunden. */
|
|
/* DIE KLASSE HEISST `chat-raum__knopf` -- nachgesehen in chat.js
|
|
(Zeile 810), nicht geraten. Mein erster Versuch probierte drei
|
|
erfundene Namen und traf keinen; der Raum blieb zu, und der
|
|
GIF-Knopf deshalb verborgen. */
|
|
|
|
/* ==== EIN GANZ NORMALES GESPRAECH, NICHT DAS ERSTE DER LISTE ======
|
|
(02.10.2026 -- und das ist eine Zeitbombe, die hochgegangen ist)
|
|
|
|
Hier stand `document.querySelector(".chat-raum__knopf")?.click()`
|
|
-- also „nimm das erste Gespraech". Das erste ist der TREFF, den
|
|
der Server beim Start selbst anlegt. Und im Treff gilt die
|
|
Nachtruhe: zwischen 0 und 6 Uhr weist der Server jedes Schreiben
|
|
mit 403 ab, Text, Anhang und GIF.
|
|
|
|
FOLGE: Diese Pruefung war tagsueber gruen und nachts rot --
|
|
seit es sie gibt. Gemessen um 04:30 Uhr, und die Antwort stand
|
|
im Protokoll:
|
|
|
|
403 {"fehler":"Das Rudel schläft. Ab 06:00 Uhr geht es
|
|
weiter …","nachtruhe":{"zu":true,"ab":0,"bis":6}}
|
|
|
|
Nicht das GIF war kaputt, sondern die Uhr war eine Annahme. Das
|
|
ist derselbe Fehler wie am 06.09. im Shop („ein Test, der die
|
|
Wanduhr als Annahme benutzt, misst irgendwann das Gegenteil") --
|
|
dort war es ein Oeffnungstermin, hier ein Raum mit Nachtruhe.
|
|
|
|
Deshalb wird jetzt ein eigenes Gespraech mit Miss angelegt und
|
|
GENAU DAS geoeffnet: Fuer ein normales Gespraech gibt es keine
|
|
Nachtruhe, die Messung gilt rund um die Uhr.
|
|
|
|
UND DIE NACHTRUHE IST NICHT UNGEPRUEFT GEBLIEBEN: Sie steht
|
|
jetzt in pruef-treffchat, wo das Zeitfenster richtig gesetzt
|
|
wird (ueber die Einstellung, nicht ueber die Systemuhr) und
|
|
beide Zustaende in einem Lauf vorkommen. Dort war der GIF-Weg
|
|
als einziger der drei noch gar nicht geprueft. */
|
|
await seite.evaluate(async (id) => {
|
|
await fetch("/workspace/api/chat/direkt", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
credentials: "same-origin",
|
|
body: JSON.stringify({ mit: id }),
|
|
});
|
|
}, idMiss);
|
|
await seite.reload({ waitUntil: "networkidle" });
|
|
await seite.waitForSelector(".chat-raum__knopf", { timeout: 8000 });
|
|
const zuMiss = seite.locator(".chat-raum__knopf", { hasText: "Miss" }).first();
|
|
/* LAUT ABBRECHEN, WENN ES IHN NICHT GIBT. Ein stilles
|
|
`?.click()` auf nichts waere wieder eine Messung, die „der Raum
|
|
ist zu" meldet und den Grund verschweigt. */
|
|
await zuMiss.waitFor({ timeout: 8000 });
|
|
await zuMiss.click();
|
|
try {
|
|
await seite.waitForFunction(
|
|
() => { const k = document.getElementById("gif-auf"); return k && !k.hidden; },
|
|
{ timeout: 8000 });
|
|
} catch { /* dann ist er wirklich nicht da -- die Pruefung sagt es */ }
|
|
|
|
const lage = await seite.evaluate(() => {
|
|
const k = document.getElementById("gif-auf");
|
|
return {
|
|
knopfDa: !!k,
|
|
knopfSichtbar: k ? !k.hidden : false,
|
|
tafelDa: !!document.getElementById("gif-tafel"),
|
|
raumOffen: !!document.querySelector("#eingabe, .chat__eingabe, #nachricht"),
|
|
};
|
|
});
|
|
console.log(" " + JSON.stringify(lage));
|
|
ok(lage.knopfDa && lage.knopfSichtbar, "der GIF-Knopf steht da");
|
|
ok(lage.tafelDa, "und die Kiste gibt es");
|
|
|
|
/* ---- 1. Ein GIF hineinlegen (ueber die Route, wie der Knopf) ---- */
|
|
const rein = await seite.evaluate(async (b64) => {
|
|
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
/* MIT Content-Type. Ohne ihn nimmt express.raw den Rumpf nicht
|
|
an ("Keine Datei empfangen") -- beim ersten Lauf sah das aus
|
|
wie eine kaputte Route. Der echte Knopf schickt eine File,
|
|
und die bringt ihren Typ mit. */
|
|
const a = await fetch("/workspace/api/chat/gifs", {
|
|
method: "POST",
|
|
credentials: "same-origin",
|
|
/* DER SERVER LIEST `x-dateiname`, nicht `x-name` -- nachgesehen
|
|
in workspace-chat.js. Mit dem falschen Kopf hiess das GIF
|
|
"anhang", und ich hielt das fuer einen verlorenen Namen. */
|
|
headers: { "x-dateiname": encodeURIComponent("Winken.gif"),
|
|
"Content-Type": "application/octet-stream" },
|
|
body: bytes,
|
|
});
|
|
return { code: a.status, text: await a.text() };
|
|
}, GIF_B64);
|
|
console.log(" hineinlegen: HTTP " + rein.code + " " + rein.text.slice(0, 80));
|
|
ok(rein.code === 200 || rein.code === 201, `ein GIF geht in die Kiste (${rein.code})`);
|
|
|
|
/* ---- 2. Steht es in der Kiste? ---- */
|
|
const liste = await seite.evaluate(async () => {
|
|
const a = await fetch("/workspace/api/chat/gifs", { credentials: "same-origin" });
|
|
return a.ok ? await a.json() : { fehler: a.status };
|
|
});
|
|
const erstes = (liste.gifs || [])[0];
|
|
ok((liste.gifs || []).length >= 1, `die Kiste hat ${(liste.gifs || []).length} GIF(s)`);
|
|
ok(!!erstes?.weg, `mit einer Adresse zum Anzeigen (${erstes?.weg})`);
|
|
|
|
/* ---- 3. Die Tafel zeigt es ---- */
|
|
await seite.evaluate(() => document.getElementById("gif-auf")?.click());
|
|
await seite.waitForTimeout(900);
|
|
const tafel = await seite.evaluate(() => {
|
|
const t = document.getElementById("gif-tafel");
|
|
return {
|
|
offen: t ? !t.hidden : false,
|
|
kacheln: document.querySelectorAll("#gif-tafel img").length,
|
|
/* DER KNOPF HEISST `gif-kachel__weg` -- nachgesehen in chat.js,
|
|
nicht geraten. Mein erster Versuch suchte nach dem Wort
|
|
"ausnehm" im Titel; dort steht "Aus der Kiste nehmen", und
|
|
der Knopf fiel durch. Ich haette gebaut, was es laengst
|
|
gibt -- so wie heute frueh beim Farbwerkzeug. */
|
|
wegKnoepfe: document.querySelectorAll("#gif-tafel .gif-kachel__weg").length,
|
|
knoepfeGesamt: document.querySelectorAll("#gif-tafel button").length,
|
|
};
|
|
});
|
|
console.log(" Tafel: " + JSON.stringify(tafel));
|
|
ok(tafel.offen, "die Kiste geht auf");
|
|
ok(tafel.kacheln >= 1, `und zeigt das GIF (${tafel.kacheln})`);
|
|
ok(tafel.wegKnoepfe >= 1,
|
|
`es gibt einen Weg zum Herausnehmen (${tafel.wegKnoepfe} von ${tafel.knoepfeGesamt} Knoepfen)`);
|
|
ok((liste.gifs || [])[0]?.name === "Winken",
|
|
`der Name bleibt erhalten ("${(liste.gifs || [])[0]?.name}")`);
|
|
|
|
/* ==== DER EIGENTLICHE ZWECK: EIN GIF VERSCHICKEN ==============
|
|
|
|
Das hatte ich beim ersten Anlauf gar nicht gemessen -- ich habe
|
|
die Kiste geprueft und den Grund vergessen, aus dem es sie gibt.
|
|
Ein GIF, das man hineinlegen und herausnehmen kann, aber nicht
|
|
verschicken, waere eine perfekte Kiste ohne Zweck. */
|
|
const geschickt = await seite.evaluate(async () => {
|
|
const knopf = document.querySelector("#gif-tafel .gif-kachel__knopf");
|
|
if (!knopf) return { keinKnopf: true };
|
|
const vorher = document.querySelectorAll(".nachricht, .chat-nachricht, [data-nachricht]").length;
|
|
knopf.click();
|
|
await new Promise((r) => setTimeout(r, 1500));
|
|
/* NACH EINEM BILD AUSSERHALB DER TAFEL FRAGEN -- nicht nach
|
|
einer bestimmten Adresse.
|
|
|
|
Mein erster Versuch suchte nach "/chat/gifs/<n>/bild". Das
|
|
GIF wird beim Verschicken aber KOPIERT und haengt danach als
|
|
Anhang an der Nachricht (anhang_datei, anhang_art "bild") --
|
|
mit der Adresse der Anhaenge, nicht der Kiste. Wieder eine
|
|
Messung, die eine Annahme prueft statt der Sache.
|
|
|
|
Die Sache ist: Steht im Verlauf ein Bild, das vorher nicht da
|
|
war? Das gilt unabhaengig davon, welchen Weg es nimmt. */
|
|
const bilder = [...document.querySelectorAll("img")]
|
|
.filter((i) => !i.closest("#gif-tafel"))
|
|
.filter((i) => (i.src || "").includes("/workspace/api/"));
|
|
return {
|
|
vorher,
|
|
nachher: document.querySelectorAll(".nachricht, .chat-nachricht, [data-nachricht]").length,
|
|
imVerlauf: bilder.length > 0,
|
|
adressen: bilder.slice(0, 2).map((i) => (i.src || "").replace(/^https?:\/\/[^/]+/, "")),
|
|
tafelZu: document.getElementById("gif-tafel")?.hidden === true,
|
|
};
|
|
});
|
|
console.log(" verschicken: " + JSON.stringify(geschickt));
|
|
ok(!geschickt.keinKnopf, "es gibt eine Kachel zum Anklicken");
|
|
ok(geschickt.imVerlauf === true,
|
|
"das GIF steht danach wirklich im Verlauf");
|
|
ok(geschickt.tafelZu === true,
|
|
"und die Tafel geht zu -- man will sehen, wie es ankommt");
|
|
|
|
/* ---- Und wirklich herausnehmen ---- */
|
|
const raus = await seite.evaluate(async (id) => {
|
|
const a = await fetch("/workspace/api/chat/gifs/" + id,
|
|
{ method: "DELETE", credentials: "same-origin" });
|
|
const b = await fetch("/workspace/api/chat/gifs", { credentials: "same-origin" });
|
|
const j = b.ok ? await b.json() : {};
|
|
return { code: a.status, uebrig: (j.gifs || []).length };
|
|
}, erstes?.id);
|
|
ok(raus.code === 200, `es laesst sich herausnehmen (${raus.code})`);
|
|
ok(raus.uebrig === 0, `und ist wirklich weg (${raus.uebrig} uebrig)`);
|
|
|
|
/* ---- 4. Wer es NICHT darf ---- */
|
|
await kontext.close();
|
|
const k2 = await browser.newContext({ viewport: { width: 1200, height: 900 } });
|
|
const s2 = await k2.newPage();
|
|
await s2.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
|
/* KEIN NOTNAGEL AUF IRGENDEINE KACHEL (01.10.2026). Hier stand
|
|
`|| await s2.$(".rolle")`. Er greift heute nie -- die
|
|
Creator-Kachel steht auf der Agenturseite. Dieselbe Zeile hat in
|
|
pruef-glocke drei Rollen unpruefbar gemacht: Seit die Kachel
|
|
bindend ist, wird eine falsch geklickte Anmeldung zu Recht
|
|
abgewiesen, und heraus kam eine Zeitsperre, die wie ein Fehler
|
|
am Produkt aussah. */
|
|
const kachel = await s2.$('.rolle[data-rolle="creator"]');
|
|
if (!kachel) throw new Error('Kachel "creator" steht nicht auf der Anmeldeseite');
|
|
if (kachel) await kachel.click();
|
|
await s2.fill("#code", "CODE-CREA-0001");
|
|
await s2.click("button[type=submit]");
|
|
await s2.waitForURL("**/start.html", { timeout: 15000 });
|
|
await s2.goto(BASIS + "/workspace/chat.html", { waitUntil: "networkidle" });
|
|
await s2.waitForTimeout(1600);
|
|
const alsCreator = await s2.evaluate(async (b64) => {
|
|
const k = document.getElementById("gif-auf");
|
|
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
const a = await fetch("/workspace/api/chat/gifs", {
|
|
method: "POST", credentials: "same-origin",
|
|
headers: { "Content-Type": "application/octet-stream" },
|
|
body: bytes,
|
|
});
|
|
return { knopf: k ? !k.hidden : false, code: a.status };
|
|
}, GIF_B64);
|
|
console.log(" als Creator: " + JSON.stringify(alsCreator));
|
|
ok(alsCreator.knopf === false, "ein Creator sieht den Knopf nicht");
|
|
ok(alsCreator.code >= 400, `und kommt auch an der Route nicht durch (${alsCreator.code})`);
|
|
await k2.close();
|
|
|
|
ok(konsole.length === 0, konsole.length ? konsole[0] : "keine Abstuerze");
|
|
} finally {
|
|
await browser.close();
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
}
|
|
|
|
console.log("");
|
|
console.log(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
|
console.log(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
|
|
process.exit(fehler ? 1 : 0);
|