Die Dogen-Muenzen: drei Saetze zu je drei Stufen

Filipe, 28.09.2026: "ich werde die drei varianten schicken fuer
niedrige spenden. mittlere und hohe spenden. alle drei varianten will
ich drauf so dass ich sie aussuchen kann wie ich will. aber pass sie
sofort diesen 3 kategorien an."

DIE ZUORDNUNG IST GELESEN, NICHT GERATEN

In allen drei Saetzen geht es nacktes Metall -> Steinkranz ->
Vollbesatz; Filipes Dateinummern (01/02/03) und die Uhrzeiten seiner
Entwuerfe laufen genau mit. Klassik: Palladium, Gold mit Saphir,
Diamant. Amethyst: Stahl, Rosegold, Vollbesatz. Neon: Chrom auf
Schwarz, Steinkranz, Vollbesatz.

AUS 25 MB WURDEN 443 KB

Je Bild 320 px WebP statt 1254 px PNG, Faktor 58. Die Zahl ist
gemessen: Groesster Fall in der Seite 110 px (Karte, hoechste Stufe),
auf der OBS-Tafel 189 px, bei doppelter Bildschirmdichte rund 220.
Drei Megabyte fuer ein 24-Pixel-Zeichen waeren ein Ladebalken mitten
in der Sendung -- wer auf dem Handy zusieht, bekaeme die Karte, wenn
sie schon wieder weg ist. Die Originale liegen neben den
Datenbanksicherungen, nicht im Repo: 26 MB, die bei jedem Klon
mitkaemen und die niemand ausliefert.

WELCHE MUENZE WANN -- ABGELEITET STATT GEPFLEGT

"Niedrig, mittel, hoch" gibt es im Haus schon: die Stufenleiter. Zwei
eigene Grenzen daneben waeren eine zweite Antwort auf dieselbe Frage,
und spaetestens beim Verschieben einer Stufe zeigte die Muenze etwas
anderes an als der Name auf der Karte. Die Leiter wird deshalb in
DRITTEL geteilt -- bei den drei Werksstufen genau eine je Muenze, bei
sechs zwei je Muenze, bei einer einzigen ueberall die mittlere.

DIE MUENZE STEHT AUCH GROSS AUF DER KARTE

Sonst haette Filipe neun Bilder fuer ein 24-Pixel-Zeichen gezeichnet.
"dogen" ist dafuer eine neue Vorlage neben Herz, Welle und Krone --
und die drei Werksstufen bekommen sie EINMAL zugeteilt, nur wo noch
die Werksvorlage steht und kein eigenes Bild hochgeladen ist. Wer
danach ein Herz zurueckstellt, findet es morgen nicht wieder als
Muenze vor: Eine Einstellung, die sich gegen den Benutzer durchsetzt,
wird abgeschafft.

Und steht sie gross da, nimmt das Stilblatt die kleine neben der Zahl
weg -- dieselbe Muenze in zwei Groessen auf einer Karte sieht aus wie
ein Versehen.

AM BILDSCHIRMFOTO NACHGEBESSERT

Bei 0,92em blieb an den Spendenknoepfen ein 13,5-Pixel-Fleck uebrig;
bei einem flachen Symbol reicht das, bei einer Muenze mit Pfote,
Schriftzug und Steinen nicht. Jetzt 1,05em ueberall und 1,6em auf den
Knoepfen. In der Auswahl sahen "Amethyst" und "Neon" bei 40 px
praktisch gleich aus -- und genau sie auseinanderzuhalten ist der
Zweck dieser Liste. Jetzt 56/64/72 px. Und was gewaehlt ist, steht
als WORT da: Neben neun glaenzenden Muenzen geht ein ruhiger Rahmen
unter, und wer Farben schlecht unterscheidet, sieht ihn gar nicht.

EIN SATZWECHSEL ERREICHT ALLE

Die Karten trugen ihren Satz immer selbst mit und waren richtig. Die
Muenzen an den Spendenknoepfen und beim eigenen Stand aber nicht --
die holt eine Seite nur bei einer Spende neu. Eine Einstellung, die
nur dort ankommt, wo sie gemacht wurde, ist keine Einstellung des
Hauses.

ZWEI BEFUNDE GEGEN DIE PRUEFUNG SELBST

1. Sie verlangte "erste Grenze ECHT kleiner als zweite". Bei genau
   zwei Stufen fallen beide absichtlich zusammen -- sie meldete einen
   Fehler, wo das Verhalten richtig ist.
2. Sie prueft die Muenzverteilung jetzt auf einer FRISCHEN Datenbank.
   Vorher lief sie auf der, die ein frueherer Abschnitt umgebaut
   hatte: Eine Pruefung, die den eigenen Kollateralschaden misst,
   misst sich selbst. Dazu eine Gegenprobe, dass eine selbst
   gewaehlte Vorlage NICHT ueberschrieben wird.

UND DREI AN DER MESSUNG

Sie suchte noch die gezeichnete Muenze (svg), wo jetzt ein Bild
liegt. Umgestellt nicht auf "ist ein img da", sondern auf
`naturalWidth > 0` -- ob es etwas ZEIGT. Ein img, das 404 antwortet,
steht genauso im Dokument wie eines, das laedt; auf dem Bildschirm
ist an seiner Stelle nichts. Und beim Satzwechsel las sie die vorige
Karte, die noch acht Sekunden stand: Die Karten laufen in einer
Schlange, also wird auf das Merkmal gewartet, das nur die neue hat.

pruef-spenden 167/0 (vorher 136) - pruef-reaktion 321/0 -
pruef-css-klassen ok - pruef-tippziele 11/0 -
mess-reaktion und mess-buehne ohne Beanstandung

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-28 19:36:05 +02:00
co-authored by Claude Opus 5
parent c00c209ac9
commit 96bdd79a91
63 changed files with 1446 additions and 770 deletions
+219 -11
View File
@@ -16,7 +16,7 @@
Aufrufen mit: node server/pruef-spenden.mjs
===================================================================== */
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
import { mkdtempSync, rmSync, readFileSync, statSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
@@ -396,12 +396,20 @@ melde("=== 8. Die Gestaltung der Karten ===");
const block = karte.match(/const VORLAGEN = \{([\s\S]*?)\n \};/);
const inKarte = [...(block ? block[1] : "").matchAll(/^\s{4}([a-z]+):/gm)].map((m) => m[1]);
ok(inKarte.length >= 5, `die Karte kennt ${inKarte.length} Zeichnungen`);
const fehlt = VORLAGEN.filter((v) => !inKarte.includes(v));
const zuviel = inKarte.filter((v) => !VORLAGEN.includes(v));
/* „dogen" IST EINE VORLAGE, ABER KEINE ZEICHNUNG. Sie zeigt
die Muenze des gewaehlten Satzes und steht deshalb nicht im
Zeichnungsblock -- verglichen wird ohne sie. Dass die Karte
sie trotzdem behandelt, prueft die Zeile darunter: Ohne sie
waere „dogen" eine Auswahl, die als Herz erscheint. */
const zeichnungen = VORLAGEN.filter((v) => v !== "dogen");
const fehlt = zeichnungen.filter((v) => !inKarte.includes(v));
const zuviel = inKarte.filter((v) => !zeichnungen.includes(v));
ok(fehlt.length === 0 && zuviel.length === 0,
(fehlt.length || zuviel.length)
? `UNGLEICH: fehlt ${fehlt.join(",")} / zuviel ${zuviel.join(",")}`
: `Server und Karte kennen dieselben ${VORLAGEN.length} Zeichnungen`);
: `Server und Karte kennen dieselben ${zeichnungen.length} Zeichnungen`);
ok(VORLAGEN.includes("dogen") && /vorlage === 'dogen'/.test(karte),
"und die Karte behandelt die Muenze als eigene Vorlage");
ok(!VORLAGEN.includes("wolke"), "Gegenprobe: eine erfundene Zeichnung steht in keiner Liste");
}
@@ -789,8 +797,12 @@ melde("=== 9. Dogen, eigener Text und Punkte ===");
"und in der Liste steht nur, wer wirklich etwas hat");
/* ---- DAS ZEICHEN ---- */
/* SEIT DEM 28.09. IST `zeichen` EINE AUSWAHL UND KEINE ADRESSE:
drei Muenzen und zwei Grenzen. Das Hochgeladene steht darin als
`eigen`. Die Saetze selbst prueft Abschnitt 10; hier geht es nur
um den Weg des eigenen Bildes. */
const ohneZeichen = await roh(CREW, "/workspace/api/spenden", { keks: k.admin });
ok(ohneZeichen.j?.zeichen === "", "ohne Hochladen gibt es kein Zeichen");
ok(ohneZeichen.j?.zeichen?.eigen === "", "ohne Hochladen gibt es kein eigenes Zeichen");
const png = Buffer.from(
"89504e470d0a1a0a0000000d494844520000000100000001080600000"
@@ -799,19 +811,23 @@ melde("=== 9. Dogen, eigener Text und Punkte ===");
const hochZ = await roh(CREW, "/workspace/api/spenden/zeichen", {
method: "POST", keks: k.admin, roher: png,
});
ok(hochZ.code === 200 && /\/spendenbild\/[a-f0-9]{32}\.png$/.test(hochZ.j?.zeichen || ""),
ok(hochZ.code === 200
&& /\/spendenbild\/[a-f0-9]{32}\.png$/.test(hochZ.j?.zeichen?.eigen || ""),
`das Zeichen laesst sich hochladen (${hochZ.code})`);
const abrufbar = await roh(CREW, hochZ.j.zeichen, {});
const eigenAdresse = hochZ.j?.zeichen?.eigen || "";
const abrufbar = await roh(CREW, eigenAdresse, {});
ok(abrufbar.code === 200,
`und ist OHNE Anmeldung abrufbar (${abrufbar.code}) -- sonst fehlte es im Stream`);
const mitZeichen = await roh(CREW, "/workspace/api/spenden", { keks: k.gast });
ok(mitZeichen.j?.zeichen === hochZ.j.zeichen,
ok(mitZeichen.j?.zeichen?.eigen === eigenAdresse,
"es steht im Stand, und zwar fuer alle");
ok(mitZeichen.j?.zeichen?.niedrig === eigenAdresse,
"und gilt fuer alle drei Stufen gleich");
const probe = await roh(CREW, "/workspace/api/spenden/probe", {
method: "POST", keks: k.admin, rumpf: { dogen_mille: 500 },
});
ok(probe.j?.karte?.zeichen === hochZ.j.zeichen,
ok(probe.j?.karte?.zeichen?.eigen === eigenAdresse,
"und reist mit jeder Karte mit");
const keinBild = await roh(CREW, "/workspace/api/spenden/zeichen", {
@@ -826,12 +842,204 @@ melde("=== 9. Dogen, eigener Text und Punkte ===");
const wegZ = await roh(CREW, "/workspace/api/spenden/zeichen",
{ method: "DELETE", keks: k.admin });
ok(wegZ.code === 200 && wegZ.j?.zeichen === "",
ok(wegZ.code === 200 && wegZ.j?.zeichen?.eigen === "",
`es laesst sich wieder wegnehmen (${wegZ.code})`);
const fortZ = await roh(CREW, hochZ.j.zeichen, {});
const fortZ = await roh(CREW, eigenAdresse, {});
ok(fortZ.code === 404, `und die Datei ist danach fort (${fortZ.code})`);
}
/* =======================================================================
10. DIE DREI MUENZSAETZE
Filipe, 28.09.2026: „ich werde die drei varianten schicken fuer
niedrige spenden. mittlere und hohe spenden. alle drei varianten
will ich drauf so dass ich sie aussuchen kann wie ich will. aber
pass sie sofort diesen 3 kategorien an."
======================================================================= */
melde("");
melde("=== 10. Die drei Muenzsaetze ===");
{
const { DOGEN_SAETZE, DOGEN_STUFEN, muenzeAdresse }
= await import("./spenden-tabellen.js");
ok(DOGEN_SAETZE.length === 3,
`es gibt ${DOGEN_SAETZE.length} Saetze: ${DOGEN_SAETZE.map((x) => x.name).join(", ")}`);
ok(DOGEN_STUFEN.length === 3 && DOGEN_STUFEN[0] === "niedrig"
&& DOGEN_STUFEN[2] === "hoch",
`und drei Stufen: ${DOGEN_STUFEN.join(", ")}`);
/* ---- JEDE MUENZE MUSS ES AUCH GEBEN ----
Die wichtigste Pruefung hier. Ein fehlendes Bild waere ein
kaputtes Zeichen neben JEDER Zahl -- im Saal und im Stream, und
gemerkt haette man es erst im Livestream. */
{
const fehlend = [];
const zuGross = [];
for (const satz of DOGEN_SAETZE) {
for (const stufe of DOGEN_STUFEN) {
const pfad = join(HIER, "..", muenzeAdresse(satz.schluessel, stufe)
.replace(/^\//, "").split("/").join("/"));
try {
const gross = statSync(pfad).size;
if (gross > 100 * 1024) zuGross.push(`${satz.schluessel}-${stufe} ${Math.round(gross / 1024)} KB`);
} catch { fehlend.push(`${satz.schluessel}-${stufe}`); }
}
}
ok(fehlend.length === 0,
fehlend.length ? `ES FEHLEN: ${fehlend.join(", ")}`
: `alle ${DOGEN_SAETZE.length * DOGEN_STUFEN.length} Muenzen liegen im Baum`);
/* UND KEINE DARF SCHWER SEIN. Filipes Vorlagen waren je drei
Megabyte; gezeigt wird die Muenze zwischen 24 und 110 Pixeln.
Ohne diese Grenze rutscht irgendwann wieder ein Original
herein, und dann laedt die Karte laenger, als sie steht. */
ok(zuGross.length === 0,
zuGross.length ? `ZU SCHWER: ${zuGross.join(", ")}`
: "und keine ist schwerer als 100 KB");
}
/* ---- UND SIE MUESSEN OHNE ANMELDUNG ZU HABEN SEIN ----
Die Spendentafel in OBS hat keine Sitzung. Eine Muenze hinter
der Schranke fehlte ausgerechnet im Stream. */
{
const a = await roh(CREW, muenzeAdresse("klassik", "hoch"), {});
ok(a.code === 200, `eine Muenze kommt ohne Anmeldung (${a.code})`);
const b = await roh(CREW, "/workspace/assets/dogen/gibtesnicht-hoch.webp", {});
ok(b.code === 404, `Gegenprobe: eine erfundene Muenze gibt es nicht (${b.code})`);
}
/* ---- WAS DER STAND SAGT ---- */
const lage = await roh(CREW, "/workspace/api/spenden", { keks: k.admin });
const z = lage.j?.zeichen;
ok(!!z && typeof z.satz === "string" && z.niedrig && z.mittel && z.hoch,
`der Stand traegt einen Satz („${z?.satz}“) mit drei Muenzen`);
/* KLEINER ODER GLEICH, nicht echt kleiner. Bei genau ZWEI Stufen
fallen die beiden Grenzen absichtlich zusammen: Dann gibt es
niedrig und hoch, und nichts dazwischen. Eine Pruefung auf „echt
kleiner" haette hier einen Fehler gemeldet, wo das Verhalten
richtig ist -- und genau das hat sie beim ersten Lauf getan. */
ok(Array.isArray(z?.grenzen) && z.grenzen.length === 2 && z.grenzen[0] <= z.grenzen[1],
`und zwei Grenzen (${z?.grenzen?.join(" / ")})`);
/* DIE GRENZEN KOMMEN AUS DER STUFENLEITER UND NICHT AUS EINER
ZWEITEN LISTE. Bei drei Stufen ist das genau eine Muenze je
Stufe. */
const leiter = (lage.j?.stufen || []).map((x) => x.ab_mille);
ok(leiter.length < 3
|| (z.grenzen[0] === leiter[Math.floor(leiter.length / 3)]
&& z.grenzen[1] === leiter[Math.floor((2 * leiter.length) / 3)]),
`sie sind aus der Leiter abgeleitet (Leiter ${leiter.join(", ")})`);
ok(Array.isArray(lage.j?.saetze) && lage.j.saetze.length >= 3,
`die Leitung bekommt ${lage.j?.saetze?.length} Saetze zum Aussuchen`);
const gast = await roh(CREW, "/workspace/api/spenden", { keks: k.gast });
ok(gast.j?.saetze === null, "ein Zuschauer bekommt die Auswahl nicht");
ok(!!gast.j?.zeichen?.niedrig, "aber die Muenzen selbst schon");
/* ---- DIE KARTE TRAEGT DEN SATZ ----
Eine Form ueberall: Die Karte bekommt dieselbe Auswahl wie der
Stand und sucht sich daraus die Muenze zu ihrer Zahl. Schickte
der Server hier EINE Adresse, gaebe es zwei Formen desselben
Dinges. */
const probe = await roh(CREW, "/workspace/api/spenden/probe", {
method: "POST", keks: k.admin, rumpf: { dogen_mille: 2500 },
});
ok(!!probe.j?.karte?.zeichen?.hoch && Array.isArray(probe.j.karte.zeichen.grenzen),
"und jede Karte bekommt ihn mit");
/* ---- AUSSUCHEN ---- */
for (const satz of DOGEN_SAETZE) {
const a = await roh(CREW, "/workspace/api/spenden/zeichen/satz", {
method: "PUT", keks: k.admin, rumpf: { satz: satz.schluessel },
});
ok(a.code === 200 && a.j?.zeichen?.satz === satz.schluessel,
`„${satz.name}“ laesst sich waehlen (${a.code})`);
ok((a.j?.zeichen?.hoch || "").includes(`${satz.schluessel}-hoch`),
`und die hohe Muenze kommt aus diesem Satz`);
}
const quatsch = await roh(CREW, "/workspace/api/spenden/zeichen/satz", {
method: "PUT", keks: k.admin, rumpf: { satz: "platin" },
});
ok(quatsch.code === 400 && quatsch.j?.fehler === "satz_unbekannt",
`ein erfundener Satz wird abgelehnt (${quatsch.code})`);
/* „eigen" OHNE HOCHGELADENES BILD GIBT ES NICHT -- sonst stuende
neben jeder Zahl ein Bild, das 404 antwortet. */
const ohneEigenes = await roh(CREW, "/workspace/api/spenden/zeichen/satz", {
method: "PUT", keks: k.admin, rumpf: { satz: "eigen" },
});
ok(ohneEigenes.code === 400,
`„eigen“ ohne eigenes Bild wird abgelehnt (${ohneEigenes.code})`);
const fremd = await roh(CREW, "/workspace/api/spenden/zeichen/satz", {
method: "PUT", keks: k.modi, rumpf: { satz: "neon" },
});
ok(fremd.code === 403, `ein Modi waehlt den Satz nicht (${fremd.code})`);
/* ---- EIN EIGENES ZEICHEN GILT SOFORT UND FAELLT SAUBER ZURUECK ---- */
const png = Buffer.from(
"89504e470d0a1a0a0000000d494844520000000100000001080600000"
+ "01f15c4890000000a49444154789c6360000002000100ffff0300000600"
+ "0557bfabd40000000049454e44ae426082", "hex");
const hoch = await roh(CREW, "/workspace/api/spenden/zeichen", {
method: "POST", keks: k.admin, roher: png,
});
ok(hoch.code === 200 && hoch.j?.zeichen?.satz === "eigen",
`ein hochgeladenes Zeichen gilt sofort (${hoch.j?.zeichen?.satz})`);
ok(hoch.j?.zeichen?.niedrig === hoch.j?.zeichen?.hoch,
"es gilt fuer alle drei Stufen gleich");
ok((hoch.j?.saetze || []).some((x) => x.schluessel === "eigen"),
"und steht ab jetzt in der Auswahl");
const weg = await roh(CREW, "/workspace/api/spenden/zeichen",
{ method: "DELETE", keks: k.admin });
ok(weg.code === 200 && weg.j?.zeichen?.satz === DOGEN_SAETZE[0].schluessel,
`nach dem Loeschen gilt wieder ein Satz, den es gibt (${weg.j?.zeichen?.satz})`);
ok(!(weg.j?.saetze || []).some((x) => x.schluessel === "eigen"),
"und „eigen“ steht nicht mehr zur Wahl");
/* ==== DIE MUENZEN HAENGEN VON ANFANG AN AN DEN DREI STUFEN ====
Filipe: „aber pass sie sofort diesen 3 kategorien an."
GEPRUEFT WIRD AUF EINER FRISCHEN DATENBANK und nicht auf dieser:
Die Leiter hier ist in Abschnitt 8 umgebaut worden, und danach
steht sie so da, wie die Pruefung sie gesetzt hat -- nicht so,
wie ein frisches Haus sie bekommt. Eine Pruefung, die den
eigenen Kollateralschaden misst, misst sich selbst.
Und die zweite Haelfte ist die wichtigere: Eine Stufe, die
jemand SELBST gewaehlt hat, wird nicht ueberschrieben. Eine
Verteilung, die sich gegen den Benutzer durchsetzt, wird
abgeschafft. */
{
const { mkdtempSync: neuerOrdner } = await import("node:fs");
const { tmpdir: tmp2 } = await import("node:os");
const { DatabaseSync: DBM } = await import("node:sqlite");
const { spendenTabellen } = await import("./spenden-tabellen.js");
const ordner2 = neuerOrdner(join(tmp2(), "muenzen-"));
const d2 = new DBM(join(ordner2, "probe.db"));
d2.exec(`CREATE TABLE einstellungen (schluessel TEXT PRIMARY KEY,
wert TEXT NOT NULL, geaendert TEXT, von INTEGER);
CREATE TABLE personen (id INTEGER PRIMARY KEY);`);
spendenTabellen(d2);
const frisch = d2.prepare("SELECT ab_cent, vorlage FROM spenden_stufen ORDER BY ab_cent")
.all();
ok(frisch.length === 3 && frisch.every((x) => x.vorlage === "dogen"),
`ein frisches Haus zeigt die Muenze auf allen ${frisch.length} Stufen `
+ `(${frisch.map((x) => x.vorlage).join(", ")})`);
/* Gegenprobe: eine selbst gewaehlte Vorlage bleibt stehen -- auch
wenn die Verteilung noch einmal laufen wuerde. */
d2.exec("UPDATE spenden_stufen SET vorlage = 'stern' WHERE ab_cent = 500");
d2.exec("DELETE FROM einstellungen WHERE schluessel = 'dogen_muenzen_verteilt'");
spendenTabellen(d2);
const nachDemZweiten = d2.prepare("SELECT vorlage FROM spenden_stufen WHERE ab_cent = 500")
.get();
ok(nachDemZweiten?.vorlage === "stern",
`Gegenprobe: eine selbst gewaehlte Vorlage bleibt (${nachDemZweiten?.vorlage})`);
d2.close();
rmSync(ordner2, { recursive: true, force: true });
}
}
melde("");
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
melde(fehler === 0 ? "ALLES IN ORDNUNG" : "NICHT IN ORDNUNG");