GEMESSEN, BEVOR ICH ETWAS ANGEFASST HABE:
204 pruef-Dateien, abgeleitete Ports 5000-5409 (dicht belegt)
24 mess-Dateien, Nummern von Hand: 4471 bis 5493
davon IM Pruefbereich: 5387, 5397, 5397, 5399, 5399, 5401, 5403, 5405
untereinander doppelt: 5397, 5399, 5461, 5483, 5491
Aufgefallen ist es, weil mess-buehne und mess-reaktion beide auf 5483
lagen und ein haengengebliebener Lauf gestern einen ganzen Messlauf
gekostet hat.
Beim Umbau kam das Groessere heraus: EINUNDZWANZIG der 24 Messdateien
hatten nicht nur eine Nummer von Hand, sondern ueberhaupt keinen
Waechter -- schlicht `const PORT = 5397;`. Liegt dort schon ein
Server, startet der eigene still nicht, und gemessen wird ab da ein
fremder Stand. Genau der Fehler, gegen den helfer-port.mjs gebaut
wurde; die Messdateien standen die ganze Zeit ausserhalb.
WAS JETZT GILT
Zwei Sorten, zwei Bereiche, beide abgeleitet aus der Stelle im
Alphabet -- jede Sorte unter ihresgleichen, sonst verschoebe eine
neue Pruefung die Nummern aller Messungen. Pruefungen ab 5000,
Messungen ab MESS_BASIS = 5900.
5900 und nicht 5500: dazwischen bleibt Platz fuer 245 weitere
Pruefdateien (bei 5500 waeren es 45). Nach oben 5948 + AUSWEICHEN
4000 = 9948, also unter 10080, der naechsten gesperrten Nummer.
Nachgerechnet, nicht geschaetzt -- eine geschaetzte 4500 hatte bei
BASIS schon einmal danebengelegen.
Eine Wache dazu: Waechst der Pruefbereich bis an MESS_BASIS heran,
bricht die Ableitung ab und sagt, was zu tun ist. Eine stille
Ueberschneidung waere genau der Fehler, den das hier beseitigt.
Die zweite Nummer kommt ueber nr=1, nie ueber `PORT + 1`:
portNummer ueberspringt gesperrte Nummern, deshalb kann die naechste
Zahl die Nummer der naechsten DATEI sein, sobald einmal eine Sperre
dazwischenliegt. Heute liegt dort keine -- das ist Glueck, kein
Entwurf.
NEBENBEI GEFUNDEN UND MIT REPARIERT
Sechs bild-*.mjs riefen den Waechter und warfen seine Antwort weg:
await portMussFreiSein(4315, "das Bildwerkzeug");
process.env.PORT = "4315";
const BASIS = "http://127.0.0.1:4315";
Er lief, meldete nichts und wirkte nicht. Gibt das System den Port
dauerhaft nicht her, weicht er auf Port + 4000 aus und GIBT DIE NEUE
NUMMER ZURUECK -- diese Werkzeuge hoerten danach trotzdem auf der
alten und stuerzten mit `listen EACCES` ab, also mit genau dem
Fehler, gegen den er gebaut wurde. Dazu stand die Zahl dreimal je
Datei. Jetzt einmal, und die Antwort wird benutzt.
tiktok-videos.mjs hatte den Waechter ABGESCHRIEBEN -- eine kurze
eigene Fassung ohne den dritten Ausgang: Bei EACCES meldete sie
"belegt" und brach ab, statt auszuweichen. Auf diesem Rechner ist
genau das am 23.09. eingetreten (Port 5040, Windows-Dienst).
mess-fokus und mess-notizblock hatten dieselbe Abschrift. Eine
abgeschriebene Sicherung ist dieselbe Falle wie eine abgeschriebene
Liste.
GEPRUEFT
pruef-portnummern 15 -> 41 Pruefungen, 0 Fehler
pruef-ports 8 -> 10 Pruefungen, 456 statt 408 Ports geprobt
node --check auf allen 33 geaenderten Dateien
Gegenproben, die wirklich rot werden:
- eine Messdatei auf eine feste Nummer zurueckgesetzt -> 2 FEHL,
danach wieder 41/0
- die Wache: in einem Wegwerf-Ordner mit 452 pruef-Dateien bricht
eigenerPort ab statt still zu ueberlappen; eine Datei knapp
darunter bekommt weiter ihre Nummer (5846)
- die Erkennungen fuer feste Nummern, PORT + 1 und weggeworfene
Waechterantworten je gegen einen gebauten Rueckschritt
Am echten Verhalten gemessen:
- mess-chat-liste und mess-alle-einzelsicht (beide vorher 5397)
GLEICHZEITIG gestartet: 5906 und 5900, beide exit=0. Vorher war
das unmoeglich.
- die 48 neuen Nummern 5900-5947 auf diesem Rechner durchprobiert:
keine belegt, keine vom System gesperrt
- bild-chat.mjs durchgelaufen, drei Bilder, exit=0
Kein Eingriff am laufenden Dienst: helfer-port.mjs wird von index.js
und workspace.js nicht geladen (nachgesehen), nur von Pruef- und
Messwerkzeugen. Beide Haeuser unberuehrt -- es wird keine Zeile
angefasst, die eine Seite ausliefert.
Co-Authored-By: Claude Opus 5 <[email protected]>
256 lines
12 KiB
JavaScript
256 lines
12 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";
|
|
|
|
/* DIE NUMMER KOMMT AUS DEM ALPHABET (01.10.2026). Hier stand: „eine
|
|
feste Nummer -- `eigenerPort` leitet seine aus der Stelle im
|
|
Alphabet der PRUEFdateien ab, und dieses Werkzeug ist keine." Das
|
|
stimmte, war aber die falsche Schlussfolgerung: Statt den Waechter
|
|
zu meiden, leiten die Messdateien jetzt aus IHRER eigenen Liste ab,
|
|
in einem eigenen Bereich ab 5900. Die abgeschriebene Kurzfassung,
|
|
die hier stand, kannte den dritten Ausgang nicht (EACCES) und sagte
|
|
auch nicht, wie man nachsieht, wer den Port haelt. */
|
|
import { eigenerPort } from "./helfer-port.mjs";
|
|
const PORT = await eigenerPort(import.meta);
|
|
|
|
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);
|
|
/* NICHT `PORT + 1`: `portNummer` ueberspringt gesperrte Nummern,
|
|
deshalb kann die naechste Zahl die NUMMER DER NAECHSTEN DATEI sein,
|
|
sobald einmal eine Sperre dazwischenliegt. Heute liegt keine dort --
|
|
das ist Glueck, kein Entwurf. Die zweite Nummer gibt es als nr=1. */
|
|
const HTTPS_PORT = await eigenerPort(import.meta, "mess-notizblock (HTTPS)", 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);
|