Filipe: „fuer die modis, rechte und linke hand und dogfather eine
kachel hinzufuegst, sie soll: Notizen, heissen. ich will dass du das
auch wie ein notizblock erstellst. ich will dass es uebelst geil und
einzigartig ist."
=================================================================
TEIL 1: DIE FARBE -- UND WAS DABEI AUFFIEL
=================================================================
Fuer die neue Kachel braucht es einen Ton. Beim Suchen fiel auf, dass
tools/kachelton-entzerren.mjs zwoelf der 45 Farben fuer FREI hielt.
Sie sind es nicht. bereicheFuer() gibt fuer die fuenf Rollen des
Agenturhauses null zurueck -- das heisst „nimm die Liste aus dem
Browser", und die steht in workspace/assets/js/bereiche.js. Dort
stehen die Toene 1 bis 22 und 44: Dashboard, Zahlen, Team-Lage,
Scout-Pipeline. Kacheln, die jeden Tag jemand ansieht.
pruef-kachelfarben hatte denselben blinden Fleck. Sie meldete seit
Wochen „33 benutzte Toene" und war gruen. Was sie dadurch NICHT sah:
* Der Farblauf von gestern Nacht hat zwoelf dieser Kacheln
verschoben und drei davon unter die Buntheitsgrenze gedrueckt.
Gruen geblieben.
* Ton 7, 16 und 22 lagen SEIT JEHER unter der Grenze (0,112 / 0,116
/ 0,068). Nie gemeldet.
Das ist die Sorte gruener Haken, vor der die Hausregel warnt: Er sagt
nur, dass die Bedingung erfuellt war -- nicht, dass sie das Richtige
angesehen hat.
BEHOBEN, und zwar an der Wurzel: Die REGELN (Kontrast, Buntheit)
gelten jetzt fuer jeden Ton, der irgendwo an einer Kachel steht --
gelesen aus denselben fuenf Dateien, die auch pruef-kachel-universum
liest. Dazu die Namen der Kacheln, damit „Ton 1 traegt Dashboard"
ueberhaupt pruefbar ist.
DIE PALETTE WURDE NEU GERECHNET, mit dem dritten Verfahren an einem
Tag -- die ersten zwei stehen als Fehler im Kopf des Werkzeugs:
1. Alle 45 neu verteilt. Lief ueber zwei ausdrueckliche Wuensche
hinweg (#ff1a1a, #a8d8ff).
2. Nur die Kollisionen, aber immer nur EINEN der beiden bewegt.
Ergebnis: Ton 1 sprang vom Tuerkis ins Altrosa, 0,197 weit.
3. BEIDE duerfen sich bewegen, und zwar beide nur ein bisschen.
Zwei Toene, die 0,02 auseinanderstehen und 0,09 brauchen, teilen
sich das -- jeder rueckt 0,045, und beide bleiben, was sie waren.
kleinster Abstand 0,0154 -> 0,0905
zu blass 4 -> 0
sichtbar veraendert 7 Kacheln (ueber 0,05)
kaum zu sehen 28 (13 zwischen 0,02 und 0,05, 15 darunter)
EINE AUSNAHME MIT ZAHL, keine mit Achselzucken: Ton 1 (Dashboard)
bleibt acht Tausendstel unter der Buntheitsgrenze. Gemessen ueber ALLE
16,7 Millionen sRGB-Farben ist die naechste, die alle Regeln haelt,
#7a75c8 -- ein Blauviolett, 0,113 entfernt. Tuerkis erreicht in sRGB
schlicht keine hoehere Buntheit, und das schmale Band teilen sich
schon sieben Toene. Eine Kachel, die ihre Farbfamilie behaelt, ist die
bessere Antwort auf „richtig geil und speziell" als eine, die acht
Tausendstel bunter ist und niemand wiedererkennt. blassBis sagt jetzt
bei jeder Ausnahme, WIE WEIT sie reicht -- vorher hiess blassErlaubt
schlicht „hier wird weggesehen".
DIE NEUE FARBE IST GRUEN UND WOLLTE BERNSTEIN SEIN. Gesucht war die
Farbe von Papier. Gemessen gibt es sie nicht mehr: Der beste Bernstein
im ganzen Farbraum haelt 0,0799 Abstand -- unter der Hausgrenze. Und
Platz schaffen hilft nicht: Setzt man ihn fest, muss ein warmer Ton
das Band verlassen (Ton 45 waere 0,212 weit ins Magenta gewandert).
Die groesste wirklich freie Luecke liegt im Gruen, bei 0,0917. Ein
linierter Block in Gruen ist Papier, seit es Papier gibt.
=================================================================
TEIL 2: DER BLOCK
=================================================================
DREI ENTSCHEIDUNGEN, AUS DENEN DER REST FOLGT:
1. ES GIBT KEINEN SPEICHERN-KNOPF. Nirgends. Wer einen Block
aufschlaegt und lostippt, drueckt hinterher nicht auf „sichern";
er klappt ihn zu. Gesichert wird 800 ms nach dem letzten
Tastendruck. Geht das nicht, bleibt der Text im Browser liegen und
wird nachgereicht -- und der Fuss sagt ehrlich „noch nicht
gesichert", statt einen Verlust zu melden, den man gerade nicht
verhindern kann.
2. DAS BLATT IST EIN SCHREIBFELD MIT EINER DECKSCHICHT. Das Feld
traegt Text und Cursor und ist unsichtbar; darueber zeichnet eine
zweite Schicht denselben Text noch einmal -- mit Kaestchen,
Ueberschriften, Strichen und Links.
DARAUS FOLGT EINE EISERNE REGEL, und sie steht dreimal im Code:
KEINE Auszeichnung darf die BREITE eines Zeichens aendern. Kein
Fettdruck, keine andere Groesse, keine Sperrung. Erlaubt sind nur
Farbe, Flaeche, Rahmen und Durchstreichen. Ein einziges
font-weight: 700 verschoebe den Umbruch, und ab der zweiten Zeile
stuende der sichtbare Text neben dem Cursor.
pruef-notizen misst das am echten Umbruch: eine Probe mit allen
Auszeichnungen und einer Zeile, die umbrechen MUSS. Sind beide
Schichten verschieden hoch, sitzt der Umbruch woanders. Mit
Gegenprobe -- Fettdruck auf der Deckschicht bricht die Deckung um
genau eine Zeile (30 px), und die Zeile wird rot.
3. DIE TASTATUR FUEHRT DIE LISTE FORT, NICHT EINE LEISTE. Ein
Spiegelstrich und Enter macht den naechsten Punkt, ein leeres
Kaestchen und Enter das naechste Kaestchen -- und ein GESETZTER
Haken wird dabei nicht mitgenommen, die naechste Aufgabe ist ja
noch nicht erledigt. Zweimal Enter beendet die Liste. Ein Klick
aufs Kaestchen hakt ab. Es gibt keine Werkzeugleiste, weil man beim
Schreiben nie den Stift wechselt.
UND: NIEMAND SIEHT DIE NOTIZEN EINES ANDEREN. Auch DogFather nicht.
Es gibt in workspace-notizen.js keinen siehtAlles()-Zweig, keinen
Umschalter, keine fremde Liste -- jede Abfrage hat person_id = ? fest
eingebaut. Die Kachel steht deshalb in „Fuer dich" und nicht in
„Taeglich": Die Gruppe sagt die wichtigste Eigenschaft, bevor man sie
anfasst.
NUR AUF DER TEAM-ADRESSE. Auf der Agenturadresse ist die Seite ein
404 -- auch fuer DogFather. Das ist dieselbe Trennung wie bei Chat,
Kalender und Aufgaben, und sie steht an zwei Stellen: in
GEHOERT_ZU_ADRESSE (die Datei) und in der Schnittstelle (die Daten).
Die Seite ist nur HTML; die Daten sind die Sache.
Dazu: sechs Papierfarben, Anheften, Suche mit Hervorhebung im Text,
Papierkorb mit dreissig Tagen und einem Eintrag in der
Aufbewahrungsliste (ohne den waere die Frist ein Satz in einem
Kommentar -- genau das ist dem Support am 24.09. passiert).
=================================================================
EIN FUND BEIM BAUEN, DER ALLEN GEHOERT
=================================================================
DELETE /workspace/api/notizen/:id gab es schon -- in
workspace-aufgaben.js, fuer die Notizen AN einer Aufgabe. Weil jener
Router frueher eingehaengt ist, hat er jeden Loeschversuch des Blocks
abgefangen und mit 404 beantwortet.
Von aussen sah das aus wie ein Fehler im neuen Modul: Anlegen ging,
Aendern ging, Loeschen nicht. Man sucht dann im eigenen Code, und dort
ist nichts. Express meldet so etwas nicht -- es nimmt die erste Route,
die passt, und schweigt ueber die zweite.
Der Block liegt jetzt unter /workspace/api/notizblock. Und
pruef-notizen geht seitdem den Routenbaum von express durch und meldet
jedes Paar aus Methode und Pfad, das zweimal vergeben ist. Gemessen:
307 Wege, keine Dublette. Die Pruefung gilt fuers ganze Haus, auch
wenn sie in dieser Datei steht -- hier ist sie gefunden worden.
Nebenbei berichtigt: pruef-crew-adresse verlangte von jedem Eintrag
der Haustafel HTTP 200 mit Inhalt. Das stimmte, solange dort nur
oeffentliche Dateien standen. notizen.html ist die erste Seite hinter
der Anmeldung -- sie antwortet mit 302, und das ist richtig. Gefragt
wird jetzt, was die Tafel wirklich meint: GIBT es das hier.
GEMESSEN, alles nach den Aenderungen:
pruef-notizen 76 Punkte, 0 Fehler (neu)
pruef-kachelfarben 31 Punkte, 0 Fehler (vorher 26)
pruef-kachel-universum 13 Punkte, 0 Fehler
pruef-crew-adresse 157 Punkte, 0 Fehler
pruef-buehne 230 Punkte, 0 Fehler -- notizen.html neu in
der Liste, schlechtester Kontrast 6,73:1,
also 50 % ueber der Grenze
pruef-rechtetafel, -haus-seiten, -struktur, -css-klassen,
-jeder-hat-eine-seite, -workspace-seiten, -kachelraster,
-rueckmeldung alle 0 Fehler
Co-Authored-By: Claude Opus 5 <[email protected]>
691 lines
31 KiB
JavaScript
691 lines
31 KiB
JavaScript
/* =====================================================================
|
|
DER NOTIZBLOCK (26.09.2026)
|
|
|
|
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";
|
|
const { eigenerPort } = await import("./helfer-port.mjs");
|
|
const PORT = await eigenerPort(import.meta, "die Notizblockpruefung");
|
|
|
|
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(260_000, "pruef-notizen");
|
|
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;
|
|
}
|
|
|
|
/* =======================================================================
|
|
1. WER DARF — und woher die Liste kommt
|
|
======================================================================= */
|
|
melde("=== 1. Wer einen Block hat ===");
|
|
|
|
const { ROLLEN_MIT_BLOCK } = await import("./workspace-notizen.js");
|
|
const { SEITEN } = await import("./rechte.js");
|
|
{
|
|
const ausTafel = [...(SEITEN["/workspace/notizen.html"] || [])].sort();
|
|
const ausProgramm = [...ROLLEN_MIT_BLOCK].sort();
|
|
ok(ausTafel.length === 4,
|
|
`die Rechtetafel nennt vier Rollen (${ausTafel.join(", ") || "keine"})`);
|
|
/* BEIDE SEITEN GEGENEINANDER. Die Tafel bewacht die Seite, die
|
|
Menge im Modul die Daten. Wer nur eine pflegt, baut entweder eine
|
|
Tuer ohne Raum oder einen Raum ohne Tuer -- und beides faellt
|
|
niemandem auf. */
|
|
ok(JSON.stringify(ausTafel) === JSON.stringify(ausProgramm),
|
|
`Tafel und Schnittstelle nennen dieselben Rollen (${ausProgramm.join(", ")})`,
|
|
);
|
|
ok(ausTafel.includes("modi") && ausTafel.includes("hand")
|
|
&& ausTafel.includes("linke") && ausTafel.includes("admin"),
|
|
"und es sind genau die vier aus dem Auftrag");
|
|
}
|
|
|
|
const kekse = {};
|
|
for (const [rolle] of ROLLEN) kekse[rolle] = await anmelden(rolle, CREW);
|
|
ok(["admin", "hand", "linke", "modi"].every((r) => kekse[r]),
|
|
"die vier melden sich auf der Team-Adresse an");
|
|
|
|
melde("");
|
|
melde("=== 2. Und wer nicht ===");
|
|
for (const rolle of ["gast", "creator"]) {
|
|
if (!kekse[rolle]) { ok(true, `${rolle}: kommt auf dieser Adresse gar nicht herein`); continue; }
|
|
const a = await roh(CREW, "/workspace/api/notizblock", { keks: kekse[rolle] });
|
|
ok(a.code === 404, `${rolle}: bekommt 404 an der Schnittstelle (${a.code})`);
|
|
}
|
|
{
|
|
/* DIE ANDERE ADRESSE. DogFather arbeitet in beiden Haeusern -- und
|
|
genau deshalb ist er hier der richtige Pruefstein: Bei ihm
|
|
scheitert es nicht an der Rolle, sondern an der Adresse. */
|
|
const keksAgentur = await anmelden("admin", AGENTUR);
|
|
ok(!!keksAgentur, "DogFather meldet sich auch auf der Agenturadresse an");
|
|
const a = await roh(AGENTUR, "/workspace/api/notizblock", { keks: keksAgentur });
|
|
ok(a.code === 404, `und bekommt dort 404 auf seinen Block (${a.code})`);
|
|
const s = await roh(AGENTUR, "/workspace/notizen.html", { keks: keksAgentur });
|
|
ok(s.code === 404, `auch die Seite selbst gibt es dort nicht (${s.code})`);
|
|
const s2 = await roh(CREW, "/workspace/notizen.html", { keks: kekse.admin });
|
|
ok(s2.code === 200, `auf der Team-Adresse schon (${s2.code})`);
|
|
}
|
|
{
|
|
const ohne = await roh(CREW, "/workspace/api/notizblock");
|
|
ok(ohne.code === 401, `ohne Anmeldung: 401 (${ohne.code})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
3. SCHREIBEN, LESEN, ÄNDERN
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 3. Ein Blatt ===");
|
|
let blattId = null;
|
|
{
|
|
const leer = await roh(CREW, "/workspace/api/notizblock", { keks: kekse.modi });
|
|
ok(leer.code === 200 && Array.isArray(leer.j?.notizen) && leer.j.notizen.length === 0,
|
|
`am Anfang ist der Block leer (${leer.j?.notizen?.length})`);
|
|
ok(Array.isArray(leer.j?.farben) && leer.j.farben.length >= 4,
|
|
`die Papierfarben kommen vom Server (${leer.j?.farben?.length})`);
|
|
|
|
const neu = await roh(CREW, "/workspace/api/notizblock", {
|
|
method: "POST", keks: kekse.modi, rumpf: { text: "Erste Zeile\nZweite Zeile" },
|
|
});
|
|
blattId = neu.j?.notiz?.id;
|
|
ok(neu.code === 201 && blattId > 0, `ein Blatt anlegen geht (${neu.code})`);
|
|
ok(neu.j?.notiz?.titel === "Erste Zeile",
|
|
`der Titel wird aus der ersten Zeile abgeleitet ("${neu.j?.notiz?.titel}")`);
|
|
|
|
const aend = await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
|
method: "PATCH", keks: kekse.modi, rumpf: { text: "# Einkauf\n[ ] Milch\n[x] Brot" },
|
|
});
|
|
ok(aend.code === 200 && typeof aend.j?.geaendert === "string",
|
|
`aendern geht und gibt den Zeitstempel zurueck (${aend.code})`);
|
|
|
|
const nach = await roh(CREW, "/workspace/api/notizblock", { keks: kekse.modi });
|
|
ok(nach.j?.notizen?.[0]?.text?.includes("[x] Brot"), "und der Text steht drin");
|
|
ok(nach.j?.notizen?.[0]?.titel === "Einkauf",
|
|
`die Raute zaehlt nicht zum Titel ("${nach.j?.notizen?.[0]?.titel}")`);
|
|
}
|
|
{
|
|
const falsch = await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
|
method: "PATCH", keks: kekse.modi, rumpf: { farbe: "knallorange" },
|
|
});
|
|
ok(falsch.code === 400, `eine erfundene Papierfarbe wird abgelehnt (${falsch.code})`);
|
|
const echt = await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
|
method: "PATCH", keks: kekse.modi, rumpf: { farbe: "rosa" },
|
|
});
|
|
ok(echt.code === 200, `eine echte nicht (${echt.code})`);
|
|
}
|
|
{
|
|
const lang = "x".repeat(30000);
|
|
const a = await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
|
method: "PATCH", keks: kekse.modi, rumpf: { text: lang },
|
|
});
|
|
const nach = await roh(CREW, "/workspace/api/notizblock", { keks: kekse.modi });
|
|
const drin = nach.j?.notizen?.find((n) => n.id === blattId)?.text || "";
|
|
/* ABGESCHNITTEN UND NICHT ABGELEHNT. Wer zwanzigtausend Zeichen
|
|
getippt hat, soll nicht alles verlieren, weil er hundert zu viel
|
|
hatte -- er soll merken, dass hier Schluss ist. */
|
|
ok(a.code === 200 && drin.length === nach.j.text_max,
|
|
`zu langer Text wird gekuerzt statt abgelehnt (${drin.length} von ${nach.j?.text_max})`);
|
|
await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
|
method: "PATCH", keks: kekse.modi, rumpf: { text: "# Einkauf\n[ ] Milch\n[x] Brot" },
|
|
});
|
|
}
|
|
{
|
|
const fremd = await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
|
method: "PATCH", keks: kekse.modi, rumpf: { text: "x" }, origin: "https://boese.example",
|
|
});
|
|
ok(fremd.code === 403, `eine Anfrage von einer fremden Seite wird abgewiesen (${fremd.code})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
4. NIEMAND SIEHT DIE NOTIZEN EINES ANDEREN
|
|
|
|
Die Zusage steht auf der Kachel. Sie ist auch die einzige, deren
|
|
Bruch der Schreiber nicht bemerken würde.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 4. Niemand sonst ===");
|
|
for (const rolle of ["admin", "hand", "linke"]) {
|
|
const liste = await roh(CREW, "/workspace/api/notizblock", { keks: kekse[rolle] });
|
|
const fremdeDrin = (liste.j?.notizen || []).some((n) => n.id === blattId);
|
|
ok(liste.code === 200 && !fremdeDrin,
|
|
`${rolle}: sieht das Blatt des Modis nicht in seiner Liste`);
|
|
const holen = await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
|
method: "PATCH", keks: kekse[rolle], rumpf: { text: "hier war ich" },
|
|
});
|
|
ok(holen.code === 404, `${rolle}: kann es auch nicht aendern (${holen.code})`);
|
|
const weg = await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
|
method: "DELETE", keks: kekse[rolle],
|
|
});
|
|
ok(weg.code === 404, `${rolle}: und nicht wegwerfen (${weg.code})`);
|
|
}
|
|
{
|
|
/* GEGENPROBE. Ohne sie wuerde die Messung oben auch dann gruen, wenn
|
|
das Blatt gar nicht existiert. */
|
|
const eigen = await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
|
method: "PATCH", keks: kekse.modi, rumpf: { angeheftet: true },
|
|
});
|
|
ok(eigen.code === 200, `Gegenprobe: dem Modi selbst gelingt genau dasselbe (${eigen.code})`);
|
|
const nach = await roh(CREW, "/workspace/api/notizblock", { keks: kekse.modi });
|
|
ok(nach.j?.notizen?.[0]?.angeheftet === true, "und das Anheften kommt an");
|
|
}
|
|
|
|
/* =======================================================================
|
|
5. DER PAPIERKORB
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 5. Der Papierkorb ===");
|
|
{
|
|
const weg = await roh(CREW, `/workspace/api/notizblock/${blattId}`, {
|
|
method: "DELETE", keks: kekse.modi,
|
|
});
|
|
ok(weg.code === 200, `wegtun geht (${weg.code})`);
|
|
const block = await roh(CREW, "/workspace/api/notizblock", { keks: kekse.modi });
|
|
ok(!(block.j?.notizen || []).some((n) => n.id === blattId), "und es ist vom Block verschwunden");
|
|
ok(block.j?.papierkorb === 1, `der Papierkorb zaehlt es (${block.j?.papierkorb})`);
|
|
|
|
const korb = await roh(CREW, "/workspace/api/notizblock/papierkorb", { keks: kekse.modi });
|
|
const drin = (korb.j?.notizen || []).find((n) => n.id === blattId);
|
|
ok(!!drin, "es liegt im Papierkorb");
|
|
ok(typeof drin?.endgueltig_am === "string"
|
|
&& Date.parse(drin.endgueltig_am) > Date.now(),
|
|
`und es steht dabei, wann es von selbst geht (${drin?.endgueltig_am?.slice(0, 10)})`);
|
|
|
|
const zurueck = await roh(CREW, `/workspace/api/notizblock/${blattId}/zurueck`, {
|
|
method: "POST", keks: kekse.modi,
|
|
});
|
|
ok(zurueck.code === 200, `zurueckholen geht (${zurueck.code})`);
|
|
const block2 = await roh(CREW, "/workspace/api/notizblock", { keks: kekse.modi });
|
|
ok((block2.j?.notizen || []).some((n) => n.id === blattId), "und es ist wieder da");
|
|
}
|
|
{
|
|
/* ENDGUELTIG NUR AUS DEM PAPIERKORB. Vom Block aus gibt es diesen
|
|
Weg nicht -- wer danebentippt, soll nicht alles verlieren. */
|
|
const zuFrueh = await roh(CREW, `/workspace/api/notizblock/${blattId}/endgueltig`, {
|
|
method: "DELETE", keks: kekse.modi,
|
|
});
|
|
ok(zuFrueh.code === 404,
|
|
`endgueltig loeschen geht vom Block aus NICHT (${zuFrueh.code})`);
|
|
await roh(CREW, `/workspace/api/notizblock/${blattId}`, { method: "DELETE", keks: kekse.modi });
|
|
const jetzt = await roh(CREW, `/workspace/api/notizblock/${blattId}/endgueltig`, {
|
|
method: "DELETE", keks: kekse.modi,
|
|
});
|
|
ok(jetzt.code === 200, `aus dem Papierkorb schon (${jetzt.code})`);
|
|
const korb = await roh(CREW, "/workspace/api/notizblock/papierkorb", { keks: kekse.modi });
|
|
ok((korb.j?.notizen || []).length === 0, "und dann ist es wirklich weg");
|
|
}
|
|
|
|
/* =======================================================================
|
|
6. DAS AUFRÄUMEN HÄLT SEINE FRIST
|
|
|
|
Eine Frist ohne Werkzeug ist ein Satz in einem Kommentar. Geprüft
|
|
wird deshalb an echten Daten, in beide Richtungen.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 6. Dreissig Tage ===");
|
|
{
|
|
const { notizenAufraeumen } = await import("./workspace-notizen.js");
|
|
const { PAPIERKORB_TAGE } = await import("./notiz-tabellen.js");
|
|
const { db } = await import("./workspace.js");
|
|
const alt = new Date(Date.now() - (PAPIERKORB_TAGE + 2) * 86400000).toISOString();
|
|
const frisch = new Date(Date.now() - 86400000).toISOString();
|
|
const einfuegen = (weg) => db().prepare(
|
|
"INSERT INTO notizen (person_id, text, farbe, angeheftet, erstellt, geaendert, weg_am)"
|
|
+ " VALUES (?,?,?,0,?,?,?) RETURNING id")
|
|
.get(IDS.modi, "Probe", "block", nun, nun, weg).id;
|
|
const idAlt = einfuegen(alt);
|
|
const idNeu = einfuegen(frisch);
|
|
const idOffen = db().prepare(
|
|
"INSERT INTO notizen (person_id, text, farbe, angeheftet, erstellt, geaendert)"
|
|
+ " VALUES (?,?,?,0,?,?) RETURNING id").get(IDS.modi, "Auf dem Block", "block", nun, nun).id;
|
|
|
|
const weg = notizenAufraeumen();
|
|
const da = (id) => !!db().prepare("SELECT 1 FROM notizen WHERE id = ?").get(id);
|
|
ok(weg === 1, `genau eines wird geraeumt (${weg})`);
|
|
ok(!da(idAlt), `das alte ist weg (nach ${PAPIERKORB_TAGE} Tagen)`);
|
|
ok(da(idNeu), "das von gestern liegt noch da");
|
|
/* DIE WICHTIGERE HAELFTE: Was auf dem Block liegt, wird NIE
|
|
geraeumt. Ohne diese Zeile waere ein Aufraeumer, der alles
|
|
loescht, hier gruen. */
|
|
ok(da(idOffen), "und was gar nicht weggelegt wurde, bleibt unberuehrt");
|
|
db().prepare("DELETE FROM notizen WHERE id IN (?,?)").run(idNeu, idOffen);
|
|
}
|
|
{
|
|
/* Steht die Frist auch in der Aufbewahrungsliste? Sie ist die
|
|
einzige Stelle im Haus, die wirklich raeumt -- am 24.09. war
|
|
genau dieser Eintrag beim Support vergessen worden, und die
|
|
Meldungen waeren fuer immer liegen geblieben. */
|
|
const { FRISTEN } = await import("./workspace-aufbewahrung.js");
|
|
const eintrag = (FRISTEN || []).find((e) => e.schluessel === "notizen_papierkorb");
|
|
ok(!!eintrag, "der Papierkorb steht in der Aufbewahrungsliste");
|
|
ok(typeof eintrag?.raeumen === "function",
|
|
"und hat ein Werkzeug, das ihn wirklich raeumt");
|
|
}
|
|
|
|
/* =======================================================================
|
|
7. DAS BLATT AUF DEM BILDSCHIRM
|
|
|
|
Jetzt der Teil, den keine Schnittstelle beantworten kann.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 7. Das Blatt auf dem Bildschirm ===");
|
|
|
|
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, "pruef.key");
|
|
const zertDatei = join(ordner, "pruef.crt");
|
|
execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes",
|
|
"-keyout", schluesselDatei, "-out", zertDatei, "-days", "2",
|
|
"-subj", `/CN=${CREW}`, "-addext", `subjectAltName=DNS:${CREW}`],
|
|
{ stdio: "ignore" });
|
|
|
|
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}`;
|
|
|
|
const pw = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
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: 900 }, ignoreHTTPSErrors: true,
|
|
});
|
|
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();
|
|
const konsole = [];
|
|
seite.on("pageerror", (f) => konsole.push(String(f?.message || f)));
|
|
seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); });
|
|
|
|
await seite.goto(`${BASIS}/workspace/notizen.html`, { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(700);
|
|
|
|
ok(await seite.locator("#block").count() === 1, "die Seite kommt an");
|
|
|
|
/* Ein Blatt anlegen und Text hineinschreiben, der alle Auszeichnungen
|
|
enthaelt -- und eine SEHR lange Zeile, die umbrechen MUSS. Genau an
|
|
ihr faellt eine Breitenaenderung auf: Brechen die zwei Schichten an
|
|
verschiedenen Stellen um, sind sie verschieden hoch. */
|
|
const PROBE = [
|
|
"# Eine Ueberschrift",
|
|
"[ ] Offener Punkt",
|
|
"[x] Erledigter Punkt",
|
|
"- Ein Aufzaehlungspunkt",
|
|
"> Ein Zitat",
|
|
"Ein Link: https://example.org/pfad",
|
|
"Eine sehr lange Zeile, die auf jeden Fall umbrechen muss, damit man sieht, "
|
|
+ "ob beide Schichten an derselben Stelle umbrechen, und deshalb ist sie so "
|
|
+ "lang wie sie ist, mit vielen Woertern hintereinander ohne Punkt und Komma "
|
|
+ "damit der Umbruch wirklich mitten im Satz passiert und nicht am Ende.",
|
|
].join("\n");
|
|
|
|
await seite.click("#neu");
|
|
await seite.waitForTimeout(400);
|
|
await seite.fill("#feld", PROBE);
|
|
await seite.waitForTimeout(400);
|
|
|
|
{
|
|
const m = await seite.evaluate(() => {
|
|
const f = document.getElementById("feld");
|
|
const dk = document.getElementById("deck");
|
|
const cf = getComputedStyle(f), cd = getComputedStyle(dk);
|
|
const gleich = (n) => cf[n] === cd[n];
|
|
return {
|
|
hoeheFeld: f.scrollHeight, hoeheDeck: dk.scrollHeight,
|
|
breiteFeld: f.clientWidth, breiteDeck: dk.clientWidth,
|
|
schriftGleich: ["fontFamily", "fontSize", "fontWeight", "fontStyle",
|
|
"letterSpacing", "wordSpacing", "lineHeight", "whiteSpace",
|
|
"overflowWrap", "tabSize", "paddingLeft", "paddingRight",
|
|
"paddingTop"].filter((n) => !gleich(n)),
|
|
/* Und dasselbe fuer JEDES ausgezeichnete Stueck im Deck: Wenn
|
|
eine Auszeichnung die Schrift veraendert, steht es hier. */
|
|
stuecke: [...dk.querySelectorAll("span, a")].map((s) => {
|
|
const c = getComputedStyle(s);
|
|
return ["fontFamily", "fontSize", "fontWeight", "fontStyle",
|
|
"letterSpacing", "wordSpacing"]
|
|
.filter((n) => c[n] !== cf[n])
|
|
.map((n) => `${s.className || s.tagName}.${n}=${c[n]}`);
|
|
}).flat(),
|
|
haken: dk.querySelectorAll(".n-hk").length,
|
|
hakenAn: dk.querySelectorAll('.n-hk[data-an="ja"]').length,
|
|
links: dk.querySelectorAll("a.n-link").length,
|
|
ueber: dk.querySelectorAll(".n-h").length,
|
|
text: dk.textContent,
|
|
};
|
|
});
|
|
|
|
ok(m.schriftGleich.length === 0,
|
|
`beide Schichten schreiben gleich (${m.schriftGleich.join(", ") || "alles gleich"})`);
|
|
ok(m.stuecke.length === 0,
|
|
`keine Auszeichnung aendert die Schrift (${m.stuecke.slice(0, 3).join(", ") || "keine"})`);
|
|
ok(m.breiteFeld === m.breiteDeck,
|
|
`beide sind gleich breit (${m.breiteFeld} / ${m.breiteDeck})`);
|
|
/* DIE EIGENTLICHE MESSUNG. Zwei Bildpunkte Spielraum fuers Runden;
|
|
ein verschobener Umbruch kostet eine ganze Zeile (30 px). */
|
|
ok(Math.abs(m.hoeheFeld - m.hoeheDeck) <= 2,
|
|
`und gleich hoch -- der Umbruch sitzt an derselben Stelle `
|
|
+ `(${m.hoeheFeld} / ${m.hoeheDeck})`);
|
|
ok(m.text.replace(/\s+$/, "") === PROBE,
|
|
"die Deckschicht zeigt genau den Text, Zeichen fuer Zeichen");
|
|
ok(m.haken === 2 && m.hakenAn === 1,
|
|
`zwei Kaestchen, eines angehakt (${m.haken}/${m.hakenAn})`);
|
|
ok(m.links === 1, `die Adresse ist anklickbar (${m.links})`);
|
|
ok(m.ueber === 1, `die Ueberschrift ist ausgezeichnet (${m.ueber})`);
|
|
}
|
|
|
|
/* GEGENPROBE ZUR HOEHENMESSUNG. Ohne sie wuerde die Zeile oben auch
|
|
dann gruen, wenn die Messung gar nichts ansieht. Ein Fettdruck auf
|
|
der Deckschicht MUSS sie rot machen. */
|
|
{
|
|
const unterschied = await seite.evaluate(() => {
|
|
const f = document.getElementById("feld");
|
|
const dk = document.getElementById("deck");
|
|
const vorher = Math.abs(f.scrollHeight - dk.scrollHeight);
|
|
dk.style.fontWeight = "800";
|
|
dk.style.letterSpacing = "0.4px";
|
|
const nachher = Math.abs(f.scrollHeight - dk.scrollHeight);
|
|
dk.style.fontWeight = ""; dk.style.letterSpacing = "";
|
|
return { vorher, nachher };
|
|
});
|
|
ok(unterschied.nachher > 2,
|
|
`Gegenprobe: Fettdruck auf der Deckschicht bricht die Deckung `
|
|
+ `(${unterschied.vorher} -> ${unterschied.nachher} px Unterschied)`);
|
|
}
|
|
|
|
/* Ein Klick auf ein Kaestchen schaltet es um. */
|
|
{
|
|
const vorher = await seite.inputValue("#feld");
|
|
await seite.locator("#deck .n-hk").first().click();
|
|
await seite.waitForTimeout(250);
|
|
const nachher = await seite.inputValue("#feld");
|
|
ok(vorher.includes("[ ] Offener Punkt") && nachher.includes("[x] Offener Punkt"),
|
|
"ein Klick auf das Kaestchen setzt den Haken");
|
|
ok(nachher.length === vorher.length,
|
|
`und der Text bleibt zeichengenau gleich lang (${nachher.length}/${vorher.length})`);
|
|
await seite.locator("#deck .n-hk").first().click();
|
|
await seite.waitForTimeout(250);
|
|
ok((await seite.inputValue("#feld")).includes("[ ] Offener Punkt"),
|
|
"noch ein Klick nimmt ihn wieder weg");
|
|
}
|
|
|
|
/* Die Tastatur fuehrt die Liste fort. */
|
|
{
|
|
await seite.fill("#feld", "- Erster Punkt");
|
|
await seite.click("#feld");
|
|
await seite.keyboard.press("Control+End");
|
|
await seite.keyboard.press("Enter");
|
|
await seite.keyboard.type("Zweiter");
|
|
await seite.waitForTimeout(200);
|
|
const t = await seite.inputValue("#feld");
|
|
ok(t === "- Erster Punkt\n- Zweiter",
|
|
`Enter setzt die Aufzaehlung fort ("${t.replace(/\n/g, "\\n")}")`);
|
|
|
|
await seite.fill("#feld", "[x] Fertig");
|
|
await seite.click("#feld");
|
|
await seite.keyboard.press("Control+End");
|
|
await seite.keyboard.press("Enter");
|
|
await seite.keyboard.type("Noch offen");
|
|
await seite.waitForTimeout(200);
|
|
const t2 = await seite.inputValue("#feld");
|
|
/* DER HAKEN WIRD NICHT MITGENOMMEN -- die naechste Aufgabe ist noch
|
|
nicht erledigt. Das ist die Sorte Kleinigkeit, die den
|
|
Unterschied macht zwischen „funktioniert" und „denkt mit". */
|
|
ok(t2 === "[x] Fertig\n[ ] Noch offen",
|
|
`und uebernimmt einen gesetzten Haken NICHT ("${t2.replace(/\n/g, "\\n")}")`);
|
|
}
|
|
|
|
/* Es sichert sich von selbst -- gemessen am Neuladen, nicht an der
|
|
Anzeige. Eine Anzeige, die „gesichert" sagt, ist eine Behauptung. */
|
|
{
|
|
const wort = `Selbstgesichert-${Date.now()}`;
|
|
await seite.fill("#feld", wort);
|
|
await seite.waitForTimeout(1400);
|
|
const stand = (await seite.locator("#stand").textContent() || "").trim();
|
|
/* GEFRAGT WIRD DER SERVER, nicht die Seite nach dem Neuladen.
|
|
|
|
Der erste Anlauf lud neu und sah ins Schreibfeld -- und wurde
|
|
rot, obwohl alles richtig war: Die Seite schlaegt nach dem Laden
|
|
das OBERSTE Blatt auf, und das war ein anderes (ein angeheftetes
|
|
aus Abschnitt 4). Gemessen wurde also ein Blatt, in das nie
|
|
jemand getippt hat. Eine Messung, die das Falsche ansieht, ist
|
|
schlimmer als keine -- sie schickt einen los, einen Fehler zu
|
|
suchen, den es nicht gibt. */
|
|
const drueben = await roh(CREW, "/workspace/api/notizblock", { keks: kekse.modi });
|
|
const gefunden = (drueben.j?.notizen || []).some((n) => String(n.text).includes(wort));
|
|
ok(gefunden, `der Text ist ohne Knopfdruck beim Server angekommen `
|
|
+ `(Anzeige sagte "${stand}")`);
|
|
ok(stand === "gesichert", `und die Anzeige sagt es auch ("${stand}")`);
|
|
}
|
|
|
|
/* Und auf dem Handy: Das Blatt legt sich ueber die Liste, der
|
|
Zurueck-Knopf holt sie wieder. */
|
|
{
|
|
const handy = await kontext.newPage();
|
|
await handy.setViewportSize({ width: 390, height: 780 });
|
|
await handy.goto(`${BASIS}/workspace/notizen.html`, { waitUntil: "networkidle" });
|
|
await handy.waitForTimeout(700);
|
|
const listeDa = await handy.locator(".n-zettel").count();
|
|
ok(listeDa >= 1, `auf dem Handy steht zuerst der Stapel (${listeDa} Blaetter)`);
|
|
ok(await handy.locator("#blatt").isHidden(),
|
|
"und kein Blatt liegt davor -- sonst kaeme man nie an die Liste");
|
|
await handy.locator(".n-zettel").first().click();
|
|
await handy.waitForTimeout(400);
|
|
ok(await handy.locator("#blatt").isVisible(), "ein Tipp schlaegt das Blatt auf");
|
|
ok(await handy.locator("#zurueck").isVisible(),
|
|
"und der Zurueck-Knopf erscheint");
|
|
/* KEIN WAAGERECHTES SCHIEBEN. Die Hausgrenze ist die Breite des
|
|
Schirms; ein Blatt, das darueber hinausragt, macht die ganze
|
|
Seite schiebbar. */
|
|
const breit = await handy.evaluate(() =>
|
|
document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
|
ok(breit <= 1, `nichts ragt seitlich hinaus (${breit} px)`);
|
|
await handy.locator("#zurueck").click();
|
|
await handy.waitForTimeout(400);
|
|
ok(await handy.locator("#blatt").isHidden(), "der Zurueck-Knopf schliesst es wieder");
|
|
await handy.close();
|
|
}
|
|
|
|
/* =======================================================================
|
|
8. ZWEI WEGE MIT DEMSELBEN NAMEN -- EINE HAUSWEITE PRUEFUNG
|
|
|
|
Sie steht in dieser Datei, weil sie hier gefunden wurde, und sie
|
|
gilt fuer das ganze Haus.
|
|
|
|
BEIM BAUEN DIESES BLOCKS: Die Route DELETE /workspace/api/notizen/:id
|
|
gab es schon -- in workspace-aufgaben.js, fuer die Notizen AN einer
|
|
Aufgabe. Weil aufgabenRouter frueher eingehaengt wird, hat sie jeden
|
|
Loeschversuch des Blocks abgefangen und mit 404 beantwortet.
|
|
|
|
VON AUSSEN SAH DAS AUS WIE EIN FEHLER IM NEUEN MODUL: Anlegen ging,
|
|
Aendern ging, Loeschen nicht. Man sucht dann im eigenen Code, und
|
|
dort ist nichts. Express meldet so etwas nicht -- es nimmt die
|
|
erste Route, die passt, und schweigt ueber die zweite.
|
|
|
|
Geprueft wird der Routenbaum von express selbst, nicht eine Liste.
|
|
Findet sich ein Paar aus Methode und Pfad zweimal, steht hier, wo.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 8. Kein Weg heisst zweimal gleich ===");
|
|
{
|
|
const app = (await import("./index.js")).default;
|
|
const wege = new Map();
|
|
const doppelt = [];
|
|
const sammeln = (schicht, vorn = "") => {
|
|
if (schicht.route) {
|
|
const pfad = vorn + (schicht.route.path || "");
|
|
for (const m of Object.keys(schicht.route.methods || {})) {
|
|
if (m === "_all") continue;
|
|
const name = `${m.toUpperCase()} ${pfad}`;
|
|
if (wege.has(name)) doppelt.push(name); else wege.set(name, true);
|
|
}
|
|
} else if (schicht.handle?.stack) {
|
|
for (const s of schicht.handle.stack) sammeln(s, vorn);
|
|
}
|
|
};
|
|
const stapel = app?._router?.stack || app?.router?.stack || [];
|
|
for (const s of stapel) sammeln(s);
|
|
/* DIE ZAHL GEHOERT IN DIE BEDINGUNG. Faende diese Messung gar keine
|
|
Wege (weil express seinen Baum eines Tages anders ablegt), waere
|
|
„keine Dubletten" trivial wahr -- und bliebe es fuer immer. */
|
|
ok(wege.size > 200, `${wege.size} Wege im Baum gefunden`);
|
|
ok(doppelt.length === 0,
|
|
doppelt.length ? `doppelt vergeben: ${[...new Set(doppelt)].join(", ")}`
|
|
: "kein Paar aus Methode und Pfad kommt zweimal vor");
|
|
/* GEGENPROBE: Die Messung MUSS eine Dublette finden koennen. */
|
|
const probe = new Map(); const gefunden = [];
|
|
for (const n of ["GET /x", "GET /x"]) {
|
|
if (probe.has(n)) gefunden.push(n); else probe.set(n, true);
|
|
}
|
|
ok(gefunden.length === 1, "Gegenprobe: zwei gleiche Wege wuerden auffallen");
|
|
}
|
|
|
|
ok(konsole.length === 0, `keine Fehler in der Konsole (${konsole.slice(0, 2).join(" | ") || "keine"})`);
|
|
|
|
await browser.close();
|
|
await new Promise((r) => vorbau.close(r));
|
|
|
|
melde("");
|
|
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
|
melde(fehler ? "FEHLGESCHLAGEN" : "ALLES IN ORDNUNG");
|
|
process.exit(fehler ? 1 : 0);
|