DER BEFUND
Im naechtlichen Lauf standen dreizehn Pruefungen mit derselben Zeile
rot:
[uncaughtException] Error: spawnSync openssl ENOENT
Kein Programmfehler. Die Pruefungen brauchen einen https-Vorbau (der
Sitzungskeks des Hauses ist `secure` und kommt ueber http nicht an),
und dafuer erzeugen sie ein Wegwerf-Zertifikat mit openssl.
GEMESSEN, NICHT VERMUTET
openssl liegt hier: C:\Program Files\Git\mingw64\bin\openssl.exe
C:\Program Files\Git\usr\bin\openssl.exe
im System-PATH steht: C:\Program Files\Git\cmd (nur git.exe)
im Benutzer-PATH: nichts mit Git
Wer aus Git Bash startet, erbt die mingw-Pfade und merkt nie etwas.
Die Aufgabenplanung startet `node.exe` DIREKT, ohne Shell -- und
bekommt sie nicht. Bei mir gruen, nachts rot, und der Grund steht
nicht im Code.
Das Schlimmste daran ist nicht der Ausfall: Dreizehn dauerhaft rote
Zeilen in einer Notiz, die Filipe morgens liest, gewoehnen einem das
Hinsehen ab -- und decken dabei die echten Befunde zu.
DIE REPARATUR
`server/helfer-openssl.mjs` sucht openssl erst im PATH (ohne `where`
oder `which` -- beides sind selbst Programme und koennen genauso
fehlen), danach an den Orten, an denen es auf einem Windows-Rechner
mit Git wirklich liegt.
Dazu `zertifikatBauen(schluessel, zertifikat, host)`. Die acht
Aufrufformen im Haus waren identisch bis auf die Variablennamen
(schl/zert, schluesselDatei/zertDatei, zKey/zCrt, sk/zt, key/crt) --
eine Stelle traegt alle neunundzwanzig. Zwei Tage zuvor war eine
davon um ein `-addext` aermer als die anderen; gemerkt hat es
niemand, weil der Browser den fehlenden Alternativnamen erst bei
einer Weiterleitung anmahnt. Eine Stelle kann nicht von sich selbst
abweichen.
KEIN EINTRAG IN DEN SYSTEM-PATH. `mingw64\bin` enthaelt rund hundert
Programme mit Unix-Namen (`find`, `sort`, `link`), die gleichnamige
Windows-Befehle verdecken. Das fuer eine Pruefung zu aendern haette
an ganz anderen Stellen Fehler verursacht, die niemand hierher
zurueckverfolgt.
GEGENPROBE IN BEIDE RICHTUNGEN, mit dem PATH des Nachtlaufs:
vorher (direkter Aufruf): ABSTURZ: spawnSync openssl ENOENT
nachher (ueber den Helfer): Zertifikat gebaut, 1236 Bytes
Und eine echte Pruefung unter denselben Bedingungen:
PATH ohne mingw64 -> pruef-chat-neu-stelle
63 Pruefungen, 0 Fehler, 0 Abstuerze
Genau diese Datei stand im Nachtlauf mit ENOENT rot.
EINE WACHE DAGEGEN
pruef-struktur prueft ab jetzt, dass niemand openssl wieder direkt
ruft -- der naechste merkt es dort und nicht erst in einem
Nachtlauf, den niemand liest. Drei Gegenproben.
Beim ersten Anlauf schlug sie auf ihre EIGENEN Probetexte an: Sie
liest alle Serverdateien, und dazu gehoert sie selbst. Die Texte
werden jetzt zusammengesetzt. Derselbe Selbsttreffer ist mir heute
schon zweimal passiert.
DER DRITTE AUSGANG BLEIBT, WO ER HINGEHOERT
`helfer-kachel-echtfarbe.mjs` faengt den Fehler weiterhin ab und
meldet `moeglich: false` mit Grund, statt abzubrechen. Eine
Sicherung abzuschaffen, weil ihr Anlass gerade behoben ist, ist der
Anfang des naechsten stillen Fehlschlags.
NEBENBEI: pruef-community-sicht
Sie war rot mit "ZU VIEL: reaktion.html" -- mein eigener Rueckstand
vom 28.09. Die Reaction steht als Kachel im Community-Bereich; die
Liste war nicht nachgezogen. Jetzt 10 / 0.
Die Liste bleibt bewusst von Hand gepflegt: Sie ist eine ABSICHT,
keine Ableitung. Aus rechte.js gelesen verglichen sich zwei Kopien
derselben Quelle -- immer gruen, nie ein Beweis.
GEPRUEFT
pruef-struktur ALLES IN ORDNUNG (mit der neuen Wache)
pruef-community-sicht 10 / 0
pruef-chat-neu-stelle 63 / 0 (mit dem PATH des Nachtlaufs)
pruef-notizen, -teamlage-karten, -werdegang: EXIT 0, keine Abstuerze
mess-quer EXIT 0
29 Dateien: node --check auf allen, kein direkter Aufruf mehr
257 lines
11 KiB
JavaScript
257 lines
11 KiB
JavaScript
/* =====================================================================
|
|
WIE SIEHT DER NOTIZBLOCK AUS? (26.09.2026)
|
|
|
|
Ein Messwerkzeug, keine Pruefung: Es meldet nichts, es macht
|
|
Bilder. Rechner und Handy, leeres Blatt und beschriebenes.
|
|
|
|
node server/mess-notizblock.mjs
|
|
|
|
Filipe: „für die modis, rechte und linke hand und dogfather eine
|
|
kachel hinzufügst, sie soll: Notizen, heißen. ich will dass du das
|
|
auch wie ein notizblock erstellst."
|
|
|
|
---------------------------------------------------------------------
|
|
DREI DINGE WERDEN GEPRÜFT, UND ZWEI DAVON HAT NIEMAND BESTELLT
|
|
|
|
1. WER DARF ÜBERHAUPT. Genau die vier Rollen, und niemand sonst —
|
|
auch nicht über die Schnittstelle, auch nicht von der anderen
|
|
Adresse aus. Die Rollenmenge wird dabei nicht abgeschrieben,
|
|
sondern aus dem Programm gelesen und gegen die Rechtetafel
|
|
gehalten: Zwei Listen für dieselbe Frage laufen auseinander.
|
|
|
|
2. NIEMAND SIEHT DIE NOTIZEN EINES ANDEREN. Auch DogFather nicht.
|
|
Das ist die Zusage, die auf der Kachel steht („nur du siehst
|
|
ihn"), und die einzige, deren Bruch man nicht bemerken würde:
|
|
Eine Notiz, die jemand Fremdes liest, sieht für den Schreiber
|
|
genauso aus wie eine, die niemand liest.
|
|
|
|
3. DIE DECKSCHICHT LIEGT AUF DEN PIXEL GENAU. Das Blatt besteht
|
|
aus einem unsichtbaren Schreibfeld und einer sichtbaren
|
|
Deckschicht darüber. Ändert eine Auszeichnung die BREITE eines
|
|
Zeichens (Fettdruck, andere Schriftgröße, Sperrung), brechen
|
|
beide Schichten an verschiedenen Stellen um — und der Cursor
|
|
steht neben dem Text. Das sieht aus wie ein Browserfehler und
|
|
wäre einer in notizen.css. Gemessen wird deshalb am echten
|
|
Umbruch, nicht an der Absicht.
|
|
|
|
---------------------------------------------------------------------
|
|
WARUM DIESE PRÜFUNG ÜBER https SPRICHT
|
|
|
|
Derselbe Grund wie bei pruef-handy-teamdogi: `dogfather-universe.com`
|
|
steht in der fest eingebauten HSTS-Liste von Chromium, und ohne die
|
|
Team-Adresse gibt es diese Seite gar nicht — auf einer Prüfadresse
|
|
(127.0.0.1) ist `haus` weder „crew" noch „agentur", und der Block
|
|
antwortet zu Recht mit 404.
|
|
|
|
Aufrufen mit: node server/pruef-notizen.mjs
|
|
===================================================================== */
|
|
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { request } from "node:http";
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-notiz-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
import { zertifikatBauen } from "./helfer-openssl.mjs";
|
|
|
|
/* Eine feste Nummer -- `eigenerPort` leitet seine aus der Stelle im
|
|
Alphabet der PRUEFdateien ab, und dieses Werkzeug ist keine.
|
|
Geprueft wird trotzdem, ob sie frei ist: Sonst misst man an einem
|
|
fremden Server und bekommt ein Bild von einem anderen Stand. */
|
|
const PORT = 5461;
|
|
{
|
|
const netz = await import("node:net");
|
|
const frei = await new Promise((fertig) => {
|
|
const s = netz.createServer();
|
|
s.once("error", () => fertig(false));
|
|
s.once("listening", () => s.close(() => fertig(true)));
|
|
s.listen(PORT, "127.0.0.1");
|
|
});
|
|
if (!frei) { console.error(`Port ${PORT} ist belegt. Abgebrochen.`); process.exit(3); }
|
|
}
|
|
|
|
process.env.PORT = `${PORT}`;
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
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 }); };
|
|
await import("./index.js");
|
|
notbremse(200_000, "mess-notizblock");
|
|
await new Promise((r) => setTimeout(r, 900));
|
|
|
|
let fehler = 0, geprueft = 0;
|
|
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
const melde = (t) => console.log(t);
|
|
process.on("exit", () => { try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } });
|
|
|
|
const CREW = "crew.dogfather-universe.com";
|
|
const AGENTUR = "workspace.dogfather-universe.com";
|
|
|
|
/* ---------- Zugänge anlegen -------------------------------------------- */
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const nun = new Date().toISOString();
|
|
const kennSchluessel = randomBytes(32).toString("hex");
|
|
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
|
|
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
|
.run("code_kennung_schluessel", kennSchluessel, nun);
|
|
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", kennSchluessel).update(code).digest("hex"), nun);
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
}
|
|
const ROLLEN = [["admin", "Filipe"], ["hand", "VanVan"], ["linke", "Funny"],
|
|
["modi", "Miss"], ["gast", "Zuschauerin"], ["creator", "Luna"]];
|
|
const CODES = {};
|
|
const IDS = {};
|
|
for (const [i, [rolle, name]] of ROLLEN.entries()) {
|
|
CODES[rolle] = `CODE-N${String(i).padStart(2, "0")}-000001`;
|
|
IDS[rolle] = anlegen(name, rolle, CODES[rolle]);
|
|
}
|
|
d.close();
|
|
|
|
/* ---------- Sprechen mit Hausadresse ------------------------------------
|
|
`fetch` kann die Host-Kopfzeile nicht setzen (verbotene Kopfzeile),
|
|
und an ihr entscheidet das Haus alles. Deshalb node:http. */
|
|
function roh(host, pfad, { method = "GET", keks = "", rumpf = null, origin } = {}) {
|
|
return new Promise((fertig, schief) => {
|
|
const daten = rumpf === null ? null : Buffer.from(JSON.stringify(rumpf));
|
|
const a = request({
|
|
host: "127.0.0.1", port: PORT, path: pfad, method,
|
|
headers: {
|
|
Host: host,
|
|
Origin: origin === undefined ? `https://${host}` : origin,
|
|
...(keks ? { Cookie: keks } : {}),
|
|
...(daten ? { "Content-Type": "application/json", "Content-Length": daten.length } : {}),
|
|
},
|
|
}, (antwort) => {
|
|
const teile = [];
|
|
antwort.on("data", (s) => teile.push(s));
|
|
antwort.on("end", () => {
|
|
const text = Buffer.concat(teile).toString("utf8");
|
|
let j = null; try { j = JSON.parse(text); } catch { /* kein JSON */ }
|
|
fertig({ code: antwort.statusCode, text, j, kopf: antwort.headers });
|
|
});
|
|
});
|
|
a.on("error", schief);
|
|
if (daten) a.write(daten);
|
|
a.end();
|
|
});
|
|
}
|
|
|
|
async function anmelden(rolle, host) {
|
|
const a = await roh(host, "/workspace/api/anmelden", {
|
|
method: "POST", rumpf: { rolle, code: CODES[rolle] },
|
|
});
|
|
const keks = []
|
|
.concat(a.kopf["set-cookie"] || [])
|
|
.map((z) => z.split(";")[0]).join("; ");
|
|
return a.code === 200 && keks ? keks : null;
|
|
}
|
|
|
|
const kekse = { modi: await anmelden("modi", CREW) };
|
|
if (!kekse.modi) { console.error("Anmeldung ging nicht"); process.exit(1); }
|
|
|
|
const PROBE = [
|
|
"# Sendung am Freitag",
|
|
"",
|
|
"[x] Gaesteliste an VanVan",
|
|
"[ ] Technik durchgehen -- Ton am Mischpult",
|
|
"[ ] Ansage schreiben",
|
|
"",
|
|
"- Idee: Zuschauerfragen in der letzten halben Stunde",
|
|
"- Idee: Gewinnspiel, aber ohne Punkte",
|
|
"",
|
|
"> Nicht vergessen: Casper braucht um 19 Uhr seine Runde.",
|
|
"",
|
|
"Unterlagen: https://crew.dogfather-universe.com/workspace/dateien.html",
|
|
].join(String.fromCharCode(10));
|
|
|
|
const pw = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const { execFileSync } = await import("node:child_process");
|
|
const { createServer: httpsServer } = await import("node:https");
|
|
const { request: httpAnfrage } = await import("node:http");
|
|
const schluesselDatei = join(ordner, "m.key");
|
|
const zertDatei = join(ordner, "m.crt");
|
|
zertifikatBauen(schluesselDatei, zertDatei, CREW);
|
|
const HTTPS_PORT = PORT + 1;
|
|
const vorbau = httpsServer({ key: readFileSync(schluesselDatei), cert: readFileSync(zertDatei) },
|
|
(von, nach) => {
|
|
const weiter = httpAnfrage({ host: "127.0.0.1", port: PORT, path: von.url,
|
|
method: von.method, headers: { ...von.headers, host: `${CREW}:${HTTPS_PORT}` } },
|
|
(antwort) => { nach.writeHead(antwort.statusCode || 502, antwort.headers); antwort.pipe(nach); });
|
|
weiter.on("error", () => { try { nach.writeHead(502); nach.end(); } catch { /* egal */ } });
|
|
von.pipe(weiter);
|
|
});
|
|
await new Promise((r) => vorbau.listen(HTTPS_PORT, "127.0.0.1", r));
|
|
const BASIS = `https://${CREW}:${HTTPS_PORT}`;
|
|
|
|
/* Drei Blaetter, damit der Stapel etwas zeigt. */
|
|
const BR = String.fromCharCode(10);
|
|
await roh(CREW, "/workspace/api/notizblock", { method: "POST", keks: kekse.modi,
|
|
rumpf: { text: ["Einkauf fuers Studio", "[ ] Kabel", "[ ] Kaffee"].join(BR) } });
|
|
await roh(CREW, "/workspace/api/notizblock", { method: "POST", keks: kekse.modi,
|
|
rumpf: { text: ["Woerter, die ich nicht mehr sagen will", "- eigentlich", "- quasi"].join(BR),
|
|
farbe: "rosa" } });
|
|
const haupt = await roh(CREW, "/workspace/api/notizblock", { method: "POST", keks: kekse.modi,
|
|
rumpf: { text: PROBE, farbe: "sonne" } });
|
|
await roh(CREW, `/workspace/api/notizblock/${haupt.j.notiz.id}`, { method: "PATCH",
|
|
keks: kekse.modi, rumpf: { angeheftet: true } });
|
|
|
|
const browser = await pw.chromium.launch({
|
|
args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1`, "--ignore-certificate-errors"] });
|
|
const kontext = await browser.newContext({ viewport: { width: 1440, height: 980 },
|
|
ignoreHTTPSErrors: true, deviceScaleFactor: 2 });
|
|
await kontext.addCookies(kekse.modi.split("; ").filter(Boolean).map((k) => {
|
|
const [n, ...r] = k.split("=");
|
|
return { name: n, value: r.join("="), domain: CREW, path: "/" };
|
|
}));
|
|
|
|
const seite = await kontext.newPage();
|
|
await seite.goto(`${BASIS}/workspace/notizen.html`, { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1200);
|
|
await seite.screenshot({ path: "server/mess-notizblock-rechner.png" });
|
|
console.log("server/mess-notizblock-rechner.png");
|
|
|
|
/* EIN EIGENER KONTEXT MIT FINGER.
|
|
`setViewportSize` aendert nur die Groesse -- `hasTouch` gehoert
|
|
zum KONTEXT und laesst sich danach nicht mehr setzen. Ohne ihn
|
|
meldet der Browser `pointer: fine`, und keine einzige Regel aus
|
|
`@media (pointer: coarse)` greift; dort stehen im ganzen Haus
|
|
die 44-Pixel-Beruehrziele. Ein Handy-Bild mit Mauszeiger zeigt
|
|
eine Seite, die auf keinem Handy so aussieht. */
|
|
const handyKontext = await browser.newContext({
|
|
viewport: { width: 390, height: 844 },
|
|
hasTouch: true,
|
|
ignoreHTTPSErrors: true,
|
|
deviceScaleFactor: 2,
|
|
});
|
|
await handyKontext.addCookies(kekse.modi.split("; ").filter(Boolean).map((k) => {
|
|
const [n, ...r] = k.split("=");
|
|
return { name: n, value: r.join("="), domain: CREW, path: "/" };
|
|
}));
|
|
const handy = await handyKontext.newPage();
|
|
await handy.goto(`${BASIS}/workspace/notizen.html`, { waitUntil: "networkidle" });
|
|
await handy.waitForTimeout(900);
|
|
await handy.screenshot({ path: "server/mess-notizblock-handy-liste.png" });
|
|
await handy.locator(".n-zettel").first().click();
|
|
await handy.waitForTimeout(600);
|
|
await handy.screenshot({ path: "server/mess-notizblock-handy-blatt.png" });
|
|
console.log("server/mess-notizblock-handy-liste.png");
|
|
console.log("server/mess-notizblock-handy-blatt.png");
|
|
|
|
await browser.close();
|
|
await new Promise((r) => vorbau.close(r));
|
|
process.exit(0);
|