Files
dogfather-universe/tools/kachel-farbe-einzeln.mjs
T
DogFatherGitandClaude Opus 5 9f5dbb42e3 Support: melden mit Bild, und für die Leitung ein Eingang
Filipe: „da fehlt auch eine support kachel. die jeder sieht. jeder
benutzen kann. jeder kann da probleme von der seite melden und
hinweisen. fotos mit schicken mit einem text. … so einfach wie
möglich, will nichts kompliziertes oder zu viel. kurz und knapp …
bei dogfather und der rechten hand soll die kachel anders gebaut sein
weil die sind die die sich um die probleme kümmern."

WARUM DAS NICHT DER VORHANDENE MELDEWEG IST. Es gibt ihn schon
(hilfe.html) und er sieht aehnlich aus. Drei Unterschiede machen ihn
zu etwas anderem:
  1. Er ist NICHT fuer alle -- in rechte.js steht
     `["gast","hand","admin"]`. Modis, Creator, Scouts, Manager und
     die linke Hand hatten gar keinen Weg, einen kaputten Knopf zu
     melden.
  2. Er kann keine Bilder. Bei „der Knopf tut nichts" ist ein
     Bildschirmfoto die halbe Antwort.
  3. Er ist bewusst schwer: strikter Wechsel, hoechstens drei offene
     Faelle, ein Betreff. Richtig bei einem Vorfall zwischen
     Menschen, zu viel fuer „das Datum steht falsch da".

Deshalb eigen und sehr kurz: EIN Feld, EIN Bild, fertig. Kein
Betreff, keine Kategorie, keine Dringlichkeitsstufe -- wer ein
Formular mit fuenf Feldern sieht, meldet den kleinen Fehler nicht,
und genau die kleinen erfaehrt sonst niemand.

DIE LEITUNG SIEHT ETWAS ANDERES: einen Eingang mit Zahlenband
(neu / in Arbeit / erledigt), drei Filtern und zwei Handgriffen --
„Ich kuemmere mich" und „Erledigt …". Zwei, nicht fuenf; eine
Zuweisung an eine Person und Prioritaeten waeren ein Ticketsystem
fuer zwei Menschen, die nebeneinander sitzen. Das Melde-Feld bleibt
auch fuer sie da, rutscht aber unter den Eingang.

Wer zumacht, schreibt einen Satz dazu -- der Melder sieht nur diesen
Satz, und ohne ihn weiss er nicht, ob etwas behoben wurde oder ob
niemand Zeit hatte. Beide Seiten bekommen eine Benachrichtigung.

UEBERNOMMEN STATT NEU ERFUNDEN:
  * `dateiErkennen` aus workspace-chat.js -- EXPORTIERT, nicht
    abgeschrieben. An ihr haengt die ganze Sicherheit der Uploads
    (der Typ kommt aus den ersten Bytes, nicht aus Name oder
    Content-Type), und eine zweite Fassung waere die, die beim
    naechsten Dateiformat vergessen wird.
  * Die zweigeteilte Sicht und `meldungFuer()` (gibt den Datensatz
    oder null zurueck, nie true/false) aus dem vertraulichen
    Meldeweg.
  * Der Upload-Weg (express.raw, Text im Kopf, Ordner unter
    DATEN_ORDNER, damit die Sicherungspruefung ihn findet).

DIE KACHEL BEKOMMT JEDER -- an EINER Stelle angehaengt: `bereicheFuer`
haengt sie an jede Rollenliste, statt sie in fuenf Listen
einzutragen. Genau so ist heute der Benachrichtigungs-Knopf auf 14
Seiten verschwunden. Dazu der Eintrag in rechte.js (ohne ihn
verschwindet die Kachel lautlos, `nurOffeneKacheln` filtert dagegen)
und in der Browser-Kachelliste fuer die Agentur-Rollen.

TON 44 GERECHNET, NICHT GEWAEHLT -- und dabei zweimal danebengelegen:
  * Ich habe erst einen eigenen Modus in kachel-farben.mjs gebaut.
    `tools/kachel-farbe-einzeln.mjs` gibt es aber seit dem 22.09. fuer
    genau diesen Zweck. Meine Dopplung ist wieder weg.
  * Von Hand hatte ich #bcdbff eingetragen. pruef-kachelfarben lehnte
    es ab (Buntheit 0,060, verlangt sind 0,12) -- und das vorhandene
    Werkzeug verteidigte es trotzdem, weil sein Abstand gut war. Ein
    Werkzeug, das einen regelwidrigen Zustand haelt, weil er zufaellig
    gut misst, behebt genau den Fehler nicht, fuer den es da ist.
    Behoben: Zuerst die Regeln, dann der Abstand. Ergebnis #fd6401,
    Abstand 0,0914 (engstes vorhandenes Paar: 0,0154).

DREI MESSFEHLER VON MIR, die wie schwere Befunde aussahen: `fetch`
verwirft den Host-Kopf (die Community kommt nur auf crew. herein),
die Community bestaetigt ihr Alter (`alter_ok`), und die Startroute
heisst /api/ich. Alle drei stehen in pruef-treff.mjs richtig.

Ein ECHTER Fehler kam dazu: support.html lud bereiche.js nicht, und
kopf.js stuerzte mit „Cannot read properties of undefined (reading
'LEITUNG')" ab -- sichtbar nur in der Browserkonsole. Und
pruef-css-klassen fand sofort, dass auch wahl.js fehlte.

Gemessen: pruef-support 45 Pruefungen, 0 Fehler (alle neun Rollen
kommen herein, jede sieht die Kachel, als Bild getarntes HTML wird
abgelehnt, eine fremde Meldung bleibt fremd, erledigt bleibt
erledigt). Dazu 24 Messungen am Bild in drei Groessen. Gruen:
pruef-kachelfarben (22), pruef-css-klassen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-24 01:32:55 +02:00

300 lines
13 KiB
JavaScript

/* =====================================================================
EINE KACHELFARBE NACHZIEHEN — ohne die anderen anzufassen (19.09.2026)
---------------------------------------------------------------------
WOZU ES DAS BRAUCHT, OBWOHL kachel-farben.mjs EXISTIERT
Das große Werkzeug rechnet ALLE Töne gegeneinander neu. Das ist die
richtige Rechnung und das falsche Mittel, wenn eine einzige Kachel
dazukommt: Am 19.09.2026 nachgemessen hätte ein Lauf für die 40.
Farbe alle 39 anderen verändert — jede Kachel im Haus bekäme eine
neue Farbe, ohne dass jemand danach gefragt hat.
Dieses Werkzeug macht das Gegenteil: Es hält alle vorhandenen Töne
fest und sucht für EINEN die Farbe, die am weitesten von allen
anderen entfernt ist.
Gemessen am selben Tag: Für die neue Kachel „Entwicklung“ fand es
einen Abstand von 0,0988 — fast das Dreifache des engsten Paares,
das im Haus ohnehin schon stand (0,0362 zwischen Ton 15 und 39).
Ein Gesamtneuanstrich war also gar nicht nötig.
---------------------------------------------------------------------
DIE VIER BEDINGUNGEN sind dieselben wie im großen Werkzeug:
1. LESBAR. Mindestens 4,5:1 gegen den dunklen Grund (#0a121e).
Nicht verhandelbar — eine Kachelfarbe trägt auch Text.
2. SPECIELL. Buntheit bis 0,30. Augenschonend heißt dunkles
Schema, kein Flackern — NICHT blass.
3. TRENNBAR. Der Abstand zur nächstliegenden vorhandenen Farbe
wird so groß wie möglich gemacht. Gerechnet in OKLab, weil
dort der Zahlenabstand dem entspricht, was das Auge sieht.
4. NICHT SCHLECHTER ALS VORHER. Findet sich nichts Besseres,
bleibt die alte Farbe stehen — und das Werkzeug sagt es.
---------------------------------------------------------------------
UND DIE FÜNFTE, DIE HIER DAZUKOMMT: ES MUSS ALLES LESEN KÖNNEN
Der erste Anlauf dieser Datei las 31 von 40 Tönen und rechnete
fröhlich weiter. Der Grund: Einstellige Nummern stehen in start.css
mit ZWEI Leerzeichen da (`[data-ton="1"] { …`), zur Ausrichtung.
Das Ergebnis war eine plausible, falsche Farbe — sie hätte mit einer
der neun ungelesenen kollidieren können, ohne dass es auffällt.
Deshalb bricht es jetzt ab, wenn die Zahl der gelesenen Töne nicht
zur Zahl der vorhandenen passt. Das ist der dritte Ausgang:
in Ordnung / nicht in Ordnung / KONNTE NICHT NACHSEHEN, mit Grund.
---------------------------------------------------------------------
AUFRUF
node tools/kachel-farbe-einzeln.mjs 40 nur zeigen
node tools/kachel-farbe-einzeln.mjs 40 --schreiben eintragen
node tools/kachel-farbe-einzeln.mjs --pruefen nur nachmessen
Mehrere Nummern gehen auch: `… 38 39 --schreiben` zieht sie
NACHEINANDER nach, jede gegen den Stand INKLUSIVE der vorherigen.
===================================================================== */
import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { GRUND, MINDEST_BUNTHEIT, HOECHST_BUNTHEIT, MINDEST_KONTRAST }
from "./kachelton-regeln.mjs";
const HIER = dirname(fileURLToPath(import.meta.url));
const CSS = join(HIER, "..", "workspace", "assets", "css", "start.css");
/* ---------- Farbrechnung (gleich wie in kachel-farben.mjs) ---------- */
const linear = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
const zurueck = (c) => (c <= 0.0031308 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055);
const srgb = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);
function oklab(hex) {
const [r, g, b] = srgb(hex).map(linear);
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
return [
0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s,
1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s,
];
}
function ausOklch(L, C, Hgrad) {
const h = (Hgrad * Math.PI) / 180;
const a = C * Math.cos(h), b2 = C * Math.sin(h);
const l_ = (L + 0.3963377774 * a + 0.2158037573 * b2) ** 3;
const m_ = (L - 0.1055613458 * a - 0.0638541728 * b2) ** 3;
const s_ = (L - 0.0894841775 * a - 1.2914855480 * b2) ** 3;
const r = +4.0767416621 * l_ - 3.3077115913 * m_ + 0.2309699292 * s_;
const g = -1.2684380046 * l_ + 2.6097574011 * m_ - 0.3413193965 * s_;
const b = -0.0041960863 * l_ - 0.7034186147 * m_ + 1.7076147010 * s_;
const kanal = (x) => {
const v = zurueck(x);
if (v < -0.001 || v > 1.001) return null;
return Math.round(Math.min(1, Math.max(0, v)) * 255);
};
const [R, G, B] = [kanal(r), kanal(g), kanal(b)];
if (R === null || G === null || B === null) return null;
return "#" + [R, G, B].map((x) => x.toString(16).padStart(2, "0")).join("");
}
const lum = (hex) => {
const [r, g, b] = srgb(hex).map(linear);
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
};
const kontrast = (hex) => (lum(hex) + 0.05) / (lum(GRUND) + 0.05);
const abstand = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
/* ---------- Lesen, und zwar vollstaendig ---------------------------- */
/** Alle Toene als {nummer, hex, lab, zeile}.
*
* `\s*` UND NICHT EIN LEERZEICHEN: Einstellige Nummern stehen mit zwei
* Leerzeichen da. Genau daran ist der erste Anlauf gescheitert. */
function lies(css) {
const aus = [];
for (const zeile of css.split("\n")) {
const m = zeile.match(/\[data-ton="(\d+)"\]\s*\{\s*--ton:\s*(#[0-9a-fA-F]{6})/);
if (!m) continue;
const wofuer = (zeile.match(/\/\*\s*([^*]+?)\s*\*\//) || [])[1] || "";
aus.push({ n: Number(m[1]), hex: m[2].toLowerCase(), lab: oklab(m[2]), zeile, wofuer });
}
return aus;
}
/** Bricht ab, wenn nicht jeder vorhandene Ton auch gelesen wurde. */
function vollstaendigOderAbbruch(css) {
const roh = new Set([...css.matchAll(/\[data-ton="(\d+)"\]/g)].map((m) => Number(m[1])));
const gelesen = lies(css);
const nummern = gelesen.map((t) => t.n);
if (roh.size !== nummern.length) {
console.error(`ABBRUCH: ${roh.size} Toene stehen in der Datei, gelesen wurden ${nummern.length}.`);
console.error(" nicht gelesen: " + [...roh].filter((n) => !nummern.includes(n)).join(", "));
console.error(" Wahrscheinlich hat sich die Schreibweise der Zeile geaendert.");
process.exit(2);
}
return gelesen;
}
function engstesPaar(liste) {
let best = { d: Infinity, a: null, b: null };
for (let i = 0; i < liste.length; i++) {
for (let j = i + 1; j < liste.length; j++) {
const d = abstand(liste[i].lab, liste[j].lab);
if (d < best.d) best = { d, a: liste[i], b: liste[j] };
}
}
return best;
}
/** Die Farbe mit dem groessten Abstand zu allen uebergebenen. */
function beste(andere) {
let treffer = null, wert = -1;
for (let H = 0; H < 360; H += 1) {
for (let L = 0.56; L <= 0.94; L += 0.01) {
/* Von oben herunter, damit die Farbe so satt wie erlaubt wird
und nicht blass -- Filipe: „richtig geil und speziell“. */
/* DIE UNTERE GRENZE IST NICHT 0,04, SONDERN DIE DER PRUEFUNG.
Bis zum 22.09.2026 lief die Suche bis 0,04 herunter -- und
lieferte fuer Ton 43 ein blasses #bcdbff mit Buntheit 0,060.
Abstand 0,1339, der beste Wert weit und breit, und
pruef-kachelfarben hat ihn trotzdem abgelehnt: zu blass.
Ein Werkzeug, dessen Vorschlaege die Pruefung danach
verwirft, ist schlimmer als keines -- man haelt das
Ergebnis fuer fertig. Beide lesen die Zahl jetzt aus
kachelton-regeln.mjs. */
for (let C = HOECHST_BUNTHEIT; C >= MINDEST_BUNTHEIT; C -= 0.004) {
const hex = ausOklch(L, C, H);
if (!hex) continue;
if (kontrast(hex) < 4.5) break;
const lab = oklab(hex);
let naechster = Infinity;
for (const v of andere) naechster = Math.min(naechster, abstand(lab, v.lab));
if (naechster > wert) { wert = naechster; treffer = { hex, lab, L, C, H }; }
break;
}
}
}
return { ...treffer, abstand: wert };
}
/* ---------- Ablauf --------------------------------------------------- */
const argumente = process.argv.slice(2);
const schreiben = argumente.includes("--schreiben");
const nurPruefen = argumente.includes("--pruefen");
const nummern = argumente.filter((a) => /^\d+$/.test(a)).map(Number);
let css = readFileSync(CSS, "utf8");
const anfang = vollstaendigOderAbbruch(css);
console.log(`${anfang.length} Toene gelesen — vollstaendig.`);
{
const p = engstesPaar(anfang);
console.log(`Engstes Paar jetzt: ${p.d.toFixed(4)} `
+ `Ton ${p.a.n} ${p.a.hex} (${p.a.wofuer}) <-> Ton ${p.b.n} ${p.b.hex} (${p.b.wofuer})`);
}
if (nurPruefen || !nummern.length) {
if (!nummern.length && !nurPruefen) {
console.log("\nKeine Nummer angegeben. Aufruf: node tools/kachel-farbe-einzeln.mjs <nr> [--schreiben]");
}
const sortiert = [];
for (let i = 0; i < anfang.length; i++) {
for (let j = i + 1; j < anfang.length; j++) {
sortiert.push({ d: abstand(anfang[i].lab, anfang[j].lab), a: anfang[i], b: anfang[j] });
}
}
sortiert.sort((x, y) => x.d - y.d);
console.log("\nDie fuenf engsten Paare:");
for (const p of sortiert.slice(0, 5)) {
console.log(` ${p.d.toFixed(4)} Ton ${p.a.n} (${p.a.wofuer}) <-> Ton ${p.b.n} (${p.b.wofuer})`);
}
const schwach = anfang.filter((t) => kontrast(t.hex) < 4.5);
console.log(schwach.length
? `\nZU DUNKEL (< 4,5:1): ${schwach.map((t) => `Ton ${t.n}`).join(", ")}`
: "\nAlle Toene erreichen 4,5:1 gegen den Grund.");
process.exit(0);
}
for (const ziel of nummern) {
/* JEDES MAL NEU LESEN: Wer zwei Nummern nacheinander nachzieht, soll
die zweite gegen den Stand INKLUSIVE der ersten rechnen. Sonst
bekommen beide dieselbe Farbe -- genau das ist beim ersten Anlauf
passiert. */
const alle = lies(css);
const vorher = alle.find((t) => t.n === ziel);
if (!vorher) { console.error(`Ton ${ziel} gibt es nicht.`); process.exit(2); }
const andere = alle.filter((t) => t.n !== ziel);
const alt = Math.min(...andere.map((t) => abstand(vorher.lab, t.lab)));
const neu = beste(andere);
/* EIN UNGUELTIGER IST-WERT IST KEINE MESSLATTE (24.09.2026).
Gefunden beim Eintragen von Ton 44 -- und es war GENAU DERSELBE
FEHLER wie am 22.09., nur eine Ebene hoeher.
Damals: Die Suche lief bis Buntheit 0,04 herunter, die Pruefung
verlangt 0,12. Behoben, indem beide dieselbe Zahl lesen.
Heute: Ich hatte fuer Ton 44 von Hand ein zu blasses #bcdbff
eingetragen (Buntheit 0,060). Dieses Werkzeug las es als
Ausgangswert, verglich den besten NEUEN Vorschlag dagegen -- und
meldete "bleibt #bcdbff, nichts Besseres gefunden". Der Abstand
war tatsaechlich groesser; nur war der Ist-Wert gar nicht
zulaessig. Ein Werkzeug, das einen regelwidrigen Zustand
verteidigt, weil er zufaellig gut misst, haelt genau den Fehler
fest, den es beheben soll.
Geprueft werden deshalb ZUERST die Regeln, dann der Abstand. */
const istBuntheit = Math.hypot(vorher.lab[1], vorher.lab[2]);
const istGueltig = istBuntheit >= MINDEST_BUNTHEIT
&& kontrast(vorher.hex) >= MINDEST_KONTRAST;
if (!istGueltig) {
console.log(`
Ton ${ziel} (${vorher.wofuer}): ${vorher.hex} haelt die Regeln `
+ `nicht ein (Buntheit ${istBuntheit.toFixed(3)}, `
+ `Kontrast ${kontrast(vorher.hex).toFixed(2)}:1) -- er wird ersetzt, `
+ "egal wie gut sein Abstand ist.");
}
if (istGueltig && neu.abstand <= alt) {
console.log(`\nTon ${ziel} (${vorher.wofuer}): bleibt ${vorher.hex} — `
+ `nichts Besseres gefunden (${alt.toFixed(4)}).`);
continue;
}
console.log(`\nTon ${ziel} (${vorher.wofuer})`);
console.log(` vorher ${vorher.hex} Abstand zum Nachbarn ${alt.toFixed(4)}`);
console.log(` nachher ${neu.hex} Abstand zum Nachbarn ${neu.abstand.toFixed(4)}`
+ ` (${(neu.abstand / alt).toFixed(1)}-fach)`);
console.log(` Kontrast ${kontrast(neu.hex).toFixed(2)}:1, Buntheit ${neu.C.toFixed(3)}`);
if (!schreiben) { console.log(" (nur gezeigt — mit --schreiben eintragen)"); continue; }
const neueZeile = vorher.zeile.replace(vorher.hex, neu.hex);
if (neueZeile === vorher.zeile) {
console.error(" ABBRUCH: Die Zeile liess sich nicht ersetzen.");
process.exit(2);
}
css = css.replace(vorher.zeile, neueZeile);
}
if (schreiben) {
writeFileSync(CSS, css);
const nachher = vollstaendigOderAbbruch(readFileSync(CSS, "utf8"));
const p = engstesPaar(nachher);
console.log(`\nGeschrieben. ${nachher.length} Toene, engstes Paar jetzt ${p.d.toFixed(4)} `
+ `(Ton ${p.a.n} <-> Ton ${p.b.n}).`);
const schwach = nachher.filter((t) => kontrast(t.hex) < 4.5);
if (schwach.length) {
console.error(`WARNUNG: ${schwach.map((t) => `Ton ${t.n}`).join(", ")} unter 4,5:1.`);
process.exit(1);
}
}