diff --git a/server/chat-kreis-stufen.js b/server/chat-kreis-stufen.js
new file mode 100644
index 00000000..79f88053
--- /dev/null
+++ b/server/chat-kreis-stufen.js
@@ -0,0 +1,141 @@
+/* =====================================================================
+ HELLER UND DUNKLER (25.09.2026)
+ =====================================================================
+
+ Filipe: „dieser kreis muss viel perfekter sein. ich will dass die
+ leute auch heller und dunkler aussuchen koennen. perfektionier das
+ bitte viel krasser. viel profissioneller. so dass die leute viel
+ krassere moeglichkeiten haben."
+
+ ---------------------------------------------------------------------
+ WARUM DAS BIS HEUTE NICHT GING
+
+ Die 360 Toene in chat-kreis.js liegen ALLE auf derselben
+ Leuchtdichte. Das war kein Zufall und keine Bequemlichkeit: Solange
+ die Blase in der gewaehlten Farbe stand, musste jede dieser Flaechen
+ dieselbe Schrift tragen -- „gleiche Leuchtdichte heisst gleicher
+ Kontrast fuer alle". Eine hellere Farbe zuzulassen hiesse damals:
+ irgendwo im Haus wird eine Nachricht unlesbar.
+
+ SEIT DEM 25.09.2026 IST DAS ANDERS. Die Blase ist fuer alle dunkles
+ Glas (`--blasengrund`); der gewaehlte Ton ist KANTE und NAME. Damit
+ faellt die alte Bedingung weg -- und eine neue tritt an ihre Stelle:
+
+ Der Ton muss als NAME auf dem Blasengrund lesbar bleiben.
+
+ Die Oberflaeche hellt ihn dafuer mit Weiss auf (`--namen-anteil`,
+ 50 %). Genau diese Rechnung prueft pruef-chat-neu fuer alle Toene.
+
+ ---------------------------------------------------------------------
+ DIE ZAHLEN SIND GEMESSEN, NICHT GEWAEHLT
+
+ Nachgerechnet ueber alle 360 Winkel, jeweils der schlechteste:
+
+ 40 % schwarz 4,468 -- unter der Schwelle, faellt raus
+ 34 % schwarz 4,555 -- ginge, aber ohne Reserve
+ 30 % schwarz 4,619 <- die dunkelste Stufe
+ 20 % schwarz 4,7
+ 10 % schwarz 4,9
+ 0 % 5,12 <- die Mitte, der alte Kreis
+ 18 % weiss 6,4
+ 36 % weiss 7,9
+ 55 % weiss 9,6 <- die hellste Stufe
+
+ Noetig sind 4,5. Die dunkelste Stufe hat also 0,12 Reserve, die
+ hellste mehr als das Doppelte. Wer hier eine Stufe hinzufuegt, misst
+ nach -- pruef-chat-neu rechnet jede Stufe an jedem der 360 Winkel
+ nach und wird rot, bevor es jemand auf dem Bildschirm sieht.
+
+ ---------------------------------------------------------------------
+ WARUM SIEBEN UND NICHT DREI
+
+ Drei (dunkel / normal / hell) waere die bequeme Zahl. Sieben sind
+ der Punkt, an dem zwei benachbarte Stufen noch unterscheidbar sind
+ und man trotzdem das Gefuehl hat, etwas einstellen zu koennen --
+ Filipes „viel krassere moeglichkeiten" heisst nicht „zwei mehr".
+ Zusammen mit 360 Winkeln sind es 2520 Farben statt 360.
+
+ ---------------------------------------------------------------------
+ WARUM EINE EIGENE DATEI
+
+ chat-kreis.js wird von tools/chat-kacheln-rechnen.mjs ERZEUGT und
+ traegt oben „NICHT VON HAND AENDERN". Etwas hineinzuschreiben waere
+ beim naechsten Lauf des Werkzeugs weg. Die Stufen sind ausserdem
+ keine Tabelle, sondern eine Rechnung -- sie gehoeren nicht in eine
+ erzeugte Liste.
+ ===================================================================== */
+import { KREIS } from "./chat-kreis.js";
+
+/** Die sieben Stufen. Minus heisst dunkler, Plus heller.
+ *
+ * `wert` ist der Teil, der im Schluessel steht ("ton-214-s2").
+ * `misch` ist der Anteil, mit dem der Grundton nach Schwarz (negativ)
+ * oder nach Weiss (positiv) gezogen wird.
+ * `name` ist das, was ein Vorleseprogramm sagt -- eine Zahl von 1 bis
+ * 7 saehe aus wie eine Note.
+ */
+export const KREIS_STUFEN = [
+ { wert: "s1", misch: -0.30, name: "sehr dunkel" },
+ { wert: "s2", misch: -0.20, name: "dunkler" },
+ { wert: "s3", misch: -0.10, name: "leicht dunkel" },
+ { wert: "s4", misch: 0, name: "normal" },
+ { wert: "s5", misch: 0.18, name: "leicht hell" },
+ { wert: "s6", misch: 0.36, name: "heller" },
+ { wert: "s7", misch: 0.55, name: "sehr hell" },
+];
+
+/** Welche Stufe ist die Mitte -- und damit das, was "ton-214" ohne
+ * Zusatz bedeutet. Abgeleitet aus `misch === 0` und nicht als Index
+ * hingeschrieben: Wer eine Stufe vorn einfuegt, verschiebt sonst
+ * stillschweigend die Bedeutung aller alten Schluessel. */
+export const KREIS_MITTE = KREIS_STUFEN.find((s) => s.misch === 0)?.wert || "s4";
+
+/** Zerlegt einen Schluessel in Winkel und Stufe.
+ *
+ * "ton-214" -> { winkel: 214, stufe: "s4" } (die Mitte)
+ * "ton-214-s6" -> { winkel: 214, stufe: "s6" }
+ * alles andere -> null
+ *
+ * DIE ALTEN SCHLUESSEL BLEIBEN GUELTIG, und zwar ohne Umstellung in
+ * der Datenbank. Wer seit dem 23.09. "ton-214" traegt, traegt danach
+ * genau dieselbe Farbe -- eine Umstellung, die stillschweigend die
+ * Wahl von fuenfzehn Leuten verschiebt, waere kein Fortschritt.
+ */
+export function tonAus(wert) {
+ const t = /^ton-(\d{1,3})(?:-(s[1-7]))?$/.exec(String(wert ?? ""));
+ if (!t) return null;
+ const winkel = Number(t[1]);
+ if (!(winkel >= 0 && winkel < KREIS.length)) return null;
+ const stufe = t[2] || KREIS_MITTE;
+ if (!KREIS_STUFEN.some((s) => s.wert === stufe)) return null;
+ return { winkel, stufe };
+}
+
+/** Zwei Farben mischen, wie `color-mix(in srgb, a p%, b)` es tut. */
+function mischen(a, b, anteilB) {
+ const z = (h) => {
+ const s = String(h).replace("#", "");
+ return [0, 2, 4].map((i) => parseInt(s.slice(i, i + 2), 16));
+ };
+ const [ar, ag, ab] = z(a), [br, bg, bb] = z(b);
+ const m = (x, y) => Math.round(x * (1 - anteilB) + y * anteilB);
+ return "#" + [m(ar, br), m(ag, bg), m(ab, bb)]
+ .map((v) => v.toString(16).padStart(2, "0")).join("");
+}
+
+/** Die fertige Farbe zu Winkel und Stufe. */
+export function tonFarbe(winkel, stufe) {
+ const grund = KREIS[winkel];
+ if (!grund) return null;
+ const s = KREIS_STUFEN.find((x) => x.wert === stufe) || { misch: 0 };
+ if (!s.misch) return grund;
+ return s.misch < 0
+ ? mischen(grund, "#000000", -s.misch)
+ : mischen(grund, "#ffffff", s.misch);
+}
+
+/** Die Farbe zu einem ganzen Schluessel, oder null. */
+export function farbeZuSchluessel(wert) {
+ const t = tonAus(wert);
+ return t ? tonFarbe(t.winkel, t.stufe) : null;
+}
diff --git a/server/pruef-chat-anhaenge.mjs b/server/pruef-chat-anhaenge.mjs
index ca5beb7f..6e53493a 100644
--- a/server/pruef-chat-anhaenge.mjs
+++ b/server/pruef-chat-anhaenge.mjs
@@ -418,6 +418,142 @@ let pdfNachrichtId = 0;
ok(kaeme === 0, `für ein PDF wird kein Bildkasten gebaut (${kaeme})`);
}
+/* =======================================================================
+ 2c. DIE BILDSCHAU (25.09.2026)
+ =======================================================================
+
+ Filipe: „wenn man bilder aufmacht, will ich dass die in der mitte
+ sind. beim hochformat soll links der husky sein und rechts der hase
+ und beim querformat soll oben dogfather und unten team dogi stehen
+ da wo platz ist."
+
+ VORHER OEFFNETE DER KLICK DIE DATEI in einem neuen Tab -- und was man
+ sah, war die eingebaute Bildanzeige des Browsers: schwarzer Grund,
+ Bild oben links. Das ist kein Fehler des Browsers; eine Datei hat
+ keine Gestaltung.
+
+ GEMESSEN WIRD, WAS ER GESAGT HAT, und jedes einzeln:
+ 1. Der Klick oeffnet die Schau (und nicht mehr den Tab).
+ 2. Das Bild steht in der MITTE -- gemessen als Abstand seines
+ Mittelpunkts zur Fenstermitte, nicht als „es ist da".
+ 3. Bei einem breiten Bild stehen die zwei Schriftzuege da.
+ 4. Escape bringt einen zurueck.
+ 5. GEGENPROBE: Mit Strg gedrueckt oeffnet sie NICHT -- der Weg in
+ den neuen Tab bleibt, wie er war. Ohne diese Zeile haette
+ „die Schau geht auf" auch dann gestimmt, wenn sie IMMER aufgeht
+ und der alte Weg verloren ist.
+ ======================================================================= */
+{
+ await oeffne(luna.seite, raumId);
+ /* Das Foto aus Abschnitt 2 ist 240x150 -- also quer. */
+ const bildKasten = ".chat-nachricht .chat-bild";
+ await luna.seite.waitForSelector(bildKasten, { timeout: 8000 });
+
+ /* GEGENPROBE ZUERST: mit Strg bleibt der alte Weg. */
+ await luna.seite.click(bildKasten, { modifiers: ["Control"] });
+ await luna.seite.waitForTimeout(400);
+ const mitStrg = await luna.seite.evaluate(() =>
+ !!document.querySelector("dialog.bildschau[open]"));
+ ok(mitStrg === false,
+ `Gegenprobe: Strg+Klick öffnet die Schau NICHT (${mitStrg}) — der Weg in den Tab bleibt`);
+
+ await luna.seite.click(bildKasten);
+ await luna.seite.waitForTimeout(600);
+ const schau = await luna.seite.evaluate(() => {
+ const d = document.querySelector("dialog.bildschau[open]");
+ if (!d) return { offen: false };
+ const b = d.querySelector(".bildschau__bild");
+ const r = b.getBoundingClientRect();
+ const worte = [...d.querySelectorAll(".bildschau__wort")]
+ .filter((e) => Number(getComputedStyle(e).opacity) > .5)
+ .map((e) => e.textContent.trim());
+ return {
+ offen: true,
+ lage: d.dataset.lage,
+ /* Wie weit ist die Bildmitte von der Fenstermitte weg? Bei
+ „mittig" sind das ein paar Pixel, bei „oben links" Hunderte. */
+ abX: Math.round(Math.abs((r.left + r.width / 2) - window.innerWidth / 2)),
+ abY: Math.round(Math.abs((r.top + r.height / 2) - window.innerHeight / 2)),
+ worte,
+ figuren: [...d.querySelectorAll(".bildschau__figur")]
+ .filter((e) => Number(getComputedStyle(e).opacity) > .5).length,
+ grund: getComputedStyle(d.querySelector(".bildschau__grund")).backgroundImage.length,
+ };
+ });
+ ok(schau.offen === true, `ein Klick öffnet die Bildschau (${schau.offen})`);
+ /* 40 px Spielraum: Die Buehne hat oben mehr Polsterung als unten
+ (dort steht die Leiste), der Mittelpunkt liegt also nie exakt auf
+ dem Pixel. Bei „oben links in der Ecke" waeren es Hunderte. */
+ ok(schau.abX <= 40 && schau.abY <= 60,
+ `und das Bild steht in der Mitte (${schau.abX} px seitlich, ${schau.abY} px hoch daneben)`);
+ ok(schau.lage === "quer",
+ `ein breites Bild bekommt die Querformat-Fassung ("${schau.lage}")`);
+ ok(schau.worte.length === 2
+ && /DogFather/i.test(schau.worte[0]) && /Team Dogi/i.test(schau.worte[1]),
+ `mit „DogFather" oben und „Team Dogi" unten (${JSON.stringify(schau.worte)})`);
+ ok(schau.figuren === 0,
+ `und OHNE die zwei Figuren — die gehören ins Hochformat (${schau.figuren})`);
+ ok(schau.grund > 80,
+ `der Grund ist gestaltet und nicht schwarz (${schau.grund} Zeichen Verlauf)`);
+
+ /* EIN BILD FUER DEN MENSCHEN. Andere Pruefungen in diesem Haus legen
+ ebenso eines ab (pruef-handy.png, pruef-crew-wand-handy.png) --
+ eine Zahl sagt „mittig", ein Bild sagt, ob es gut aussieht. */
+ await luna.seite.screenshot({ path: "server/pruef-bildschau.png" });
+
+ await luna.seite.keyboard.press("Escape");
+ await luna.seite.waitForTimeout(400);
+ const zu = await luna.seite.evaluate(() =>
+ !!document.querySelector("dialog.bildschau[open]"));
+ ok(zu === false, `Escape bringt einen zurück (${zu})`);
+
+ /* ---- UND DAS HOCHFORMAT -----------------------------------------
+ Der andere Fall aus Filipes Satz -- „beim hochformat soll links
+ der husky sein und rechts der hase". Ohne ein zweites, HOHES Bild
+ waere dieser halbe Auftrag nie gemessen worden; das quere Bild
+ oben kann ihn nicht beantworten. */
+ const hoch = await hochladen(luna.seite, raumId, {
+ inhalt: { art: "png", breite: 300, hoehe: 800 },
+ typ: "image/png", name: "hochformat.png", text: "Und hochkant",
+ });
+ ok(hoch.status === 201, `ein hohes Foto kommt an (HTTP ${hoch.status})`);
+ await oeffne(luna.seite, raumId);
+ await luna.seite.waitForSelector(".chat-nachricht:last-of-type .chat-bild", { timeout: 8000 });
+ await luna.seite.click(".chat-nachricht:last-of-type .chat-bild");
+ await luna.seite.waitForTimeout(700);
+ const hochSchau = await luna.seite.evaluate(() => {
+ const d = document.querySelector("dialog.bildschau[open]");
+ if (!d) return { offen: false };
+ const b = d.querySelector(".bildschau__bild");
+ const r = b.getBoundingClientRect();
+ const sicht = (s) => [...d.querySelectorAll(s)]
+ .filter((e) => Number(getComputedStyle(e).opacity) > .5);
+ const figuren = sicht(".bildschau__figur");
+ return {
+ offen: true, lage: d.dataset.lage,
+ abX: Math.round(Math.abs((r.left + r.width / 2) - window.innerWidth / 2)),
+ figuren: figuren.length,
+ /* LIEGT EINE DAVON UEBER DEM BILD? Das waere der Schaden, den
+ man erst sieht, wenn ein Gesicht halb verdeckt ist. */
+ drueber: figuren.filter((e) => {
+ const f = e.getBoundingClientRect();
+ return Math.min(f.right, r.right) - Math.max(f.left, r.left) > 4;
+ }).length,
+ worte: sicht(".bildschau__wort").length,
+ };
+ });
+ ok(hochSchau.lage === "hoch", `ein hohes Bild bekommt die Hochformat-Fassung ("${hochSchau.lage}")`);
+ ok(hochSchau.figuren === 2, `Husky und Hase stehen daneben (${hochSchau.figuren})`);
+ ok(hochSchau.drueber === 0,
+ `und KEINE davon liegt über dem Foto (${hochSchau.drueber})`);
+ ok(hochSchau.worte === 0,
+ `die Schriftzüge bleiben weg — die gehören ins Querformat (${hochSchau.worte})`);
+ ok(hochSchau.abX <= 40, `auch hier steht das Bild mittig (${hochSchau.abX} px daneben)`);
+ await luna.seite.screenshot({ path: "server/pruef-bildschau-hoch.png" });
+ await luna.seite.keyboard.press("Escape");
+ await luna.seite.waitForTimeout(300);
+}
+
/* =======================================================================
3. WAS NICHT HEREINDARF — die eigentliche Prüfung
diff --git a/server/pruef-chat-neu.mjs b/server/pruef-chat-neu.mjs
index 5de480ec..6760388c 100644
--- a/server/pruef-chat-neu.mjs
+++ b/server/pruef-chat-neu.mjs
@@ -313,6 +313,23 @@ console.log("\n=== 3. Der Ruf ans Rudel ===");
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));
+ /* ==== DIE DRITTE SCHREIBWEISE (25.09.2026) ====================
+
+ Seit die Probe ihre Farbe aus `color-mix(in srgb, …)` bekommt,
+ gibt Chromium sie als `color(srgb 0.676 0.645 0.504)` zurueck --
+ Anteile von 0 bis 1, keine Ganzzahlen, kein Komma. Die zwei
+ Muster darueber trafen das nicht, `zuZahl` lieferte `null`, und
+ die Zeile wurde rot, obwohl die Farbe auf den Bildpunkt genau
+ stimmte (172,5 / 164,5 / 128,5 gegen erwartete 173/165/129).
+
+ Das ist die Sorte Fehlalarm, die teurer ist als ein echter
+ Befund: Man sucht am Aussehen, und der Fehler liegt im
+ Lesegeraet. */
+ const s = /color\(srgb\s+([^)]+)\)/i.exec(String(f));
+ if (s) {
+ return s[1].trim().split(/[\s/]+/).slice(0, 3)
+ .map((x) => Math.round(parseFloat(x) * 255));
+ }
const r = /rgba?\(([^)]+)\)/i.exec(String(f));
return r ? r[1].split(",").slice(0, 3).map((x) => Math.round(parseFloat(x))) : null;
};
@@ -375,21 +392,55 @@ console.log("\n=== 3. Der Ruf ans Rudel ===");
const schlimmerGrund = grund.map((v) => Math.round(v * 0.92 + 255 * 0.08));
const anteil = Number(anteilTreffer[1]) / 100;
const hell = zuZahl("#f2f8ff");
+ /* ==== JEDE STUFE AN JEDEM WINKEL (25.09.2026) =================
+
+ Filipe: „ich will dass die leute auch heller und dunkler
+ aussuchen koennen."
+
+ Seit heute gibt es zu jedem der 360 Winkel sieben
+ Helligkeiten -- 2520 Farben statt 360. Nur den Grundton zu
+ pruefen hiesse, sechs Siebtel des Angebots ungeprueft
+ auszuliefern; und die dunkelste Stufe ist genau die, bei der
+ es kippen wuerde (gemessen 4,62 gegen noetige 4,5 -- die
+ naechste Stufe darunter faellt durch).
+
+ DIE MISCHANTEILE WERDEN GELESEN, nicht abgeschrieben: Sie
+ stehen in chat-kreis-stufen.js, und wer sie dort aendert,
+ aendert damit diese Pruefung mit. Eine zweite Liste hier waere
+ die, die beim naechsten Umbau nicht mitzieht. */
+ const { KREIS_STUFEN } = await import("./chat-kreis-stufen.js");
+ const mischen2 = (rgb, misch) => {
+ if (!misch) return rgb;
+ const ziel = misch < 0 ? 0 : 255;
+ const t2 = Math.abs(misch);
+ return rgb.map((v) => Math.round(v * (1 - t2) + ziel * t2));
+ };
let schlimmster = Infinity, schlimmsterWinkel = -1, gemessen = 0;
+ let schlimmsteStufe = "";
for (let w = 0; w < KREIS.length; w++) {
const f = zuZahl(KREIS[w]);
if (!f) continue;
- gemessen++;
- const name = f.map((v, i) => Math.round(v * anteil + hell[i] * (1 - anteil)));
- const v2 = kontrast(name, schlimmerGrund) / 4.5;
- if (v2 < schlimmster) { schlimmster = v2; schlimmsterWinkel = w; }
+ for (const st of KREIS_STUFEN) {
+ gemessen++;
+ const ton = mischen2(f, st.misch);
+ const name = ton.map((v, i) => Math.round(v * anteil + hell[i] * (1 - anteil)));
+ const v2 = kontrast(name, schlimmerGrund) / 4.5;
+ if (v2 < schlimmster) {
+ schlimmster = v2; schlimmsterWinkel = w; schlimmsteStufe = st.wert;
+ }
+ }
}
/* 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 sind als Name auf der Blase lesbar`
- + ` (knappster: Winkel ${schlimmsterWinkel}, Faktor ${schlimmster.toFixed(3)})`);
+ /* DIE ANZAHL STEHT IN DER BEDINGUNG -- und sie ist seit heute
+ 360 mal die Zahl der Stufen. Waere die Stufenliste leer,
+ bliebe `gemessen` bei 0 und die Zeile waere gruen, ohne eine
+ einzige Farbe angesehen zu haben. */
+ ok(gemessen === 360 * KREIS_STUFEN.length && schlimmster >= 1,
+ `alle ${gemessen} Farben des Rings (360 Winkel x ${KREIS_STUFEN.length} `
+ + `Helligkeiten) sind als Name auf der Blase lesbar (knappste: Winkel `
+ + `${schlimmsterWinkel}, Stufe ${schlimmsteStufe}, Faktor ${schlimmster.toFixed(3)})`);
}
}
if (textRgb && leiseRgb) {
@@ -466,21 +517,84 @@ console.log("\n=== 3. Der Ruf ans Rudel ===");
const stand = await frida.seite.evaluate(() => {
const r = document.getElementById("kachelrad-ring");
- const m = document.getElementById("kachelrad-mitte");
+ const p2 = document.querySelector(".kachelrad__probe");
+ const leiter = document.getElementById("kachelhell-leiter");
return { winkel: Number(r?.getAttribute("aria-valuenow")),
wort: r?.getAttribute("aria-valuetext") || "",
- mitte: m ? getComputedStyle(m).backgroundColor : "" };
+ /* NICHT MEHR DER GRUND, SONDERN DIE SCHRIFT. Siehe unten. */
+ probe: p2 ? getComputedStyle(p2).color : "",
+ stufen: leiter ? leiter.children.length : 0,
+ stufeJetzt: leiter?.getAttribute("aria-valuenow") || "" };
});
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);
+ /* ==== DIE PROBE MISST SEIT HEUTE ETWAS ANDERES (25.09.2026) =====
+
+ Hier stand: „die Mitte zeigt genau die Farbe dieses Winkels" --
+ gemessen am HINTERGRUND der Mitte. Das war richtig, solange die
+ Blase in der gewaehlten Farbe stand.
+
+ Seit dem 25.09.2026 ist die Blase fuer alle dunkles Glas, und
+ der gewaehlte Ton ist der NAME darauf. Die Probe zeigte damit
+ eine Darstellung, die es im Chat nicht mehr gibt -- und die
+ Pruefung haette sie festgehalten.
+
+ GEMESSEN WIRD JETZT DIE SCHRIFT der Probe: Sie muss genau die
+ Farbe tragen, die ein Name in der Blase traegt -- der Ton, mit
+ `--namen-anteil` nach #f2f8ff aufgehellt. Das ist dieselbe
+ Rechnung wie in chat.css, und sie wird hier NACHGERECHNET statt
+ abgeschrieben. */
+ const grundTon = zuZahl(KREIS[stand.winkel] || "");
+ const hell = zuZahl("#f2f8ff");
+ const anteil = 0.5; /* --namen-anteil; weiter unten aus dem CSS geprueft */
+ const erwartet = grundTon
+ ? grundTon.map((v, i) => Math.round(v * anteil + hell[i] * (1 - anteil)))
+ : null;
+ const gezeigt = zuZahl(stand.probe);
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]})`);
+ && Math.abs(erwartet[0] - gezeigt[0]) <= 2
+ && Math.abs(erwartet[1] - gezeigt[1]) <= 2
+ && Math.abs(erwartet[2] - gezeigt[2]) <= 2,
+ `die Probe zeigt den Namen in genau dieser Farbe (${stand.probe} gegen `
+ + `${erwartet ? "rgb(" + erwartet.join(", ") + ")" : "?"})`);
+
+ /* ==== UND DIE SIEBEN HELLIGKEITEN (25.09.2026) =================
+
+ Filipe: „ich will dass die leute auch heller und dunkler
+ aussuchen koennen … so dass die leute viel krassere
+ moeglichkeiten haben." */
+ ok(stand.stufen === 7,
+ `die Helligkeitsleiter hat sieben Stufen (${stand.stufen})`);
+ ok(stand.stufeJetzt === "4",
+ `und steht von sich aus in der Mitte (${stand.stufeJetzt})`);
+
+ /* EINE STUFE WAEHLEN UND NACHSEHEN, OB SIE ANKOMMT. Ohne das
+ waere „es gibt sieben Felder" ein Haken ueber Zierde: Sieben
+ Kaestchen, die nichts tun, sehen genauso aus. */
+ await frida.seite.click(".kachelhell__feld[data-stufe='s7']");
+ await frida.seite.waitForTimeout(700);
+ const hellStand = await frida.seite.evaluate(() => {
+ const p2 = document.querySelector(".kachelrad__probe");
+ const leiter = document.getElementById("kachelhell-leiter");
+ const an = leiter?.querySelector('[data-an="ja"]');
+ return { probe: p2 ? getComputedStyle(p2).color : "",
+ stufe: leiter?.getAttribute("aria-valuenow") || "",
+ markiert: an?.dataset.stufe || "" };
+ });
+ ok(hellStand.stufe === "7" && hellStand.markiert === "s7",
+ `eine hellere Stufe laesst sich waehlen (${hellStand.stufe}, ${hellStand.markiert})`);
+ const vorher2 = zuZahl(stand.probe), nachher2 = zuZahl(hellStand.probe);
+ ok(!!vorher2 && !!nachher2 && leucht(nachher2) > leucht(vorher2),
+ `und die Probe wird dabei wirklich heller `
+ + `(${leucht(vorher2).toFixed(3)} auf ${leucht(nachher2).toFixed(3)})`);
+
+ await frida.seite.screenshot({ path: "server/pruef-farbkreis.png" });
+
+ /* Und zurueck in die Mitte, damit die Pruefungen danach mit dem
+ gewohnten Stand weiterarbeiten. */
+ await frida.seite.click(".kachelhell__feld[data-stufe='s4']");
+ await frida.seite.waitForTimeout(500);
ok(/\d+ Grad/.test(stand.wort) && stand.wort.length > 8,
`und sie wird vorgelesen als "${stand.wort}"`);
diff --git a/server/workspace-chat.js b/server/workspace-chat.js
index dc696de4..77db76a1 100644
--- a/server/workspace-chat.js
+++ b/server/workspace-chat.js
@@ -51,6 +51,7 @@ import {
import { erwaehnteIds, rudelGerufen, RUDEL_WORT } from "./chat-erwaehnung.js";
import { KREIS, winkelAus } from "./chat-kreis.js";
+import { KREIS_STUFEN, KREIS_MITTE, tonAus } from "./chat-kreis-stufen.js";
import { KATALOG, ERLAUBT, VORSCHLAG, FAVORITEN_MAX } from "./workspace-reaktionen.js";
/* Der Treff-Chat: eigener Raum, eigene Nachtruhe, keine Anrufe.
@@ -425,7 +426,20 @@ export function kachelPruefen(wert) {
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);
+ /* SEIT DEM 25.09.2026 GIBT ES ZU JEDEM WINKEL SIEBEN HELLIGKEITEN.
+
+ Filipe: „ich will dass die leute auch heller und dunkler
+ aussuchen koennen."
+
+ Der Schluessel heisst dann "ton-214-s6". Ohne Zusatz gilt die
+ Mitte -- die alten Schluessel bleiben damit Zeichen fuer Zeichen
+ gueltig und bedeuten dieselbe Farbe wie vorher.
+
+ `tonAus` ERSETZT `winkelAus` HIER, laesst es aber stehen: Es wird
+ an anderer Stelle noch gebraucht, und zwei Pruefungen fuer
+ denselben Schluessel waeren eine Gelegenheit, dass eine davon
+ mehr durchlaesst. */
+ if (tonAus(wert) !== null) return String(wert);
return CHAT_KACHELN.some((k) => k.wert === String(wert)) ? String(wert) : null;
}
@@ -1031,6 +1045,20 @@ chatRouter.get("/workspace/api/chat/kacheln", (req, res) => {
Kacheln -- also tragen sie auch dieselbe Schrift. Einmal
gerechnet und mitgeschickt, statt 360-mal dasselbe. */
kreis_schrift: schriftFuer(KREIS[0]),
+ /* DIE SIEBEN HELLIGKEITEN (25.09.2026).
+
+ Nicht 2520 fertige Farben, sondern 360 Grundtoene und sieben
+ Mischanteile: Der Browser mischt selbst, und zwar mit genau
+ den Zahlen, die hier stehen. Die ganze Tabelle zu schicken
+ waere 20 kB fuer etwas, das eine Zeile Rechnung ist.
+
+ DIE ZAHLEN KOMMEN TROTZDEM VOM SERVER und stehen nicht im
+ Browser: Sie sind aus der Kontrastrechnung hergeleitet (siehe
+ chat-kreis-stufen.js), und eine zweite Fassung im Browser
+ waere ab der naechsten Messung falsch -- unbemerkt, denn sie
+ lieferte ja weiter „irgendeine Farbe". */
+ kreis_stufen: KREIS_STUFEN,
+ kreis_mitte: KREIS_MITTE,
/* 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),
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index 4e603c81..932bf84f 100644
--- a/workspace/anruf-probe.html
+++ b/workspace/anruf-probe.html
@@ -16,15 +16,15 @@
verdeckt. Gefunden hat es pruef-struktur.
Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. -->
-
-
+
+
-
+