Compare commits
2
Commits
c0d2896419
...
7a6749630c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a6749630c | ||
|
|
a9d920e7d0 |
@@ -0,0 +1,77 @@
|
||||
/* ERZEUGT von tools/chat-kacheln-rechnen.mjs --kreis.
|
||||
NICHT VON HAND AENDERN -- wer hier eine Farbe eintippt, umgeht
|
||||
genau die Kontrastrechnung, die die Lesbarkeit garantiert.
|
||||
|
||||
360 Farbtoene, Schritt 1 Grad, alle auf derselben Leuchtdichte
|
||||
wie die zwoelf Kacheln. Der Index IST der Winkel: KREIS[214] ist
|
||||
214 Grad. */
|
||||
export const KREIS = [
|
||||
"#a60156", "#a60054", "#a60252", "#a80051", "#a8004f", "#a7004c",
|
||||
"#a8014a", "#a80148", "#a90046", "#a90044", "#a90042", "#a90040",
|
||||
"#aa013e", "#a9023b", "#aa0039", "#aa0037", "#ab0035", "#ab0032",
|
||||
"#ab0130", "#ab012d", "#ab002a", "#ab0027", "#ab0124", "#ab0221",
|
||||
"#ac001e", "#ac011a", "#ac0015", "#ab0010", "#ad000a", "#ac0002",
|
||||
"#ab0b01", "#a91400", "#a51a00", "#a41f00", "#a02300", "#9f2600",
|
||||
"#9d2900", "#9b2b00", "#9b2e00", "#993000", "#973201", "#963400",
|
||||
"#933500", "#933700", "#923800", "#8f3901", "#8e3a00", "#8d3b00",
|
||||
"#8c3c00", "#8b3d01", "#893e00", "#883f01", "#874001", "#864102",
|
||||
"#844102", "#844100", "#834302", "#834300", "#814400", "#804402",
|
||||
"#7f4500", "#7e4501", "#7e4600", "#7d4702", "#7c4700", "#7c4800",
|
||||
"#7a4800", "#7a4900", "#784902", "#784900", "#784a00", "#754a01",
|
||||
"#754a00", "#744b03", "#734b01", "#734c01", "#724c00", "#724d00",
|
||||
"#704d02", "#6f4d01", "#6f4d01", "#6e4e00", "#6d4e00", "#6d4f00",
|
||||
"#6d4f00", "#6b4f03", "#6a5003", "#6a5003", "#685002", "#685002",
|
||||
"#675102", "#665102", "#665102", "#655202", "#645202", "#635202",
|
||||
"#635303", "#615303", "#625400", "#615400", "#5f5400", "#5e5400",
|
||||
"#5e5401", "#5d5502", "#5c5503", "#5c5600", "#5a5601", "#595602",
|
||||
"#585602", "#585700", "#575701", "#555701", "#555801", "#535800",
|
||||
"#525802", "#525900", "#505800", "#4f5903", "#4d5900", "#4d5a00",
|
||||
"#4b5a00", "#4a5a02", "#485a00", "#485b00", "#455b00", "#445c00",
|
||||
"#435c03", "#415c01", "#3f5c00", "#3d5d00", "#3b5e00", "#395d01",
|
||||
"#365e01", "#345e01", "#325e01", "#2f5f01", "#2b5f01", "#296002",
|
||||
"#246100", "#1f6100", "#1a6102", "#126200", "#086201", "#006205",
|
||||
"#006210", "#006216", "#00621c", "#016220", "#016123", "#006227",
|
||||
"#016129", "#00612c", "#00622f", "#036131", "#026133", "#026135",
|
||||
"#026137", "#036139", "#01603a", "#02603b", "#00613e", "#01603f",
|
||||
"#026040", "#046041", "#006042", "#036044", "#006045", "#036046",
|
||||
"#016048", "#036049", "#00604a", "#025f4a", "#005f4b", "#035f4c",
|
||||
"#015f4d", "#01604f", "#045f4f", "#025f50", "#015f51", "#045f52",
|
||||
"#025f52", "#015f53", "#005f55", "#045e55", "#035e56", "#005e56",
|
||||
"#005e57", "#005f58", "#015f5a", "#045e59", "#035e5a", "#025e5b",
|
||||
"#035e5c", "#025e5d", "#025e5d", "#015e5e", "#015e5f", "#015e5f",
|
||||
"#015e60", "#005d61", "#005d61", "#005d62", "#015d63", "#015d63",
|
||||
"#015d64", "#025d65", "#025d65", "#035d66", "#045d67", "#055d68",
|
||||
"#055c68", "#005d6a", "#005c6a", "#015c6a", "#025c6b", "#035c6b",
|
||||
"#065c6d", "#015c6e", "#025c6e", "#045c6f", "#005c71", "#015b71",
|
||||
"#025b71", "#005c73", "#015b73", "#035b74", "#005b76", "#025b76",
|
||||
"#055b77", "#025b78", "#005b7a", "#015a79", "#005b7c", "#025a7c",
|
||||
"#005a7d", "#045a7e", "#02597f", "#005980", "#005982", "#035982",
|
||||
"#035984", "#015885", "#015886", "#025888", "#02588a", "#01588d",
|
||||
"#00578e", "#02578f", "#005792", "#015693", "#005696", "#025597",
|
||||
"#00559c", "#00549e", "#0053a0", "#0053a4", "#0152a6", "#0151aa",
|
||||
"#0150af", "#004fb5", "#004dbc", "#004ac0", "#0047ca", "#0042d6",
|
||||
"#0035ee", "#0d27ff", "#1824ff", "#2024ff", "#2622ff", "#2c21fd",
|
||||
"#3120fe", "#351bff", "#3a17ff", "#3d17fd", "#4112ff", "#450efe",
|
||||
"#4808fe", "#4b05fe", "#4e00fe", "#5102fd", "#5402fb", "#5602f9",
|
||||
"#5801f5", "#5b02f6", "#5d02f4", "#5f02f2", "#6202f0", "#6300ee",
|
||||
"#6500ec", "#6701ea", "#6902e8", "#6b00e7", "#6d01e5", "#6e02e1",
|
||||
"#7001e0", "#7202de", "#7300db", "#7401d9", "#7700d9", "#7802d7",
|
||||
"#7900d4", "#7b00d3", "#7b01cf", "#7d00ce", "#7f01cc", "#7f01c9",
|
||||
"#8100c8", "#8301c6", "#8301c3", "#8500c2", "#8601bf", "#8701bc",
|
||||
"#8802ba", "#8900b9", "#8a01b6", "#8c01b5", "#8d00b2", "#8d00af",
|
||||
"#8e02ad", "#9001ac", "#9001a9", "#9200a8", "#9200a5", "#9200a2",
|
||||
"#9301a0", "#95019e", "#96019d", "#96019a", "#960097", "#980096",
|
||||
"#980093", "#980090", "#9a008f", "#9a028d", "#9a028a", "#9b0088",
|
||||
"#9b0086", "#9c0084", "#9e0083", "#9d0080", "#9f007f", "#9e007c",
|
||||
"#a0007b", "#a00178", "#a10177", "#a00074", "#a10072", "#a10170",
|
||||
"#a2016e", "#a3016d", "#a2006a", "#a40069", "#a30166", "#a30064",
|
||||
"#a50163", "#a40260", "#a5015e", "#a5005c", "#a6005b", "#a50058",
|
||||
];
|
||||
|
||||
/** Der Winkel zu einem Schluessel der Form "ton-214", sonst null. */
|
||||
export function winkelAus(wert) {
|
||||
const t = /^ton-(\d{1,3})$/.exec(String(wert ?? ""));
|
||||
if (!t) return null;
|
||||
const n = Number(t[1]);
|
||||
return n >= 0 && n < KREIS.length ? n : null;
|
||||
}
|
||||
+191
-1
@@ -232,7 +232,7 @@ console.log("\n=== 3. Der Ruf ans Rudel ===");
|
||||
});
|
||||
ok(!!liste, "ein @ oeffnet die Liste");
|
||||
ok(liste?.erster === "rudel", `der Ruf steht oben (${liste?.erster})`);
|
||||
ok(/alle im Team/i.test(liste?.zusatz || ""), `mit der Erklaerung daneben (${liste?.zusatz})`);
|
||||
ok(/alle in diesem Chat/i.test(liste?.zusatz || ""), `mit der Erklaerung daneben (${liste?.zusatz})`);
|
||||
ok((liste?.hoch || 0) >= 44 && liste?.imBild,
|
||||
`am Daumen treffbar und im Bild (${liste?.hoch} px)`);
|
||||
await frida.seite.keyboard.press("Enter");
|
||||
@@ -264,6 +264,196 @@ console.log("\n=== 3. Der Ruf ans Rudel ===");
|
||||
`gerufen wurden ${gemeint.length} -- die andere Modi und DogFather, nicht sie selbst`);
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
4. DER FARBRING (23.09.2026)
|
||||
|
||||
Filipe: "anstatt sowas um die farben auszusuchen, kannst du nicht
|
||||
so eine art diagramm machen wo man mit einem kreis herum gehen kann
|
||||
und sie die farbe ganz genau selber auswaehlen kann?"
|
||||
|
||||
WAS HIER WIRKLICH AUF DEM SPIEL STEHT, ist nicht die Optik. Die
|
||||
zwoelf Kacheln waren gerechnet, damit niemand sich unlesbar machen
|
||||
kann -- alle auf derselben Leuchtdichte. Ein Ring mit freier Wahl
|
||||
ist genau die Gelegenheit, dieses Versprechen zu verlieren, und
|
||||
zwar unbemerkt: Die Farbe sieht huebsch aus, und erst die anderen
|
||||
koennen den Text nicht mehr lesen.
|
||||
|
||||
Deshalb misst diese Probe zuerst den KONTRAST und dann die
|
||||
Bedienung.
|
||||
===================================================================== */
|
||||
{
|
||||
console.log("");
|
||||
console.log("=== 4. Der Farbring ===");
|
||||
|
||||
const { KREIS } = await import("./chat-kreis.js");
|
||||
|
||||
/* ---- 4a. Das Versprechen, an allen 360 Toenen ------------------ */
|
||||
const schriftFarben = await frida.seite.evaluate(() => {
|
||||
/* AM ECHTEN ELEMENT MESSEN, nicht an einem selbstgebauten.
|
||||
Erster Anlauf legte ein <div class="chat-nachricht__blase"> an
|
||||
und las dort -- und bekam zweimal nichts zurueck. Die beiden
|
||||
Werte stehen naemlich eine Ebene hoeher, auf .chat-nachricht,
|
||||
und werden von dort vererbt. Ergebnis war ein roter Haken an
|
||||
einer Stelle, an der nichts kaputt war: die Messung war falsch,
|
||||
nicht das Programm.
|
||||
|
||||
Erst die Blase im Verlauf; gibt es keine, ein Ersatzstueck mit
|
||||
der RICHTIGEN Klasse. */
|
||||
const echt = document.querySelector(".chat-nachricht");
|
||||
const ziel = echt || document.body.appendChild(
|
||||
Object.assign(document.createElement("div"), { className: "chat-nachricht" }));
|
||||
const s2 = getComputedStyle(ziel);
|
||||
const raus = { text: s2.getPropertyValue("--blase-text").trim(),
|
||||
leise: s2.getPropertyValue("--blase-leise").trim(),
|
||||
woher: echt ? "aus dem Verlauf" : "aus einem Ersatzstueck" };
|
||||
if (!echt) ziel.remove();
|
||||
return raus;
|
||||
});
|
||||
|
||||
const zuZahl = (f) => {
|
||||
const t = /^#?([0-9a-f]{6})$/i.exec(String(f).trim());
|
||||
if (t) return [0, 2, 4].map((i) => parseInt(t[1].slice(i, i + 2), 16));
|
||||
const r = /rgba?\(([^)]+)\)/i.exec(String(f));
|
||||
return r ? r[1].split(",").slice(0, 3).map((x) => Math.round(parseFloat(x))) : null;
|
||||
};
|
||||
const linear = (v) => (v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);
|
||||
const leucht = (rgb) => 0.2126 * linear(rgb[0] / 255) + 0.7152 * linear(rgb[1] / 255)
|
||||
+ 0.0722 * linear(rgb[2] / 255);
|
||||
const kontrast = (a, b) => {
|
||||
const x = leucht(a), y = leucht(b);
|
||||
return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05);
|
||||
};
|
||||
|
||||
const textRgb = zuZahl(schriftFarben.text);
|
||||
const leiseRgb = zuZahl(schriftFarben.leise);
|
||||
ok(!!textRgb && !!leiseRgb,
|
||||
`die Schriftfarben der Blase sind lesbar, ${schriftFarben.woher}`
|
||||
+ ` (${schriftFarben.text || "nichts"} / ${schriftFarben.leise || "nichts"})`);
|
||||
|
||||
if (textRgb && leiseRgb) {
|
||||
let schlimmster = Infinity, schlimmsterWinkel = -1, gemessen = 0;
|
||||
for (let w = 0; w < KREIS.length; w++) {
|
||||
const f = zuZahl(KREIS[w]);
|
||||
if (!f) continue;
|
||||
gemessen++;
|
||||
for (const [s3, noetig] of [[textRgb, 7], [leiseRgb, 4.8]]) {
|
||||
const v = kontrast(f, s3) / noetig;
|
||||
if (v < schlimmster) { schlimmster = v; schlimmsterWinkel = w; }
|
||||
}
|
||||
}
|
||||
/* DIE ANZAHL STEHT IN DER BEDINGUNG. Waere die Tabelle leer,
|
||||
bliebe schlimmster auf Infinity und die Zeile waere gruen,
|
||||
ohne eine einzige Farbe angesehen zu haben. */
|
||||
ok(gemessen === 360 && schlimmster >= 1,
|
||||
`alle ${gemessen} Toene des Rings tragen die Schrift der Blase`
|
||||
+ ` (knappster: Winkel ${schlimmsterWinkel}, Faktor ${schlimmster.toFixed(3)})`);
|
||||
}
|
||||
|
||||
/* ---- 4b. Steht der Ring da, und ist er zu treffen? ------------- */
|
||||
/* ERST ZURUECK ZUR LISTE -- und das ist keine Umstaendlichkeit,
|
||||
sondern der Weg, den ein Mensch am Handy geht. Der Farbknopf
|
||||
steht in der Spalte mit den Gespraechen, und die weicht auf
|
||||
einem schmalen Bildschirm zur Seite, sobald ein Gespraech offen
|
||||
ist. Der erste Anlauf dieser Probe hat genau hier 30 Sekunden
|
||||
lang auf einen Knopf gewartet, den es zwar gab, den aber niemand
|
||||
sehen konnte -- ein gutes Beispiel dafuer, dass "ist im HTML"
|
||||
und "ist erreichbar" zwei verschiedene Aussagen sind. */
|
||||
const zurueck = frida.seite.locator("#zur-liste");
|
||||
if (await zurueck.isVisible().catch(() => false)) {
|
||||
await zurueck.click();
|
||||
await frida.seite.waitForTimeout(600);
|
||||
}
|
||||
const knopfDa = await frida.seite.locator("#kachel-oeffnen").isVisible().catch(() => false);
|
||||
ok(knopfDa, "der Farbknopf ist am Handy erreichbar (ueber die Gespraechsliste)");
|
||||
if (!knopfDa) throw new Error("ohne den Knopf ist der Rest nicht messbar");
|
||||
await frida.seite.click("#kachel-oeffnen");
|
||||
await frida.seite.waitForTimeout(600);
|
||||
|
||||
const rad = await frida.seite.evaluate(() => {
|
||||
const r = document.getElementById("kachelrad-ring");
|
||||
if (!r) return null;
|
||||
const k = r.getBoundingClientRect();
|
||||
const g = getComputedStyle(r);
|
||||
return {
|
||||
breit: Math.round(k.width), hoch: Math.round(k.height),
|
||||
links: Math.round(k.left), oben: Math.round(k.top),
|
||||
imBild: k.left >= -1 && k.right <= innerWidth + 1
|
||||
&& k.top >= -1 && k.bottom <= innerHeight + 1,
|
||||
farben: (g.backgroundImage.match(/rgb\(/g) || []).length,
|
||||
fassen: g.touchAction,
|
||||
rolle: r.getAttribute("role"),
|
||||
};
|
||||
});
|
||||
ok(!!rad, "der Ring ist da");
|
||||
/* 180 px sind nicht geraten: Ein Ring, den man mit dem Daumen im
|
||||
Kreis fuehren soll, braucht sichtbar mehr als die 44 px einer
|
||||
Schaltflaeche -- sonst rutscht man beim Drehen heraus. */
|
||||
ok((rad?.breit || 0) >= 180 && rad?.imBild,
|
||||
`er passt auf den Bildschirm und ist gross genug (${rad?.breit}x${rad?.hoch} px)`);
|
||||
/* Der Verlauf muss aus VIELEN Farben bestehen. Waere die Liste
|
||||
nicht angekommen, stuende dort ein einzelner Ton -- und der Ring
|
||||
saehe aus wie eine Scheibe. */
|
||||
ok((rad?.farben || 0) >= 300, `er ist aus ${rad?.farben} Farben gemalt`);
|
||||
/* OHNE DAS SCHIEBT DAS HANDY DIE SEITE WEG, statt den Ring zu
|
||||
drehen. Genau diese Zeile ist es, die auf einem Beruehrgeraet
|
||||
ueber brauchbar und unbrauchbar entscheidet. */
|
||||
ok(rad?.fassen === "none", `Wischen dreht den Ring, statt zu scrollen (${rad?.fassen})`);
|
||||
ok(rad?.rolle === "slider", `und eine Sprachausgabe versteht ihn (${rad?.rolle})`);
|
||||
|
||||
/* ---- 4c. Drehen -- und kommt die richtige Farbe heraus? -------- */
|
||||
if (rad) {
|
||||
/* Winkel 90 heisst DREI UHR: der Verlauf beginnt oben und laeuft
|
||||
im Uhrzeigersinn. Waere die Rechnung im Browser um 90 Grad
|
||||
verdreht (der uebliche atan2-Fehler), kaeme hier eine andere
|
||||
Farbe heraus als die, die unter dem Finger liegt. */
|
||||
const mx = rad.links + rad.breit / 2;
|
||||
const my = rad.oben + rad.hoch / 2;
|
||||
const r80 = (rad.breit / 2) * 0.8;
|
||||
await frida.seite.mouse.move(mx, my - r80);
|
||||
await frida.seite.mouse.down();
|
||||
await frida.seite.mouse.move(mx + r80, my, { steps: 12 });
|
||||
await frida.seite.mouse.up();
|
||||
await frida.seite.waitForTimeout(900);
|
||||
|
||||
const stand = await frida.seite.evaluate(() => {
|
||||
const r = document.getElementById("kachelrad-ring");
|
||||
const m = document.getElementById("kachelrad-mitte");
|
||||
return { winkel: Number(r?.getAttribute("aria-valuenow")),
|
||||
wort: r?.getAttribute("aria-valuetext") || "",
|
||||
mitte: m ? getComputedStyle(m).backgroundColor : "" };
|
||||
});
|
||||
ok(Math.abs((stand.winkel || 0) - 90) <= 2,
|
||||
`nach dem Drehen auf drei Uhr steht der Ring bei ${stand.winkel} Grad (erwartet 90)`);
|
||||
|
||||
const erwartet = zuZahl(KREIS[stand.winkel] || "");
|
||||
const gezeigt = zuZahl(stand.mitte);
|
||||
ok(!!erwartet && !!gezeigt
|
||||
&& Math.abs(erwartet[0] - gezeigt[0]) <= 1
|
||||
&& Math.abs(erwartet[1] - gezeigt[1]) <= 1
|
||||
&& Math.abs(erwartet[2] - gezeigt[2]) <= 1,
|
||||
`die Mitte zeigt genau die Farbe dieses Winkels (${stand.mitte} gegen ${KREIS[stand.winkel]})`);
|
||||
ok(/\d+ Grad/.test(stand.wort) && stand.wort.length > 8,
|
||||
`und sie wird vorgelesen als "${stand.wort}"`);
|
||||
|
||||
/* ---- 4d. Wird es gespeichert? ------------------------------- */
|
||||
const db3 = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true });
|
||||
const gespeichert = db3.prepare("SELECT chat_kachel FROM personen WHERE id = ?")
|
||||
.get(idA)?.chat_kachel;
|
||||
db3.close();
|
||||
ok(gespeichert === "ton-" + stand.winkel,
|
||||
`und in der Datenbank steht die Wahl (${gespeichert})`);
|
||||
|
||||
/* ---- 4e. Und mit der Tastatur? ------------------------------ */
|
||||
await frida.seite.focus("#kachelrad-ring");
|
||||
await frida.seite.keyboard.press("ArrowRight");
|
||||
await frida.seite.waitForTimeout(200);
|
||||
const nachher = await frida.seite.evaluate(() =>
|
||||
Number(document.getElementById("kachelrad-ring")?.getAttribute("aria-valuenow")));
|
||||
ok(nachher === (stand.winkel + 1) % 360,
|
||||
`eine Pfeiltaste dreht um genau ein Grad weiter (${stand.winkel} -> ${nachher})`);
|
||||
}
|
||||
}
|
||||
|
||||
const alleMeldungen = [...frida.meckern, ...nele.meckern];
|
||||
console.log("");
|
||||
ok(alleMeldungen.length === 0,
|
||||
|
||||
@@ -327,7 +327,7 @@ console.log("=== Die Kacheln ===");
|
||||
Untertitel. Wer morgen eine Kachel "Dein Kopf" nennt und dasselbe
|
||||
verspricht, ist damit mitgeprueft; die Namensliste haette ihn
|
||||
durchgelassen. */
|
||||
const verspricht = (k) => /nur du|nur dich|siehst nur du/i.test(k.unter || "");
|
||||
const verspricht = (k) => /\bnur du\b|\bnur dich\b|siehst nur du/i.test(k.unter || "");
|
||||
const heikel = alleX.filter((k) => verspricht(k) && k.ziel === "entwicklung.html");
|
||||
ok(alleX.some(verspricht) && heikel.length === 0,
|
||||
`${wer}: keine Kachel verspricht Vertraulichkeit und fuehrt doch auf die`
|
||||
|
||||
@@ -989,10 +989,22 @@ melde("\n" + "=== 7. Der Ruf ans Rudel geht durch die Tür ===");
|
||||
|
||||
/* WER ERWARTET WIRD, WIRD ABGELEITET -- nicht abgeschrieben. Eine
|
||||
feste Liste hier waere beim naechsten Rollenzuschnitt still
|
||||
falsch, und die Pruefung haette den Fehler mitgemacht. */
|
||||
falsch, und die Pruefung haette den Fehler mitgemacht.
|
||||
|
||||
SEIT DEM 23.09.2026 IST DAS HIER DER GANZE RAUM. Filipe: "wenn wir
|
||||
da im chat @rudel machen will ich dass jeder in dem chat markiert
|
||||
wird. nicht nur team ... ich rede nur von dem chat wo die ganze
|
||||
community auch drin ist." Der Raum heisst "Das Rudel" -- wer drin
|
||||
sitzt, gehoert dazu, Gast oder nicht.
|
||||
|
||||
DIE ALTE REGEL IST DAMIT NICHT WEG, sie gilt nur woanders. Dass
|
||||
sie ueberall sonst weiterhin gilt, prueft die Gegenprobe unten in
|
||||
der Gruppe. Ohne die waere diese Zeile hier eine Erlaubnis statt
|
||||
einer Aussage: Haette jemand die Regel versehentlich im ganzen
|
||||
Haus umgestellt, bliebe dieser Block gruen. */
|
||||
const TEAM = new Set(["admin", "hand", "linke", "modi"]);
|
||||
const erwartet = besetzung
|
||||
.filter((x) => TEAM.has(x.rolle) && x.id !== idModi)
|
||||
.filter((x) => x.id !== idModi)
|
||||
.map((x) => x.id).sort((x, y) => x - y);
|
||||
|
||||
/* ---- 1. DER MODI RUFT ------------------------------------------ */
|
||||
@@ -1007,17 +1019,49 @@ melde("\n" + "=== 7. Der Ruf ans Rudel geht durch die Tür ===");
|
||||
"SELECT person_id FROM chat_erwaehnungen WHERE nachricht_id = ? ORDER BY person_id")
|
||||
.all(nrId).map((x) => x.person_id).sort((x, y) => x - y);
|
||||
ok(gemeint.length > 0 && JSON.stringify(gemeint) === JSON.stringify(erwartet),
|
||||
`gerufen wird genau das Team im Raum (${JSON.stringify(gemeint)}, erwartet ${JSON.stringify(erwartet)})`);
|
||||
/* DIE EIGENTLICHE AUSSAGE. "Rudel" heisst das Team -- nicht jeder im
|
||||
Raum. Im Treff sitzt die Community; ein Ruf, der dort jeden
|
||||
Zuschauer weckt, waere etwas anderes als der bestellte. */
|
||||
ok(!gemeint.includes(idGast),
|
||||
" der Gast im selben Raum aber NICHT — das Rudel ist das Team, nicht der Raum");
|
||||
`gerufen wird jeder im Raum (${JSON.stringify(gemeint)}, erwartet ${JSON.stringify(erwartet)})`);
|
||||
/* DIE EIGENTLICHE AUSSAGE, und sie hat sich am 23.09.2026 umgedreht.
|
||||
Der Gast ist KEIN Sonderfall mehr, sondern der Beweis: In diesem
|
||||
einen Raum meint der Ruf alle Anwesenden. Waere er weiterhin
|
||||
draussen, waere die Bestellung nicht ausgefuehrt -- und zwar
|
||||
unsichtbar, denn der Rufer sieht nicht, wen er erreicht hat. */
|
||||
ok(gemeint.includes(idGast),
|
||||
" der Gast im selben Raum AUCH — hier ist das Rudel der Raum, nicht nur das Team");
|
||||
ok(!gemeint.includes(idModi),
|
||||
" und der Rufer selbst auch nicht — man ruft nicht sich");
|
||||
ok(gemeint.includes(idLinke),
|
||||
" die linke Hand ist dabei — sie gehoert zum Team");
|
||||
|
||||
/* ---- GEGENPROBE: UEBERALL SONST GILT WEITER DAS TEAM -----------
|
||||
"sonst nichts anfassen" ist eine Zusage, und eine Zusage ohne
|
||||
Messung ist eine Hoffnung. Die Testgruppe besteht aus DogFather
|
||||
und zwei Scouts; Scouts sind nicht Team Dogi. Ein Ruf dort darf
|
||||
deshalb NIEMANDEN erreichen.
|
||||
|
||||
WARUM DAS TRENNSCHARF IST: Haette jemand die neue Regel im ganzen
|
||||
Haus eingeschaltet, waeren hier die beiden Scouts markiert. Die
|
||||
Pruefung kann also "nicht in Ordnung" sagen -- sonst waere sie
|
||||
nur eine Verzierung.
|
||||
|
||||
ZWEI HAEUSER, EIN PORT: Die Testgruppe wurde ueber `ruf` angelegt
|
||||
(Agentur-Haus, ohne Host-Kopf), der Treff-Block darueber misst
|
||||
ueber `crewRuf` (crew., mit Host-Kopf). Hier gehoert deshalb
|
||||
`ruf` hin -- mit dem crew-Keks in einem Agentur-Raum haette die
|
||||
Gegenprobe vermutlich nur bewiesen, dass eine Anmeldung fehlt. */
|
||||
const rufGruppe = await ruf(`/workspace/api/chat/raeume/${raumId}/nachrichten`,
|
||||
aDogi.keks, "POST", { text: "@rudel gilt hier nicht" });
|
||||
if (rufGruppe.code !== 201) {
|
||||
ok(false, `Gegenprobe: DogFather kann in der Gruppe schreiben (${rufGruppe.code} ${rufGruppe.text.slice(0, 80)})`);
|
||||
} else {
|
||||
const inGruppe = drin.prepare(
|
||||
"SELECT person_id FROM chat_erwaehnungen WHERE nachricht_id = ?")
|
||||
.all(json(rufGruppe)?.nachricht?.id).map((x) => x.person_id);
|
||||
ok(inGruppe.length === 0,
|
||||
`Gegenprobe: in einer Gruppe ohne Team ruft @rudel niemanden (${inGruppe.length})`);
|
||||
ok(!inGruppe.includes(idVan) && !inGruppe.includes(idFunny),
|
||||
" die beiden Scouts bleiben unberuehrt — die neue Regel gilt NUR im Rudel-Raum");
|
||||
}
|
||||
|
||||
/* Die Spalte bleibt stehen -- sie ist es, aus der beim Lesen die
|
||||
Hervorhebung entsteht. */
|
||||
const gespeichert = drin.prepare(
|
||||
@@ -1028,6 +1072,12 @@ melde("\n" + "=== 7. Der Ruf ans Rudel geht durch die Tür ===");
|
||||
const geladen = (verlauf.json?.nachrichten || []).find((x) => x.id === nrId);
|
||||
ok(geladen?.rudel === true, "der Verlauf bringt es mit — auch nach dem Neuladen");
|
||||
ok(verlauf.json?.darf_rudel === true, "und der Modi bekommt gesagt, dass er rufen darf");
|
||||
/* UND WEN ER DAMIT MEINT. Ohne diese Auskunft stuende im
|
||||
Auswahlfeld weiter "alle im Team" -- im Rudel-Raum schlicht
|
||||
falsch, und zwar nach unten: Man drueckt leichter, wenn man
|
||||
weniger Leute zu wecken glaubt. */
|
||||
ok(verlauf.json?.rudel_wen === "raum",
|
||||
`und wen er meint, steht dabei (${verlauf.json?.rudel_wen})`);
|
||||
ok(verlauf.json?.rudel_wort === RUDEL_WORT,
|
||||
`das Wort kommt vom Server, nicht aus dem Browser (${verlauf.json?.rudel_wort})`);
|
||||
|
||||
|
||||
Binary file not shown.
@@ -159,7 +159,7 @@ for (const s of seiten) {
|
||||
einem reinen Formular-Dialog hier als „totes Gewicht" gemeldet --
|
||||
und jemand naehme die Datei heraus, die dort gerade arbeitet. */
|
||||
const hatDialogMitFeld = [...html.matchAll(/<dialog[^>]*>[\s\S]*?<\/dialog>/g)]
|
||||
.some((m) => /<(input|textarea|select)/.test(m[0]));
|
||||
.some((m) => /<(input|textarea|select)\b/.test(m[0]));
|
||||
const kannFragen = geladen.some((g) => nutzer.has(g)) || hatDialogMitFeld;
|
||||
const hatDatei = html.includes("assets/js/nachfrage.js");
|
||||
if (kannFragen) { mitNutzung++; fragendeSeiten.push(s); if (!hatDatei) ohneDatei.push(s); }
|
||||
|
||||
@@ -614,13 +614,47 @@ melde("=== 11. Probe → Zugang ===");
|
||||
ok(!/personAnlegen|INSERT INTO personen/.test(eigen),
|
||||
"und der Talente-Server legt selbst keinen einzigen Zugang an");
|
||||
|
||||
/* DIE RECHTE HAND SIEHT DEN TRICHTER, VERGIBT ABER KEINE ZUGAENGE. */
|
||||
/* DIE RECHTE HAND LEGT ZUGAENGE AN -- SEIT DEM 22.09.2026.
|
||||
|
||||
Hier stand bis zum 23.09. das Gegenteil: "vergibt aber keine
|
||||
Zugaenge", geprueft mit 404. Das war bis zum 22.09. richtig. Dann
|
||||
hat Filipe es ausdruecklich umgedreht -- "damit sie das auch
|
||||
machen kann wenn er live ist" -- und die Route in
|
||||
workspace-personen.js oeffnet seither genau zwei Wege: anlegen
|
||||
und einen Code neu erzeugen.
|
||||
|
||||
DIE PRUEFUNG WAR ALSO SEIT EINEM TAG ROT, ohne dass es jemandem
|
||||
aufgefallen waere -- sie lief in der Zwischenzeit nicht. Das ist
|
||||
der Normalfall und nicht die Ausnahme: Man startet die
|
||||
Pruefungen, an denen man gerade baut.
|
||||
|
||||
UMGEDREHT WIRD SIE, NICHT GELOESCHT. Eine Erlaubnis ohne Grenze
|
||||
ist keine Entscheidung, sondern ein Loch. Geprueft wird deshalb
|
||||
ab jetzt beides: dass die rechte Hand es DARF -- und dass die
|
||||
Grenzen daneben stehen bleiben. */
|
||||
const hand = await roh("/workspace/api/verwaltung/personen", CREW, { cookie: kHand },
|
||||
{ name: "Heimlich", rolle: "modi" });
|
||||
ok(hand.code === 404, `die rechte Hand kommt am Anlegen nicht vorbei (${hand.code})`);
|
||||
ok(hand.code === 201, `die rechte Hand darf einen Zugang anlegen (${hand.code})`);
|
||||
|
||||
/* ABER NICHT ALLES. Sperren gehoert nicht dazu -- das ist die
|
||||
Grenze, die "anlegen" von "verwalten" trennt. Ohne diese Zeile
|
||||
waere die Aenderung oben eine Erlaubnis ohne Rand. */
|
||||
const handSperrt = await roh(`/workspace/api/verwaltung/personen/${json(hand).id}/aktiv`,
|
||||
CREW, { cookie: kHand }, { aktiv: 0 }, "PUT");
|
||||
ok(handSperrt.code === 404 || handSperrt.code === 403,
|
||||
`aber sperren darf sie niemanden (${handSperrt.code})`);
|
||||
|
||||
/* UND DAS PROTOKOLL BLEIBT BEI DOGFATHER. Dort stehen die
|
||||
Einstellungsaenderungen des ganzen Hauses. */
|
||||
const handProtokoll = await roh("/workspace/api/verwaltung/protokoll", CREW, { cookie: kHand });
|
||||
ok(handProtokoll.code === 404 || handProtokoll.code === 403,
|
||||
`und ins Protokoll sieht sie nicht (${handProtokoll.code})`);
|
||||
|
||||
/* EIN MODI DARF WEITERHIN GAR NICHTS DAVON. Das ist die eigentliche
|
||||
Aussage dieses Blocks -- und sie ist unveraendert. */
|
||||
const einModi = await roh("/workspace/api/verwaltung/personen", CREW, { cookie: kModi },
|
||||
{ name: "Heimlich", rolle: "modi" });
|
||||
ok(einModi.code === 404, `ein Modi erst recht nicht (${einModi.code})`);
|
||||
ok(einModi.code === 404, `ein Modi kommt am Anlegen nicht vorbei (${einModi.code})`);
|
||||
|
||||
/* GEGENPROBE: DogFather kann es -- sonst waere "404" oben auch dann
|
||||
gruen, wenn der Weg gar nicht existiert. */
|
||||
@@ -1477,7 +1511,7 @@ melde("=== 17. Der Anschluss im Browser ===");
|
||||
const neueTexte = await seite.evaluate(() =>
|
||||
[...document.querySelectorAll(".t-start, .t-person")]
|
||||
.map((e) => e.textContent || "").join(" "));
|
||||
ok(!/modis?/i.test(neueTexte),
|
||||
ok(!/\bmodis?\b/i.test(neueTexte),
|
||||
`die neuen Bausteine nennen die Rolle nicht (${neueTexte.length} Zeichen geprueft)`);
|
||||
/* GEGENPROBE zur Voraussetzung: Es wurde ueberhaupt etwas
|
||||
angesehen. Ohne Text waere die Zeile darueber auch dann gruen,
|
||||
|
||||
@@ -235,7 +235,7 @@ ok(mass.kartenZeichen.modi && mass.kartenZeichen.modi !== mass.kartenZeichen.adm
|
||||
Auf dem Bildschirmfoto stand "Sichtbar nur fuer die DogFather-Rolle"
|
||||
-- in einem Text, den jemand liest, sieht das aus wie ein Fehler,
|
||||
weil es einer ist. */
|
||||
ok(mass.kartenTexte.modi && !/(fuer|ueber|waehrend|koennen|moeglich)/
|
||||
ok(mass.kartenTexte.modi && !/\b(fuer|ueber|waehrend|koennen|moeglich)\b/
|
||||
.test(mass.kartenTexte.modi),
|
||||
`und ihre Beschreibung hat echte Umlaute ("${mass.kartenTexte.modi.slice(0, 60)}…")`);
|
||||
ok(mass.wahlUnten <= mass.formUnten,
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
/* =====================================================================
|
||||
STEHT IN EINER AUSGELIEFERTEN DATEI EIN ZERSTOERTES ZEICHEN?
|
||||
|
||||
WARUM ES DIESE PRUEFUNG GIBT: Weil genau das zweimal live stand,
|
||||
und zwar am selben Tag (23.09.2026), in zwei Dateien.
|
||||
|
||||
1. `content: "<0x15>C9<0x00>A0"` in chat.css. Gemeint war U+25C9
|
||||
(dasselbe Zeichen, das chat.js fuer das Rudel benutzt) und ein
|
||||
geschuetztes Leerzeichen. Im Quelltext standen stattdessen ZWEI
|
||||
ROHE STEUERZEICHEN. Auf dem Bildschirm las man deshalb woertlich
|
||||
"C9 A0" vor jedem hervorgehobenen @rudel -- die Steuerzeichen
|
||||
selbst sind unsichtbar.
|
||||
|
||||
2. `content: "¹3"` in chat.css UND entwicklung.css. Gemeint
|
||||
war ein Haken. Auf der gewaehlten Kachel stand "¹3".
|
||||
|
||||
DAS BESONDERE AN DIESER FEHLERART: Niemand sieht sie beim Lesen.
|
||||
Punkt 1 war sogar committet, ausgeliefert und lag live -- und als
|
||||
ich in der Datei danach suchte, meldete grep "Binary file matches"
|
||||
und zeigte die Zeile gar nicht erst an. Ein Mensch, der den
|
||||
Unterschied durchsieht, liest `content: "C9 A0"` und denkt sich
|
||||
nichts dabei.
|
||||
|
||||
Gefunden hat es am Ende Filipes Bildschirmfoto, nicht der Quelltext.
|
||||
Das ist zu spaet und zu zufaellig. Also eine Pruefung.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
WAS GEPRUEFT WIRD, UND WARUM DIE GRENZE DA LIEGT
|
||||
|
||||
AUSGELIEFERTE DATEIEN (workspace/, assets/, *.html) -- dort ist
|
||||
beides ein Fehler: ein Steuerzeichen UND ein Ersatzzeichen (U+FFFD).
|
||||
Was dort steht, sieht ein Mensch.
|
||||
|
||||
SERVERDATEIEN -- dort nur Steuerzeichen. Ein U+FFFD kann in einer
|
||||
Pruefung absichtlich stehen: pruef-ics.mjs sucht genau danach in
|
||||
Kalenderdateien. Eine Ausnahmeliste mit Dateinamen waere die
|
||||
naheliegende Loesung und die schlechtere -- sie altert. Die Grenze
|
||||
"wird es ausgeliefert?" altert nicht.
|
||||
|
||||
TABULATOR, ZEILENVORSCHUB UND WAGENRUECKLAUF sind erlaubt. Alles
|
||||
andere unter 0x20 ist in Quelltext nie Absicht.
|
||||
===================================================================== */
|
||||
import { readdirSync, readFileSync, statSync, writeFileSync, unlinkSync } from "node:fs";
|
||||
import { join, dirname, extname, relative } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
const HIER = dirname(fileURLToPath(import.meta.url));
|
||||
const WURZEL = join(HIER, "..");
|
||||
|
||||
let fehler = 0;
|
||||
let geprueft = 0;
|
||||
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||||
|
||||
/* NOTBREMSE. Auch eine Pruefung ohne Netzzugriff bekommt eine -- ein
|
||||
Lauf, der haengt, ist schlimmer als einer, der scheitert (06.09.2026,
|
||||
damals drei Stunden). */
|
||||
const notbremse = setTimeout(() => {
|
||||
console.log(" FEHL Notbremse: die Pruefung haengt.");
|
||||
process.exit(1);
|
||||
}, 60_000);
|
||||
|
||||
const ENDUNGEN = new Set([".js", ".mjs", ".html", ".css", ".json", ".svg",
|
||||
".txt", ".webmanifest"]);
|
||||
|
||||
/* Ordner, die uns nicht gehoeren oder erzeugt sind. */
|
||||
const AUSSEN = new Set([".git", "node_modules", "sicherungen", ".tmp"]);
|
||||
|
||||
/** Alle Dateien unterhalb eines Ordners, die uns angehen. */
|
||||
function dateien(ordner) {
|
||||
const raus = [];
|
||||
const gehe = (o) => {
|
||||
let eintraege;
|
||||
try { eintraege = readdirSync(o, { withFileTypes: true }); } catch { return; }
|
||||
for (const e of eintraege) {
|
||||
if (e.name.startsWith(".tmp-")) continue;
|
||||
const p = join(o, e.name);
|
||||
if (e.isDirectory()) { if (!AUSSEN.has(e.name)) gehe(p); continue; }
|
||||
if (ENDUNGEN.has(extname(e.name).toLowerCase())) raus.push(p);
|
||||
}
|
||||
};
|
||||
gehe(ordner);
|
||||
return raus;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
Die beiden Sucher. Getrennt, damit die Gegenprobe jeden einzeln
|
||||
nachweisen kann.
|
||||
--------------------------------------------------------------------- */
|
||||
|
||||
/** Rohe Steuerzeichen -- alles unter 0x20 ausser \t \n \r. */
|
||||
function steuerzeichenIn(roh) {
|
||||
const treffer = [];
|
||||
for (let i = 0; i < roh.length; i++) {
|
||||
const b = roh[i];
|
||||
if (b >= 0x20 || b === 0x09 || b === 0x0a || b === 0x0d) continue;
|
||||
/* Die Zeile mitgeben -- eine Bytenummer allein sucht man lange. */
|
||||
const zeile = roh.subarray(0, i).toString("utf8").split("\n").length;
|
||||
treffer.push({ zeile, byte: b });
|
||||
}
|
||||
return treffer;
|
||||
}
|
||||
|
||||
/** Ersatzzeichen U+FFFD -- in UTF-8 die drei Bytes ef bf bd. */
|
||||
function ersatzzeichenIn(roh) {
|
||||
const treffer = [];
|
||||
let i = -1;
|
||||
while ((i = roh.indexOf(Buffer.from([0xef, 0xbf, 0xbd]), i + 1)) !== -1) {
|
||||
treffer.push({ zeile: roh.subarray(0, i).toString("utf8").split("\n").length });
|
||||
}
|
||||
return treffer;
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
1. GEGENPROBE ZUERST
|
||||
|
||||
Eine Pruefung, die immer bestaetigt, bestaetigt nichts. Bevor sie
|
||||
ueber echte Dateien redet, weist sie nach, dass sie ueberhaupt
|
||||
anschlagen KANN -- an einer Datei, die sie sich selbst baut. Ohne
|
||||
das waere ein gruener Lauf hier auch dann gruen, wenn beide Sucher
|
||||
ins Leere greifen (der Fehler vom 01.09.2026, zweimal an einem Tag).
|
||||
===================================================================== */
|
||||
console.log("=== 0. Kann die Pruefung ueberhaupt Alarm schlagen? ===");
|
||||
{
|
||||
const probe = join(tmpdir(), "pruef-zeichen-probe.css");
|
||||
try {
|
||||
/* Genau die beiden Faelle, die live standen. */
|
||||
writeFileSync(probe, Buffer.concat([
|
||||
Buffer.from('a { content: "'),
|
||||
Buffer.from([0x15]), Buffer.from("C9"),
|
||||
Buffer.from([0x00]), Buffer.from('A0"; }\n'),
|
||||
Buffer.from('b::after { content: "'), Buffer.from([0xef, 0xbf, 0xbd]), Buffer.from('"; }\n'),
|
||||
]));
|
||||
const roh = readFileSync(probe);
|
||||
const st = steuerzeichenIn(roh);
|
||||
const er = ersatzzeichenIn(roh);
|
||||
ok(st.length === 2, `sie findet rohe Steuerzeichen (${st.length}, erwartet 2)`);
|
||||
ok(er.length === 1, `sie findet Ersatzzeichen (${er.length}, erwartet 1)`);
|
||||
|
||||
/* Und die andere Richtung: saubere Datei, kein Alarm. Eine
|
||||
Pruefung, die das richtige Verhalten bestraft, wird abgeschafft
|
||||
(03.09.2026). */
|
||||
writeFileSync(probe, 'a { content: "\\25C9\\A0"; }\n\tb { color: red; }\r\n');
|
||||
const sauber = readFileSync(probe);
|
||||
ok(steuerzeichenIn(sauber).length === 0 && ersatzzeichenIn(sauber).length === 0,
|
||||
"und sie schweigt bei einer sauberen Datei -- auch mit Tabulator und CRLF");
|
||||
} finally {
|
||||
try { unlinkSync(probe); } catch { /* beim Aufraeumen ist Scheitern egal */ }
|
||||
}
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
2. DIE AUSGELIEFERTEN DATEIEN
|
||||
===================================================================== */
|
||||
console.log("");
|
||||
console.log("=== 1. Was der Browser bekommt ===");
|
||||
|
||||
const geliefert = [
|
||||
...dateien(join(WURZEL, "workspace")),
|
||||
...dateien(join(WURZEL, "assets")),
|
||||
...dateien(WURZEL).filter((p) => dirname(p) === WURZEL && extname(p) === ".html"),
|
||||
];
|
||||
|
||||
{
|
||||
const schlimm = [];
|
||||
for (const p of geliefert) {
|
||||
const roh = readFileSync(p);
|
||||
for (const t of steuerzeichenIn(roh)) {
|
||||
schlimm.push(`${relative(WURZEL, p)}:${t.zeile} (Byte 0x${t.byte.toString(16).padStart(2, "0")})`);
|
||||
}
|
||||
for (const t of ersatzzeichenIn(roh)) {
|
||||
schlimm.push(`${relative(WURZEL, p)}:${t.zeile} (Ersatzzeichen)`);
|
||||
}
|
||||
}
|
||||
/* DIE ANZAHL STEHT IN DER BEDINGUNG, nicht nur im Text. Waere der
|
||||
Ordner leer oder der Pfad falsch, meldete "keine Fundstellen"
|
||||
sonst Erfolg -- gruen, ohne eine einzige Datei angesehen zu
|
||||
haben. */
|
||||
ok(geliefert.length > 50 && schlimm.length === 0,
|
||||
`${geliefert.length} ausgelieferte Dateien, ${schlimm.length} mit zerstoerten Zeichen`);
|
||||
for (const s of schlimm.slice(0, 12)) console.log(" " + s);
|
||||
if (schlimm.length > 12) console.log(` ... und ${schlimm.length - 12} weitere`);
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
3. DIE SERVERDATEIEN -- nur Steuerzeichen
|
||||
===================================================================== */
|
||||
console.log("");
|
||||
console.log("=== 2. Der Server ===");
|
||||
|
||||
{
|
||||
const schlimm = [];
|
||||
const serverDateien = [...dateien(join(WURZEL, "server")), ...dateien(join(WURZEL, "tools"))];
|
||||
for (const p of serverDateien) {
|
||||
const roh = readFileSync(p);
|
||||
for (const t of steuerzeichenIn(roh)) {
|
||||
schlimm.push(`${relative(WURZEL, p)}:${t.zeile} (Byte 0x${t.byte.toString(16).padStart(2, "0")})`);
|
||||
}
|
||||
}
|
||||
ok(serverDateien.length > 50 && schlimm.length === 0,
|
||||
`${serverDateien.length} Serverdateien, ${schlimm.length} mit Steuerzeichen`);
|
||||
for (const s of schlimm.slice(0, 12)) console.log(" " + s);
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
4. DIE STELLE, AN DER ES ZWEIMAL PASSIERT IST
|
||||
|
||||
`content:` in CSS ist der eine Ort, an dem ein Zeichen SICHTBAR
|
||||
wird, ohne dass es im Text steht. Beide Fehler sassen dort.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
DIE REGEL WAR IM ERSTEN ANLAUF ZU WEIT -- und das ist hier notiert,
|
||||
weil es der wichtigere Teil ist.
|
||||
|
||||
Zuerst stand hier "jedes Zeichen ueber 0x7f ist verdaechtig, schreib
|
||||
es als Escape". Das meldete auf Anhieb ACHT Stellen: ✓ in vier
|
||||
Dateien, ↯, ↻, „ und ⚠. Alle acht sind richtig, absichtlich und
|
||||
seit Monaten in Ordnung. Eine Pruefung, die beim ersten Lauf acht
|
||||
korrekte Stellen anmeckert, wird beim neunten Mal weggeklickt -- und
|
||||
dann faengt sie auch den echten Fall nicht mehr (03.09.2026).
|
||||
|
||||
DIE ENGERE REGEL KOMMT AUS DEM BEFUND SELBST. Das kaputte Zeichen
|
||||
war U+00B9 (hochgestellte Eins) -- ein Rest aus dem Latin-1-Block.
|
||||
Genau dort, zwischen U+0080 und U+00BF, liegen die typischen
|
||||
Truemmer einer verungluekten Kodierung: ¡ ¢ £ ¤ ¥ ¦ § ¨ © ª « ¬ ®
|
||||
° ± ² ³ ´ µ ¶ · ¸ ¹ º » ¼ ½ ¾.
|
||||
|
||||
Nachgemessen ueber alle 203 content-Angaben im Haus: KEINE
|
||||
einzige benutzt ein Zeichen aus diesem Block. Alle echten liegen
|
||||
darueber (✓ = U+2713, ⚠ = U+26A0, „ = U+201E). Die Regel ist damit
|
||||
heute gruen, haette den Fehler aber gefunden -- und sie ist nicht
|
||||
geraten, sondern an den vorhandenen Daten geprueft.
|
||||
===================================================================== */
|
||||
console.log("");
|
||||
console.log("=== 3. Zeichen, die CSS auf den Bildschirm schreibt ===");
|
||||
|
||||
/** Truemmerverdacht: Latin-1-Block, wo echte Typografie nie hingehoert. */
|
||||
const istTruemmer = (c) => c >= 0x80 && c <= 0xbf;
|
||||
|
||||
{
|
||||
const verdaechtig = [];
|
||||
let stellen = 0;
|
||||
for (const p of geliefert.filter((x) => extname(x) === ".css")) {
|
||||
const text = readFileSync(p, "utf8");
|
||||
for (const m of text.matchAll(/content:\s*"([^"]*)"/g)) {
|
||||
stellen++;
|
||||
for (const z of m[1]) {
|
||||
const c = z.codePointAt(0);
|
||||
if (istTruemmer(c)) {
|
||||
verdaechtig.push(`${relative(WURZEL, p)}: content "${m[1]}" enthaelt U+${c.toString(16).toUpperCase().padStart(4, "0")}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ok(stellen > 50 && verdaechtig.length === 0,
|
||||
`${stellen} content-Angaben in CSS, ${verdaechtig.length} mit Kodierungstruemmern`);
|
||||
for (const s of verdaechtig.slice(0, 12)) console.log(" " + s);
|
||||
|
||||
/* GEGENPROBE: Wuerde sie den echten Fall sehen? Nicht theoretisch --
|
||||
an genau der Zeichenfolge, die in chat.css stand. */
|
||||
const kaputt = [...'content: "¹3"'.matchAll(/content:\s*"([^"]*)"/g)]
|
||||
.flatMap((m) => [...m[1]]).some((z) => istTruemmer(z.codePointAt(0)));
|
||||
const heil = [...'content: "✓"'.matchAll(/content:\s*"([^"]*)"/g)]
|
||||
.flatMap((m) => [...m[1]]).some((z) => istTruemmer(z.codePointAt(0)));
|
||||
ok(kaputt && !heil,
|
||||
'sie erkennt das echte "¹3" und laesst den richtigen Haken in Ruhe');
|
||||
}
|
||||
|
||||
clearTimeout(notbremse);
|
||||
console.log("");
|
||||
console.log(fehler === 0
|
||||
? `Alles in Ordnung — ${geprueft} Prüfungen`
|
||||
: `${fehler} FEHLER — ${geprueft} Prüfungen`);
|
||||
process.exit(fehler === 0 ? 0 : 1);
|
||||
+84
-17
@@ -50,6 +50,7 @@ import {
|
||||
} from "./workspace.js";
|
||||
|
||||
import { erwaehnteIds, rudelGerufen, RUDEL_WORT } from "./chat-erwaehnung.js";
|
||||
import { KREIS, winkelAus } from "./chat-kreis.js";
|
||||
import { KATALOG, ERLAUBT, VORSCHLAG, FAVORITEN_MAX } from "./workspace-reaktionen.js";
|
||||
|
||||
/* Der Treff-Chat: eigener Raum, eigene Nachtruhe, keine Anrufe.
|
||||
@@ -409,6 +410,22 @@ export function schriftFuer(farbe) {
|
||||
* „Standard", und das ist ein gueltiger Zustand. */
|
||||
export function kachelPruefen(wert) {
|
||||
if (!wert) return null;
|
||||
/* SEIT DEM 23.09.2026 GIBT ES ZWEI SORTEN SCHLUESSEL.
|
||||
|
||||
Filipe wollte statt der Kachelreihe einen Kreis, auf dem man die
|
||||
Farbe "ganz genau selber auswaehlen kann". Ein Winkel ist damit
|
||||
eine gueltige Wahl: "ton-214" heisst 214 Grad.
|
||||
|
||||
WARUM NICHT EINFACH DIE FARBE SPEICHERN: Dann koennte sich jeder
|
||||
ein "#ffffff" eintragen -- und die helle Schrift der Blase waere
|
||||
darauf unlesbar. Der Winkel dagegen ist eine Nummer in einer
|
||||
Tabelle, die aus der Kontrastrechnung stammt. Wer sich etwas
|
||||
Unlesbares wuenscht, bekommt schlicht keinen Treffer.
|
||||
|
||||
Die alten Schluessel bleiben gueltig. Wer "veilchen" gewaehlt hat,
|
||||
behaelt es -- eine Umstellung, die stillschweigend die Wahl von
|
||||
fuenfzehn Leuten zuruecksetzt, waere kein Fortschritt. */
|
||||
if (winkelAus(wert) !== null) return String(wert);
|
||||
return CHAT_KACHELN.some((k) => k.wert === String(wert)) ? String(wert) : null;
|
||||
}
|
||||
|
||||
@@ -958,6 +975,22 @@ chatRouter.get("/workspace/api/chat/kacheln", (req, res) => {
|
||||
rechnen -- dann staende die Regel zweimal da, und beim
|
||||
naechsten Ton waere eine der beiden Fassungen aelter. */
|
||||
kacheln: CHAT_KACHELN.map((k) => ({ ...k, schrift: schriftFuer(k.farbe) })),
|
||||
/* DER KREIS: 360 fertige Farben, Index = Winkel.
|
||||
|
||||
Der Browser rechnet sie NICHT selbst aus. Die Formel steht in
|
||||
tools/chat-kacheln-rechnen.mjs und haengt an den Schriftfarben
|
||||
aus dem CSS; sie ein zweites Mal in JavaScript zu fuehren
|
||||
hiesse, dass die zweite Fassung ab dem naechsten
|
||||
Schriftwechsel falsch ist -- und zwar unbemerkt, denn beide
|
||||
liefern ja "irgendeine Farbe".
|
||||
|
||||
Rund 3 kB. Sie kommen mit derselben Anfrage, die es ohnehin
|
||||
gibt; eine eigene waere eine zweite Wartezeit fuer nichts. */
|
||||
kreis: KREIS,
|
||||
/* Alle 360 liegen auf derselben Leuchtdichte wie die elf bunten
|
||||
Kacheln -- also tragen sie auch dieselbe Schrift. Einmal
|
||||
gerechnet und mitgeschickt, statt 360-mal dasselbe. */
|
||||
kreis_schrift: schriftFuer(KREIS[0]),
|
||||
/* Die eigene WAHL, nicht der getragene Ton -- sonst stuende im
|
||||
Waehler ein Haken bei einer Farbe, die niemand gewaehlt hat. */
|
||||
meine: gewaehlteKachelFuer(req.person.id),
|
||||
@@ -1529,9 +1562,29 @@ chatRouter.get("/workspace/api/chat/raeume/:id/nachrichten", (req, res) => {
|
||||
von Hand tippt, ruft eben niemanden -- genau wie ein "@Anna",
|
||||
das Anna nicht beantwortet. Verboten ist es nicht, es ist nur
|
||||
keine Hilfe, es vorzuschlagen. */
|
||||
/* WEN DER VORSCHLAG ERREICHT, HAENGT AM RAUM -- und die
|
||||
Schranke davor muss dieselbe Frage stellen.
|
||||
|
||||
Sonst entsteht ein stiller Widerspruch: Sitzt im Rudel-Raum
|
||||
ein Modi mit neun Zuschauern, aber ohne zweites Teammitglied,
|
||||
dann traefe "@rudel" jetzt neun Leute -- der Vorschlag beim
|
||||
Tippen waere aber ausgeblendet, weil er nach einem zweiten
|
||||
Teammitglied sucht. Die Funktion gaebe es, und niemand faende
|
||||
sie. Also wird hier genau das gefragt, was der Ruf nachher
|
||||
auch tut. */
|
||||
darf_rudel: istTeamDogi(req.person)
|
||||
&& leute.some((t) => t.id !== req.person.id && istTeamDogi(t)),
|
||||
&& (istTreffRaum(raumId)
|
||||
? leute.some((t) => t.id !== req.person.id)
|
||||
: leute.some((t) => t.id !== req.person.id && istTeamDogi(t))),
|
||||
rudel_wort: RUDEL_WORT,
|
||||
/* UND WEN ER MEINT, IN WORTEN.
|
||||
|
||||
Der Browser koennte die Kategorie des Raums vergleichen --
|
||||
dann stuende die Regel an zwei Stellen und liefe irgendwann
|
||||
auseinander. Er bekommt stattdessen die Antwort: "raum" oder
|
||||
"team". Die Worte dazu bildet er selbst, damit deutscher Text
|
||||
nicht im Server liegt. */
|
||||
rudel_wen: istTreffRaum(raumId) ? "raum" : "team",
|
||||
/* DARF ICH SPRACHNACHRICHTEN SCHICKEN? (23.09.2026)
|
||||
|
||||
Aus derselben Funktion wie die Schranke in der Anhang-Route.
|
||||
@@ -1754,24 +1807,38 @@ chatRouter.post("/workspace/api/chat/raeume/:id/nachrichten", gleicheHerkunft,
|
||||
const alleLeute = teilnehmerVon(raumId);
|
||||
const rudel = rudelGerufen(text, alleLeute, { rudel: darfRudel });
|
||||
|
||||
/* WEN DER RUF ERREICHT: die Leute IN DIESEM RAUM, die zu Team
|
||||
Dogi gehoeren -- nicht jeden im Raum.
|
||||
/* WEN DER RUF ERREICHT -- und das haengt vom RAUM ab.
|
||||
|
||||
Das Wort sagt es selbst. Ein Rudel ist das Team, und Filipe
|
||||
nennt in einem Atemzug dieselben vier Gruppen, die es auch
|
||||
rufen duerfen. In einem reinen Team-Kanal ist das jeder
|
||||
Anwesende, also genau "alle auf einmal".
|
||||
Bis zum 23.09.2026 galt ueberall dasselbe: Der Ruf erreichte
|
||||
nur die aus Team Dogi. Die Begruendung stand hier und war
|
||||
nicht falsch -- im Treff sitzt die Community, und "@rudel"
|
||||
haette dort jeden geweckt.
|
||||
|
||||
DER UNTERSCHIED WIRD IM TREFF SICHTBAR, und dort haette die
|
||||
andere Lesart wehgetan: Dort sitzt die Community. "@rudel"
|
||||
haette zweihundert Zuschauer gleichzeitig geweckt -- und beim
|
||||
zweiten Mal haetten sie die Benachrichtigungen abgeschaltet.
|
||||
Genau davor warnt der erste Absatz dieser Datei seit dem
|
||||
20.09. Ein Moderator, der im Treff "@rudel" schreibt, erreicht
|
||||
damit seine Kollegen; die Zuschauer lesen es einfach mit. */
|
||||
const ausDemRudel = rudel
|
||||
? alleLeute.filter((t) => istTeamDogi(t)).map((t) => t.id)
|
||||
: [];
|
||||
GENAU DAS WILL FILIPE JETZT, und zwar ausdruecklich nur dort:
|
||||
"wenn wir da im chat @rudel machen will ich dass jeder in dem
|
||||
chat markiert wird. nicht nur team ... und sehr wichtig ich
|
||||
rede nur von dem chat wo die ganze community auch drin ist.
|
||||
sonst nichts anfassen."
|
||||
|
||||
Also zwei Bedeutungen, nach Raum getrennt:
|
||||
|
||||
im Rudel-Raum -> ALLE, die drin sind. Der Raum IST das
|
||||
Rudel; er heisst auch so.
|
||||
ueberall sonst -> Team Dogi, wie bisher. Ein Gruppenchat
|
||||
mit drei Creatorinnen bleibt unberuehrt.
|
||||
|
||||
WAS DIE ALTE SORGE ENTSCHAERFT, und warum das hier tragbar
|
||||
ist: RUFEN darf weiterhin nur Team Dogi (siehe darfRudel eine
|
||||
Zeile hoeher) -- kein Zuschauer kann alle wecken. Und die
|
||||
Nachtruhe gilt in diesem Raum ohnehin, der Ruf kommt also
|
||||
nicht um drei Uhr nachts. Bleibt die eine Stelle, an der man
|
||||
hinsehen muss: Waechst der Raum auf Hunderte, unterbricht ein
|
||||
Ruf sie alle. Das steht im Protokoll (chat_rudel mit Anzahl),
|
||||
damit es eine Zahl bleibt und kein Gefuehl. */
|
||||
const ausDemRudel = !rudel ? []
|
||||
: istTreffRaum(raumId)
|
||||
? alleLeute.map((t) => t.id)
|
||||
: alleLeute.filter((t) => istTeamDogi(t)).map((t) => t.id);
|
||||
|
||||
const erwaehnt = [...new Set([
|
||||
...erwaehnteIds(text, alleLeute, { rudel: darfRudel }),
|
||||
|
||||
@@ -452,7 +452,7 @@ treffRouter.get("/workspace/api/treff/regeln", (req, res) => {
|
||||
Skript. Jetzt kommt er von hier.
|
||||
|
||||
UND DER SCHLUESSEL HIESS SELBST SO: `modi_frist_tage`. Das
|
||||
Wortleck-Muster ist `modi` und trifft das nicht, weil danach
|
||||
Wortleck-Muster ist `\bmodi\b` und trifft das nicht, weil danach
|
||||
ein Unterstrich folgt -- ein Leck durch eine Luecke im Muster,
|
||||
nicht durch eine Entscheidung. Deshalb heisst das Feld jetzt
|
||||
nach dem, was es bedeutet. */
|
||||
|
||||
@@ -245,6 +245,38 @@ export function treffAngleichen() {
|
||||
try {
|
||||
const id = treffRaumId();
|
||||
const d = db();
|
||||
|
||||
/* DER NAME GEHÖRT DAZU — und genau das fehlte.
|
||||
|
||||
treffRaumId() setzt TREFF_NAME nur BEIM ANLEGEN. Als der Raum
|
||||
später von „Der Treff" in „Das Rudel" umbenannt wurde, änderte
|
||||
sich damit die Konstante im Code — die Zeile in der Datenbank
|
||||
aber nicht. Der Raum existierte ja längst. Ergebnis: Im Code
|
||||
stand „Das Rudel", in der Gesprächsliste stand weiter „Der
|
||||
Treff", und zwar so lange, bis Filipe es gesehen hat
|
||||
(23.09.2026: „da steht links immer noch der treff anstatt das
|
||||
rudel").
|
||||
|
||||
Das ist dieselbe Falle wie nebenan bei den Mitgliedern, und
|
||||
der Kommentar dort hat sie sogar benannt — „drei Stellen, von
|
||||
denen die dritte vergessen wird". Nur galt die Vorsorge für
|
||||
die Teilnehmer und nicht für den Namen. Eine halbe
|
||||
Selbstheilung heilt die andere Hälfte nicht.
|
||||
|
||||
Jetzt wird der Name ABGELEITET statt gepflegt: Er steht an
|
||||
einer Stelle (TREFF_NAME), und dieser Abgleich zieht die
|
||||
Datenbank bei jedem Start hinterher. Eine künftige
|
||||
Umbenennung ist damit wieder eine Zeile.
|
||||
|
||||
"IS NOT" statt "!=": Wäre der Name NULL, ergäbe "name != ?" in
|
||||
SQLite NULL — also weder wahr noch falsch, und die Zeile bliebe
|
||||
unverändert liegen. Genau der Fall, den niemand von Hand
|
||||
ausprobiert. */
|
||||
const umbenannt = d.prepare(
|
||||
"UPDATE chat_raeume SET name = ? WHERE id = ? AND name IS NOT ?")
|
||||
.run(TREFF_NAME, id, TREFF_NAME).changes;
|
||||
if (umbenannt) console.log("[treff] Raum heißt jetzt " + TREFF_NAME + ".");
|
||||
|
||||
const platz = TREFF_ROLLEN.map(() => "?").join(", ");
|
||||
const fehlen = d.prepare(`
|
||||
SELECT p.id FROM personen p
|
||||
|
||||
+39
-1
@@ -7220,15 +7220,53 @@ export function einstellung(schluessel, vorgabe = null) {
|
||||
}
|
||||
}
|
||||
|
||||
/* WELCHE EINSTELLUNGEN NICHT IM KLARTEXT INS PROTOKOLL GEHOEREN
|
||||
---------------------------------------------------------------------
|
||||
Gefunden am 23.09.2026, beim Vorbereiten des KLIPY-Zugangs.
|
||||
|
||||
einstellungSetzen() schrieb bis dahin IMMER Name UND Wert ins
|
||||
Protokoll. Fuer "nachtruhe_an = 1" ist das genau richtig -- man
|
||||
will sehen, wer wann was umgestellt hat. Fuer einen Schluessel
|
||||
ist es das Gegenteil.
|
||||
|
||||
NACHGEMESSEN in der echten Datenbank, nicht vermutet:
|
||||
|
||||
kopie_schluessel (31.08.) steht VOLLSTAENDIG im Protokoll
|
||||
vapid_paar (05.09.) wurde bei 120 Zeichen abgeschnitten,
|
||||
kurz VOR dem privaten Teil
|
||||
|
||||
Dass der zweite heil blieb, lag an einer Laengengrenze, nicht an
|
||||
einer Absicht. Das ist keine offene Tuer -- das Protokoll liegt
|
||||
hinter nurAdmin --, aber der Wert wandert in JEDE Sicherung und in
|
||||
jeden Export. Und der naechste Schluessel waere denselben Weg
|
||||
gegangen.
|
||||
|
||||
ES IST EINE REGEL UND KEINE LISTE. Eine Liste
|
||||
["kopie_schluessel","vapid_paar"] waere beim naechsten Geheimnis
|
||||
still unvollstaendig -- derselbe Fehler wie bei den abgeschriebenen
|
||||
Spaltennamen am 11.09. Wer eine Einstellung so BENENNT, meint auch
|
||||
ein Geheimnis. Faellt ein harmloser Name versehentlich darunter,
|
||||
ist der Schaden, dass im Protokoll etwas weniger Text steht.
|
||||
|
||||
WAS STEHEN BLEIBT: dass sich die Einstellung geaendert hat, wann,
|
||||
und durch wen. Nur der Wert fehlt. Ein Protokolleintrag ohne
|
||||
Eintrag waere die schlechtere Loesung -- dann saehe niemand mehr,
|
||||
dass jemand den Schluessel ausgetauscht hat. */
|
||||
const GEHEIME_EINSTELLUNG = /(schluessel|schlüssel|geheim|token|passwort|kennwort|secret|paar|(^|[_-])key([_-]|$))/i;
|
||||
|
||||
export function einstellungSetzen(schluessel, wert, akteur = null) {
|
||||
db().prepare(`
|
||||
INSERT INTO einstellungen (schluessel, wert, geaendert, von) VALUES (?,?,?,?)
|
||||
ON CONFLICT(schluessel) DO UPDATE SET
|
||||
wert = excluded.wert, geaendert = excluded.geaendert, von = excluded.von`).run(
|
||||
schluessel, String(wert), new Date().toISOString(), akteur?.id ?? null);
|
||||
const roh = String(wert ?? "");
|
||||
const detail = GEHEIME_EINSTELLUNG.test(String(schluessel))
|
||||
? `${schluessel} = ${roh ? "(gesetzt, " + roh.length + " Zeichen)" : "(geleert)"}`
|
||||
: `${schluessel} = ${wert}`;
|
||||
protokolliere("einstellung_geaendert", {
|
||||
personId: akteur?.id ?? null, rolle: akteur?.rolle ?? null, ip: akteur?.ip ?? null,
|
||||
detail: `${schluessel} = ${wert}`.slice(0, 120),
|
||||
detail: detail.slice(0, 120),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -241,6 +241,125 @@ function buntest(hGrad) {
|
||||
return besteRgb ? { c: besteC, rgb: besteRgb } : null;
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
DER GANZE KREIS (23.09.2026)
|
||||
|
||||
Filipe: "anstatt sowas um die farben auszusuchen, kannst du nicht so
|
||||
eine art diagramm machen wo man mit einem kreis herum gehen kann und
|
||||
sie die farbe ganz genau selber auswaehlen kann?"
|
||||
|
||||
---------------------------------------------------------------------
|
||||
WARUM DAS HIER STEHT UND NICHT IM BROWSER
|
||||
|
||||
Die zwoelf Kacheln sind nicht ausgesucht, sondern GERECHNET: Jede
|
||||
liegt auf derselben Leuchtdichte, damit die Schrift ueberall gleich
|
||||
gut lesbar ist und niemand sich durch seine Wahl unlesbar machen
|
||||
kann. Ein Farbkreis, der einfach den Farbton dreht, wirft genau das
|
||||
weg -- ein gesaettigtes Gelb ist um ein Vielfaches heller als ein
|
||||
gesaettigtes Blau, und helle Schrift darauf ist nicht mehr zu lesen.
|
||||
|
||||
Der Kreis ist deshalb KEINE neue Rechnung, sondern dieselbe in 360
|
||||
Schritten statt in zwoelf. buntest() entscheidet fuer jeden Winkel,
|
||||
und die Kontrastpruefung darin gilt unveraendert. Was dabei
|
||||
herauskommt, ist ein Ring gleicher Helligkeit: Er sieht ruhiger aus
|
||||
als ein ueblicher Farbkreis -- kein blendendes Gelb, kein
|
||||
abgesoffenes Blau -- und genau das ist der Punkt.
|
||||
|
||||
DER BROWSER RECHNET NICHTS. Er bekommt die 360 fertigen Farben. Wer
|
||||
die Formel in JavaScript nachbaut, hat zwei Rechnungen, und die
|
||||
zweite ist ab dem naechsten Schriftwechsel falsch -- ohne dass es
|
||||
jemand merkt, weil beide "irgendeine Farbe" liefern.
|
||||
===================================================================== */
|
||||
if (process.argv.includes("--kreis")) {
|
||||
const SCHRITTE = 360;
|
||||
const toene = [];
|
||||
/* DIE ZAHLEN BLEIBEN LIEGEN, nicht nur das Hex. kontrast() und
|
||||
helligkeit() erwarten [r,g,b] -- gaebe man ihnen die
|
||||
Zeichenkette "#aabbcc", zerlegt die Destrukturierung sie in
|
||||
"#", "a", "a" und rechnet ab da mit NaN. Der Vergleich
|
||||
"NaN < 1" ist immer falsch, die Pruefung waere also gruen und
|
||||
haette nichts gemessen. */
|
||||
const rgbs = [];
|
||||
const schwach = [];
|
||||
let fehlend = 0;
|
||||
|
||||
for (let h = 0; h < SCHRITTE; h++) {
|
||||
const b = buntest(h);
|
||||
if (!b) { fehlend++; toene.push(null); rgbs.push(null); continue; }
|
||||
toene.push(hex(b.rgb));
|
||||
rgbs.push(b.rgb);
|
||||
if (b.c < MINDEST_BUNTHEIT) schwach.push(h);
|
||||
}
|
||||
|
||||
/* DRITTER AUSGANG. Findet sich fuer einen Winkel ueberhaupt keine
|
||||
Farbe, die den Kontrast haelt, ist der Ring unterbrochen -- dann
|
||||
wird NICHT stillschweigend etwas Naheliegendes eingesetzt,
|
||||
sondern gesagt, dass es nicht geht. Eine Luecke, die als Farbe
|
||||
getarnt ist, faellt niemandem auf. */
|
||||
if (fehlend) {
|
||||
console.error("\nKONNTE NICHT NACHSEHEN: " + fehlend + " von " + SCHRITTE
|
||||
+ " Winkeln halten den Kontrast nicht."
|
||||
+ "\n Heisst: die Schrift auf der Blase ist zu dunkel."
|
||||
+ "\n Erst --blase-leise in chat.css aufhellen, dann erneut rechnen.");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
/* DIE NACHPRUEFUNG AM AUSGELIEFERTEN WERT, nicht am gerechneten.
|
||||
Ausgeliefert wird das gerundete Hex -- und gerundet wird nach der
|
||||
Kontrastpruefung. Das ist die Stelle, an der am 22.09. schon
|
||||
einmal drei Hundertstel gefehlt haben. */
|
||||
let schlechtester = Infinity, schlechtesterWinkel = -1;
|
||||
for (let h = 0; h < SCHRITTE; h++) {
|
||||
for (const sf of pruefFarben) {
|
||||
const k = kontrast(rgbs[h], sf.farbe);
|
||||
if (k / sf.noetig < schlechtester) { schlechtester = k / sf.noetig; schlechtesterWinkel = h; }
|
||||
}
|
||||
}
|
||||
if (schlechtester < 1) {
|
||||
console.error("\nNICHT IN ORDNUNG: Winkel " + schlechtesterWinkel
|
||||
+ " verfehlt seine Schwelle (Faktor " + schlechtester.toFixed(3) + ").");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
console.log("Kreis gerechnet: " + SCHRITTE + " Winkel, alle ueber ihrer Schwelle"
|
||||
+ " (knappster: Winkel " + schlechtesterWinkel + ", Faktor "
|
||||
+ schlechtester.toFixed(3) + ").");
|
||||
if (schwach.length) {
|
||||
console.log(" Unter Buntheit " + MINDEST_BUNTHEIT + ": " + schwach.length
|
||||
+ " Winkel. Das ist im Kreis in Ordnung -- dort darf es blasse"
|
||||
+ " Stellen geben, bei den zwoelf Kacheln nicht.");
|
||||
}
|
||||
|
||||
const ziel = new URL("../server/chat-kreis.js", import.meta.url);
|
||||
const zeilen = [];
|
||||
for (let i = 0; i < SCHRITTE; i += 6) {
|
||||
zeilen.push(" " + toene.slice(i, i + 6).map((t) => '"' + t + '"').join(", ") + ",");
|
||||
}
|
||||
writeFileSync(ziel, [
|
||||
"/* ERZEUGT von tools/chat-kacheln-rechnen.mjs --kreis.",
|
||||
" NICHT VON HAND AENDERN -- wer hier eine Farbe eintippt, umgeht",
|
||||
" genau die Kontrastrechnung, die die Lesbarkeit garantiert.",
|
||||
"",
|
||||
" 360 Farbtoene, Schritt 1 Grad, alle auf derselben Leuchtdichte",
|
||||
" wie die zwoelf Kacheln. Der Index IST der Winkel: KREIS[214] ist",
|
||||
" 214 Grad. */",
|
||||
"export const KREIS = [",
|
||||
...zeilen,
|
||||
"];",
|
||||
"",
|
||||
"/** Der Winkel zu einem Schluessel der Form \"ton-214\", sonst null. */",
|
||||
"export function winkelAus(wert) {",
|
||||
" const t = /^ton-(\\d{1,3})$/.exec(String(wert ?? \"\"));",
|
||||
" if (!t) return null;",
|
||||
" const n = Number(t[1]);",
|
||||
" return n >= 0 && n < KREIS.length ? n : null;",
|
||||
"}",
|
||||
"",
|
||||
].join("\n"), "utf8");
|
||||
console.log("Geschrieben: server/chat-kreis.js");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const bunte = HUES.map((h) => ({ h, ...(buntest(h) || {}) }));
|
||||
const fehlen = bunte.filter((b) => !b.rgb || b.c < MINDEST_BUNTHEIT);
|
||||
if (fehlen.length) {
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
/* =====================================================================
|
||||
nachtlauf.mjs — einmal in der Nacht alles prüfen und es AUFSCHREIBEN
|
||||
|
||||
Filipe am 23.09.2026 auf die Frage, ob ein nächtlicher Lauf die
|
||||
Prüfungen selbst anstoßen und morgens sagen soll, welche rot sind:
|
||||
„ja".
|
||||
|
||||
---------------------------------------------------------------------
|
||||
WARUM ES DAS BRAUCHT — drei Funde an EINEM Tag
|
||||
|
||||
Am 23.09.2026 sind mir drei Prüfungen aufgefallen, die rot waren,
|
||||
ohne dass es jemandem aufgefallen wäre:
|
||||
|
||||
pruef-modi-wortleck seit dem 22.09. — der verborgene Rollenname
|
||||
stand offen im Netz
|
||||
pruef-zuteilung stürzte ab und prüfte danach gar nichts mehr
|
||||
pruef-push-ziel meldete einen Befund, der in Wahrheit eine
|
||||
erfüllte Bestellung war
|
||||
|
||||
Alle drei habe ich durch ZUFALL gefunden — weil sie neben dem lagen,
|
||||
woran ich gerade gebaut habe. Prüfungen laufen nur, wenn jemand sie
|
||||
startet, und man startet die, an denen man arbeitet.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
WOHIN DAS ERGEBNIS GEHT — und wohin ausdrücklich NICHT
|
||||
|
||||
In eine Notiz im Vault. NICHT in die laufende Anwendung.
|
||||
|
||||
Das ist keine Bequemlichkeit, sondern die Lehre vom 04.09.2026: Ein
|
||||
Prüflauf ist kein harmloser Lesevorgang, sein Ergebnis wird
|
||||
VERÖFFENTLICHT. Damals hat ein Netzproblem bei npm einen nächtlichen
|
||||
Selbsttest rot gemacht, und ab da stand VanVan in roter Schrift auf
|
||||
dem Startbildschirm, die Buchführung sei womöglich verletzt. An der
|
||||
Buchführung war nichts.
|
||||
|
||||
Wer einen Prüflauf anstößt, muss vorher überlegen, was der
|
||||
SCHLECHTESTE Ausgang auf dem Bildschirm einer Nutzerin anrichtet.
|
||||
Hier richtet er nichts an: Er schreibt eine Notiz, die Filipe und ich
|
||||
lesen. Die Community sieht davon nichts.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
DREI AUSGÄNGE, NICHT ZWEI
|
||||
|
||||
in Ordnung alles grün
|
||||
nicht in Ordnung mindestens eine Datei rot
|
||||
KONNTE NICHT NACHSEHEN der Lauf hat gar nicht stattgefunden
|
||||
|
||||
Der dritte ist der wichtige. Wenn der Rechner nachts aus war, gibt es
|
||||
kein Ergebnis — und ein FEHLENDES Ergebnis sieht sonst genauso aus
|
||||
wie ein altes gutes. Die Notiz sagt deshalb immer, WANN zuletzt
|
||||
wirklich gemessen wurde, und ab zwei Tagen ohne Lauf steht das ganz
|
||||
oben.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
DIE ZAHL WIRD MIT GESTERN VERGLICHEN
|
||||
|
||||
Ein grüner Lauf ist kein Beweis, solange nicht auch die ANZAHL
|
||||
stimmt (28.08.2026, RunOne: 789 statt 804 Prüfungen, weil acht je
|
||||
Gerät stillschweigend weggefallen waren). Diese Datei merkt sich
|
||||
deshalb den letzten Stand und meldet jeden Rückgang — auch wenn
|
||||
alles grün ist.
|
||||
|
||||
AUFRUF:
|
||||
node tools/nachtlauf.mjs normal
|
||||
node tools/nachtlauf.mjs --jetzt auch wenn gerade gearbeitet wird
|
||||
===================================================================== */
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import {
|
||||
writeFileSync, readFileSync, existsSync, mkdirSync, unlinkSync, statSync,
|
||||
} from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const HIER = dirname(fileURLToPath(import.meta.url));
|
||||
const WURZEL = join(HIER, "..");
|
||||
|
||||
/* Der Vault -- dort liest Filipe. Absichtlich fest und nicht geraten:
|
||||
Wenn der Ordner umzieht, soll das auffallen und nicht still in einen
|
||||
neuen Ordner schreiben, den niemand aufmacht. */
|
||||
const VAULT = "C:/Users/qciga/Documents/Obelix/DogFather";
|
||||
const NOTIZ = join(VAULT, "02 Projekte", "Pruefstand.md");
|
||||
const ZUSTAND = join(WURZEL, "tools", ".nachtlauf-stand.json");
|
||||
const SCHLOSS = join(WURZEL, "tools", ".nachtlauf-laeuft");
|
||||
|
||||
/* ZWEI LÄUFE GLEICHZEITIG SIND KEIN LAUF, SONDERN ZWEI KAPUTTE
|
||||
(06.09.2026, RunOne). Beide belegen dieselben Ports und legen
|
||||
dieselben Testdaten an; heraus kommen Fehler, die es nicht gibt --
|
||||
und wer sie für echt hält, sucht stundenlang an Stellen, an denen
|
||||
nichts kaputt ist.
|
||||
|
||||
DREI STUNDEN, dann gilt das Schloss als vergessen. Ein Schloss, das
|
||||
nie verfällt, blockiert beim nächsten Stromausfall für immer -- und
|
||||
was blockiert, wird abgeschafft. */
|
||||
const SCHLOSS_FRIST_MS = 3 * 60 * 60 * 1000;
|
||||
|
||||
const zweiZiffern = (n) => String(n).padStart(2, "0");
|
||||
function zeitWort(d = new Date()) {
|
||||
return `${zweiZiffern(d.getDate())}.${zweiZiffern(d.getMonth() + 1)}.${d.getFullYear()}`
|
||||
+ ` um ${zweiZiffern(d.getHours())}:${zweiZiffern(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
function schlossNehmen() {
|
||||
if (existsSync(SCHLOSS)) {
|
||||
let alter = Infinity;
|
||||
try { alter = Date.now() - statSync(SCHLOSS).mtimeMs; } catch { /* egal */ }
|
||||
if (alter < SCHLOSS_FRIST_MS) {
|
||||
let wer = "";
|
||||
try { wer = readFileSync(SCHLOSS, "utf8").trim(); } catch { /* egal */ }
|
||||
return { ok: false, grund: `Es läuft schon einer (seit ${Math.round(alter / 60000)} min, ${wer}).` };
|
||||
}
|
||||
/* Älter als die Frist: Der gehört niemandem mehr. */
|
||||
try { unlinkSync(SCHLOSS); } catch { /* egal */ }
|
||||
}
|
||||
try {
|
||||
writeFileSync(SCHLOSS, `PID ${process.pid}, gestartet ${zeitWort()}\n`, { flag: "wx" });
|
||||
return { ok: true };
|
||||
} catch {
|
||||
return { ok: false, grund: "Das Schloss ließ sich nicht setzen." };
|
||||
}
|
||||
}
|
||||
function schlossAbgeben() { try { unlinkSync(SCHLOSS); } catch { /* egal */ } }
|
||||
|
||||
function standLesen() {
|
||||
try { return JSON.parse(readFileSync(ZUSTAND, "utf8")); } catch { return null; }
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
Der Lauf selbst -- alles-pruefen.mjs macht die eigentliche Arbeit.
|
||||
|
||||
KEIN ZWEITER LÄUFER: Dort stecken die Frist je Datei, das Zählen der
|
||||
Einzelpunkte und die Regel "null Prüfungen ist ein Fehler". Das hier
|
||||
noch einmal zu bauen hiesse, zwei Rechnungen über dieselbe Frage zu
|
||||
haben -- und eine davon wäre irgendwann anders.
|
||||
--------------------------------------------------------------------- */
|
||||
function laufen() {
|
||||
return new Promise((fertig) => {
|
||||
const start = Date.now();
|
||||
const kind = spawn(process.execPath, [join("tools", "alles-pruefen.mjs")],
|
||||
{ cwd: WURZEL, stdio: ["ignore", "pipe", "pipe"] });
|
||||
let aus = "";
|
||||
kind.stdout.on("data", (d) => { aus += d; });
|
||||
kind.stderr.on("data", (d) => { aus += d; });
|
||||
kind.on("error", (f) => fertig({ ging: false, grund: String(f?.message || f), aus }));
|
||||
kind.on("close", (code) => fertig({
|
||||
ging: true, code, aus,
|
||||
minuten: Math.round((Date.now() - start) / 60000),
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
/** Aus der Ausgabe die Zahlen holen -- gelesen, nicht geschätzt. */
|
||||
function auswerten(aus) {
|
||||
const dateien = aus.match(/Dateien:\s+(\d+) von (\d+)/);
|
||||
const punkte = aus.match(/Einzelpunkte:\s+(\d+) von (\d+)/);
|
||||
const rote = [];
|
||||
for (const treffer of aus.matchAll(/^### (pruef-[\w-]+\.mjs) \(Exitcode (-?\d+)\)$/gm)) {
|
||||
/* Die erste FEHL-Zeile darunter ist die Auskunft, die man braucht --
|
||||
der Rest ist Zusammenhang. */
|
||||
const ab = aus.indexOf(treffer[0]) + treffer[0].length;
|
||||
const stueck = aus.slice(ab, ab + 900);
|
||||
const erste = (stueck.split("\n").find((z) => /FEHL|Error|error:/.test(z)) || "").trim();
|
||||
rote.push({ datei: treffer[1], code: Number(treffer[2]), erste: erste.slice(0, 160) });
|
||||
}
|
||||
return {
|
||||
dateienOk: dateien ? Number(dateien[1]) : null,
|
||||
dateien: dateien ? Number(dateien[2]) : null,
|
||||
punkteOk: punkte ? Number(punkte[1]) : null,
|
||||
punkte: punkte ? Number(punkte[2]) : null,
|
||||
rote,
|
||||
};
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
Die Notiz
|
||||
--------------------------------------------------------------------- */
|
||||
function notizSchreiben({ lage, vorher, minuten, konnteNicht }) {
|
||||
const jetzt = new Date();
|
||||
const zeilen = [];
|
||||
zeilen.push("# Prüfstand Team Dogi");
|
||||
zeilen.push("");
|
||||
zeilen.push("*Diese Notiz schreibt `tools/nachtlauf.mjs` — von Hand ändern bringt nichts,");
|
||||
zeilen.push("beim nächsten Lauf ist es wieder überschrieben.*");
|
||||
zeilen.push("");
|
||||
|
||||
if (konnteNicht) {
|
||||
zeilen.push("## ⚠ Konnte nicht nachsehen");
|
||||
zeilen.push("");
|
||||
zeilen.push(konnteNicht);
|
||||
zeilen.push("");
|
||||
if (vorher?.wann) {
|
||||
const tage = Math.floor((Date.now() - vorher.zeit) / 86400000);
|
||||
zeilen.push(`Zuletzt wirklich gemessen: **${vorher.wann}**`
|
||||
+ (tage >= 1 ? ` — das ist **${tage} Tag${tage === 1 ? "" : "e"} her**.` : "."));
|
||||
} else {
|
||||
zeilen.push("Es gibt noch kein früheres Ergebnis.");
|
||||
}
|
||||
return zeilen.join("\n") + "\n";
|
||||
}
|
||||
|
||||
const rot = lage.rote.length;
|
||||
zeilen.push(rot
|
||||
? `## ${rot} Prüfung${rot === 1 ? "" : "en"} ${rot === 1 ? "ist" : "sind"} rot`
|
||||
: "## Alles in Ordnung");
|
||||
zeilen.push("");
|
||||
zeilen.push(`Gemessen am **${zeitWort(jetzt)}**, Dauer ${minuten} Minuten.`);
|
||||
zeilen.push("");
|
||||
zeilen.push("| | jetzt | vorher |");
|
||||
zeilen.push("|---|---|---|");
|
||||
zeilen.push(`| Dateien in Ordnung | ${lage.dateienOk} von ${lage.dateien} | `
|
||||
+ `${vorher ? `${vorher.dateienOk} von ${vorher.dateien}` : "—"} |`);
|
||||
zeilen.push(`| Einzelprüfungen bestanden | ${lage.punkteOk} von ${lage.punkte} | `
|
||||
+ `${vorher ? `${vorher.punkteOk} von ${vorher.punkte}` : "—"} |`);
|
||||
zeilen.push("");
|
||||
|
||||
/* EIN RÜCKGANG DER ANZAHL IST EIN BEFUND, AUCH WENN ALLES GRÜN IST.
|
||||
Am 28.08.2026 meldete ein Lauf 789 statt 804 Prüfungen und war
|
||||
dabei grün: Acht Prüfungen je Gerät waren stillschweigend
|
||||
weggefallen -- ausgerechnet die, die die wichtigste Funktion
|
||||
absichern. Wer nur "bestanden" liest, hätte sie ab da dauerhaft
|
||||
ungeprüft gelassen. */
|
||||
if (vorher && lage.punkte != null && vorher.punkte != null
|
||||
&& lage.punkte < vorher.punkte) {
|
||||
zeilen.push(`> **Es werden ${vorher.punkte - lage.punkte} Prüfungen WENIGER gemacht als beim letzten Lauf.**`);
|
||||
zeilen.push("> Das ist auch dann ein Befund, wenn alles grün ist: Eine Prüfung, die");
|
||||
zeilen.push("> still nicht mehr läuft, beweist nichts — und niemandem fällt es auf.");
|
||||
zeilen.push("");
|
||||
}
|
||||
if (vorher && lage.dateien != null && vorher.dateien != null
|
||||
&& lage.dateien < vorher.dateien) {
|
||||
zeilen.push(`> **Es gibt ${vorher.dateien - lage.dateien} Prüfdatei(en) weniger als beim letzten Lauf.**`);
|
||||
zeilen.push("");
|
||||
}
|
||||
|
||||
if (rot) {
|
||||
zeilen.push("## Was rot ist");
|
||||
zeilen.push("");
|
||||
for (const r of lage.rote) {
|
||||
zeilen.push(`### \`${r.datei}\``);
|
||||
zeilen.push("");
|
||||
zeilen.push(r.erste ? `\`\`\`\n${r.erste}\n\`\`\`` : "*(keine Fehlerzeile gefunden)*");
|
||||
zeilen.push("");
|
||||
zeilen.push(`Einzeln nachsehen: \`node server/${r.datei}\``);
|
||||
zeilen.push("");
|
||||
}
|
||||
zeilen.push("**Rot heißt nicht automatisch kaputt.** Dreimal am 23.09.2026 war");
|
||||
zeilen.push("die Prüfung veraltet und das Programm richtig — einmal war es");
|
||||
zeilen.push("umgekehrt und ein Rollenname stand offen im Netz. Erst nachsehen,");
|
||||
zeilen.push("dann entscheiden, wer recht hat.");
|
||||
} else {
|
||||
zeilen.push("Keine Datei ist rot. Der nächste Lauf kommt in der nächsten Nacht.");
|
||||
}
|
||||
zeilen.push("");
|
||||
return zeilen.join("\n") + "\n";
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
Los
|
||||
--------------------------------------------------------------------- */
|
||||
const vorher = standLesen();
|
||||
const erzwingen = process.argv.includes("--jetzt");
|
||||
|
||||
const schloss = erzwingen ? { ok: true } : schlossNehmen();
|
||||
if (!schloss.ok) {
|
||||
/* DER DRITTE AUSGANG. Kein Fehler -- nur: hier war nichts zu messen. */
|
||||
mkdirSync(dirname(NOTIZ), { recursive: true });
|
||||
writeFileSync(NOTIZ, notizSchreiben({ vorher, konnteNicht: schloss.grund }), "utf8");
|
||||
console.log("Konnte nicht nachsehen: " + schloss.grund);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let ergebnis;
|
||||
try {
|
||||
console.log("Nachtlauf gestartet — das dauert. " + zeitWort());
|
||||
ergebnis = await laufen();
|
||||
} finally {
|
||||
if (!erzwingen) schlossAbgeben();
|
||||
}
|
||||
|
||||
if (!ergebnis.ging) {
|
||||
mkdirSync(dirname(NOTIZ), { recursive: true });
|
||||
writeFileSync(NOTIZ, notizSchreiben({
|
||||
vorher, konnteNicht: `Der Läufer ließ sich nicht starten: ${ergebnis.grund}`,
|
||||
}), "utf8");
|
||||
console.log("Konnte nicht nachsehen: " + ergebnis.grund);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const lage = auswerten(ergebnis.aus);
|
||||
|
||||
/* KONNTE DER LAUF ÜBERHAUPT ETWAS MESSEN? Steht keine Zahl in der
|
||||
Ausgabe, ist etwas anderes schiefgegangen als eine rote Prüfung --
|
||||
und "0 von 0 in Ordnung" wäre die beruhigendste Lüge von allen. */
|
||||
if (lage.dateien == null || lage.punkte == null || lage.punkte === 0) {
|
||||
mkdirSync(dirname(NOTIZ), { recursive: true });
|
||||
writeFileSync(NOTIZ, notizSchreiben({
|
||||
vorher,
|
||||
konnteNicht: "Der Lauf hat keine einzige Zahl geliefert — vermutlich ist er"
|
||||
+ " abgebrochen, bevor er anfangen konnte.\n\nLetzte Ausgabe:\n\n```\n"
|
||||
+ ergebnis.aus.slice(-700) + "\n```",
|
||||
}), "utf8");
|
||||
console.log("Konnte nicht nachsehen: keine Zahlen in der Ausgabe.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
mkdirSync(dirname(NOTIZ), { recursive: true });
|
||||
writeFileSync(NOTIZ, notizSchreiben({ lage, vorher, minuten: ergebnis.minuten }), "utf8");
|
||||
|
||||
/* Der Stand für den nächsten Vergleich -- erst NACH der Notiz, damit
|
||||
ein Absturz beim Schreiben nicht den alten Vergleichswert wegnimmt. */
|
||||
writeFileSync(ZUSTAND, JSON.stringify({
|
||||
wann: zeitWort(), zeit: Date.now(),
|
||||
dateien: lage.dateien, dateienOk: lage.dateienOk,
|
||||
punkte: lage.punkte, punkteOk: lage.punkteOk,
|
||||
rote: lage.rote.map((r) => r.datei),
|
||||
}, null, 1), "utf8");
|
||||
|
||||
/* Die ganze Ausgabe daneben -- für den Fall, dass die Notiz nicht
|
||||
reicht. Sie wird jedes Mal überschrieben; zwei Läufe zu vergleichen
|
||||
ist Sache der Zahlen oben, nicht eines wachsenden Protokolls. */
|
||||
writeFileSync(join(WURZEL, "tools", ".nachtlauf-letzte-ausgabe.txt"), ergebnis.aus, "utf8");
|
||||
|
||||
console.log(`Fertig nach ${ergebnis.minuten} min: `
|
||||
+ `${lage.dateienOk} von ${lage.dateien} Dateien, `
|
||||
+ `${lage.punkteOk} von ${lage.punkte} Einzelprüfungen.`);
|
||||
console.log(`Ergebnis steht in: ${NOTIZ}`);
|
||||
process.exit(lage.rote.length ? 1 : 0);
|
||||
@@ -2483,7 +2483,7 @@
|
||||
Tönungen gar nicht sieht. Es steht in derselben Farbe wie die
|
||||
Schrift -- eine zweite Farbe wäre dieselbe Wette wie oben. */
|
||||
.chat-erwaehnung[data-rudel="ja"]::before {
|
||||
content: "C9 | ||||