diff --git a/server/mess-reaktion.mjs b/server/mess-reaktion.mjs
index 45e97ce4..a1b18ffa 100644
--- a/server/mess-reaktion.mjs
+++ b/server/mess-reaktion.mjs
@@ -582,6 +582,13 @@ await zuschauer.seite.evaluate(() => {
wer: n.querySelector(".spendenkarte__wer")?.textContent?.trim(),
stufe: n.querySelector(".spendenkarte__stufe")?.textContent?.trim(),
gruss: n.querySelector(".spendenkarte__gruss")?.textContent?.trim(),
+ /* Welche Muenze gross auf der Karte steht -- und ob die
+ kleine daneben weggenommen wurde. Dieselbe Muenze in
+ zwei Groessen auf einer Karte saehe aus wie ein
+ Versehen. */
+ muenze: (n.querySelector(".spendenkarte__bild img")
+ ?.getAttribute("src") || "").split("/").pop(),
+ kleinWeg: n.dataset?.muenze === "ja",
});
}
}
@@ -619,10 +626,19 @@ const geben = await zuschauer.seite.evaluate(() => {
da: k2 && !k2.hidden,
knoepfe: knoepfe.length,
zahlen: knoepfe.map((a) => a.querySelector(".dogen__zahl")?.textContent || "?"),
- /* Ohne hochgeladenes Symbol muss die gezeichnete Muenze da sein
- -- ein Knopf mit einer nackten Zahl waere kein „Dogen mit dem
- Symbol". */
- zeichen: knoepfe.filter((a) => a.querySelector("svg.dogen__zeichen")).length,
+ /* ==== DAS ZEICHEN MUSS DA SEIN -- UND GELADEN ==============
+ Seit dem 28.09. ist es ein BILD (Filipes Muenzen) und keine
+ Zeichnung mehr. Gezaehlt wird deshalb nicht, ob ein Knoten da
+ ist, sondern ob er etwas ZEIGT: Ein `img`, das 404 antwortet,
+ steht genauso im Dokument wie eines, das laedt -- und auf dem
+ Bildschirm ist an seiner Stelle nichts.
+ `naturalWidth > 0` ist der Unterschied, und er ist genau der,
+ den man sonst erst im Livestream sieht. */
+ zeichen: knoepfe.filter((a) => {
+ const b = a.querySelector(".dogen__zeichen");
+ if (!b) return false;
+ return b.tagName.toLowerCase() === "img" ? b.naturalWidth > 0 : true;
+ }).length,
/* Und nirgends ein Euro. */
geld: /€|EUR|Euro/.test(k2?.textContent || ""),
};
@@ -820,7 +836,11 @@ if (!wartet) {
da: !!p && !p.hidden,
zahl: p?.querySelector(".dogen__zahl")?.textContent || "",
wort: p?.querySelector(".dogen__wort")?.textContent || "",
- zeichen: !!p?.querySelector("svg.dogen__zeichen"),
+ zeichen: (() => {
+ const b = p?.querySelector(".dogen__zeichen");
+ if (!b) return false;
+ return b.tagName.toLowerCase() === "img" ? b.naturalWidth > 0 : true;
+ })(),
text: p?.textContent?.trim() || "",
};
});
@@ -830,6 +850,71 @@ if (!wartet) {
console.log(" ACHTUNG: Der eigene Dogen-Stand kommt nicht an.");
ausgang = 1;
}
+
+ /* ==== DIE MUENZE MUSS ZUR HOEHE DER SPENDE PASSEN =============
+
+ Filipe: „drei varianten fuer niedrige spenden. mittlere und
+ hohe spenden. aber pass sie sofort diesen 3 kategorien an."
+
+ Die erste Karte sind 2,5 Dogen (Rudel-Legende, oberes Drittel),
+ die zweite 1 Doge (Starke Runde, mittleres). Stuende auf beiden
+ dieselbe Muenze, waere die ganze Einteilung wirkungslos -- und
+ das sieht man einer einzelnen Karte nie an. */
+ console.log(` Muenzen: „${nachMelden[0]?.muenze}" bei 2,5 · `
+ + `„${nachJa[1]?.muenze}" bei 1`);
+ console.log(` Kleine Muenze daneben weggenommen: `
+ + `${nachMelden[0]?.kleinWeg} / ${nachJa[1]?.kleinWeg}`);
+ if (!/-hoch\.webp$/.test(nachMelden[0]?.muenze || "")
+ || !/-mittel\.webp$/.test(nachJa[1]?.muenze || "")) {
+ console.log(" ACHTUNG: Die Muenze passt nicht zur Hoehe der Spende.");
+ ausgang = 1;
+ }
+ if (!nachMelden[0]?.kleinWeg || !nachJa[1]?.kleinWeg) {
+ console.log(" ACHTUNG: Die Muenze steht zweimal auf derselben Karte.");
+ ausgang = 1;
+ }
+
+ /* ---- UND DER SATZ LAESST SICH WECHSELN ----
+ Gemessen ueber die Probe beim Host: Sie schreibt nichts in die
+ Datenbank und ist damit der einzige Weg, eine Karte zu sehen,
+ ohne eine Spende zu erfinden. */
+ await roh("/workspace/api/spenden/zeichen/satz", {
+ method: "PUT", keks: k.admin, rumpf: { satz: "neon" },
+ });
+ await host.seite.waitForTimeout(700);
+ await roh("/workspace/api/spenden/probe", {
+ method: "POST", keks: k.admin, rumpf: { dogen_mille: 2500 },
+ });
+ /* AUF DIE PROBEKARTE WARTEN, NICHT AUF IRGENDEINE.
+
+ Beim ersten Lauf stand noch die vorige Karte („Starke Runde",
+ acht Sekunden) auf dem Bildschirm, und die Messung las deren
+ Muenze -- sie meldete „klassik-mittel" und damit einen Fehler,
+ den es nicht gab. Die Karten laufen in einer SCHLANGE: Die
+ naechste kommt erst, wenn die vorige abgelaufen ist. Also auf
+ das Merkmal warten, das nur die neue hat. */
+ await host.seite.waitForFunction(
+ () => [...document.querySelectorAll('.spendenkarte[data-da="ja"]')].some(
+ (k2) => k2.querySelector(".spendenkarte__wer")?.textContent === "Probe"
+ && k2.querySelector(".spendenkarte__bild img")),
+ null, { timeout: 20000 },
+ ).catch(() => {});
+ const nachWechsel = await host.seite.evaluate(() => {
+ const k2 = [...document.querySelectorAll('.spendenkarte[data-da="ja"]')]
+ .find((x) => x.querySelector(".spendenkarte__wer")?.textContent === "Probe");
+ const b = k2?.querySelector(".spendenkarte__bild img");
+ /* Und auch hier: geladen, nicht nur vorhanden. */
+ return b && b.naturalWidth > 0
+ ? (b.getAttribute("src") || "").split("/").pop() : "(nicht geladen)";
+ });
+ console.log(` Nach dem Wechsel auf „Neon": „${nachWechsel}"`);
+ if (nachWechsel !== "neon-hoch.webp") {
+ console.log(" ACHTUNG: Der gewechselte Muenzsatz kommt nicht an.");
+ ausgang = 1;
+ }
+ await roh("/workspace/api/spenden/zeichen/satz", {
+ method: "PUT", keks: k.admin, rumpf: { satz: "klassik" },
+ });
}
await host.seite.click('#reiter .reiter__knopf[data-tafel="spenden"]').catch(() => {});
diff --git a/server/pruef-spenden.mjs b/server/pruef-spenden.mjs
index 8f494611..f4e60981 100644
--- a/server/pruef-spenden.mjs
+++ b/server/pruef-spenden.mjs
@@ -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");
diff --git a/server/spenden-tabellen.js b/server/spenden-tabellen.js
index f70dce20..1d56a84f 100644
--- a/server/spenden-tabellen.js
+++ b/server/spenden-tabellen.js
@@ -142,8 +142,67 @@ export const GRUSS_MAX = 140;
* DIESE LISTE IST DIE WAHRHEIT -- `spendenkarte.js` hat dieselben
* Namen, und eine Pruefung haelt beide aneinander. Ein Name, den
* nur eine Seite kennt, waere eine Vorlage, die sich auswaehlen
- * laesst und dann als Herz erscheint. */
-export const VORLAGEN = ["herz", "welle", "krone", "stern", "pfote"];
+ * laesst und dann als Herz erscheint.
+ *
+ * „dogen" ist die einzige, die KEINE Zeichnung ist: Sie zeigt die
+ * Muenze des gewaehlten Satzes in der Stufe, die zur Spende passt.
+ * Sie steht trotzdem in derselben Liste, weil sie an derselben
+ * Stelle dieselbe Frage beantwortet -- eine zweite Auswahl daneben
+ * waere eine zweite Antwort auf „welches Bild steht auf der
+ * Karte". */
+export const VORLAGEN = ["dogen", "herz", "welle", "krone", "stern", "pfote"];
+
+/* =====================================================================
+ DIE MUENZSAETZE (28.09.2026)
+
+ Filipe: „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."
+
+ Drei Saetze zu je drei Muenzen. Die Rangfolge steht in den Bildern
+ selbst und ist in allen drei dieselbe: nacktes Metall ->
+ Steinkranz -> Vollbesatz. Filipes Dateinummern (01/02/03) und die
+ Uhrzeiten seiner Entwuerfe laufen genau mit.
+
+ DIE BILDER LIEGEN IM BAUM UND NICHT IN DER DATENBANK. Sie kommen
+ mit dem Programm, aendern sich nur mit ihm, und `express.static`
+ liefert sie OHNE Anmeldung aus -- das braucht die Spendentafel in
+ OBS, die keine Sitzung hat. Ein hochgeladenes eigenes Zeichen gibt
+ es weiterhin; es ist der vierte Satz und heisst „eigen".
+ ===================================================================== */
+
+/** Die drei Stufen, in denen es jede Muenze gibt. */
+export const DOGEN_STUFEN = ["niedrig", "mittel", "hoch"];
+
+/** Die mitgelieferten Saetze. Der erste ist die Vorgabe. */
+export const DOGEN_SAETZE = [
+ {
+ schluessel: "klassik",
+ name: "Klassik",
+ wort: "Palladium · Gold mit Saphir · Diamant",
+ },
+ {
+ schluessel: "amethyst",
+ name: "Amethyst",
+ wort: "Stahl · Rosegold mit Amethyst · Vollbesatz",
+ },
+ {
+ schluessel: "neon",
+ name: "Neon",
+ wort: "Chrom auf Schwarz · Steinkranz · Vollbesatz",
+ },
+];
+
+/** Wo eine Muenze liegt. EINE Stelle bildet den Namen -- sonst
+ * stuende der Pfad an vier Stellen, und beim naechsten Satz
+ * vergisst man die vierte. */
+export function muenzeAdresse(satz, stufe) {
+ return `/workspace/assets/dogen/${satz}-${stufe}.webp`;
+}
+
+/** Unter welchem Namen der gewaehlte Satz in den Einstellungen steht. */
+export const SATZ_NAME = "dogen_satz";
/** So viele Stufen hoechstens.
*
@@ -186,19 +245,24 @@ export const GROESSE_MAX = 2.5;
/** Wie gross ein hochgeladenes Bild sein darf. */
export const BILD_MAX_BYTES = 3 * 1024 * 1024;
-/** Unter welchem Namen das Dogen-Zeichen in den Einstellungen steht.
+/** Unter welchem Namen ein EIGENES, hochgeladenes Zeichen steht.
*
- * Filipe: „die symbole schick ich dir spaeter. bereite jetzt alles
- * vor."
- *
- * ES IST EINE EINSTELLUNG UND KEINE DATEI IM QUELLTEXT. Laege das
- * Zeichen als Bild im Verzeichnis, haenge jedes neue Symbol an
- * einem Deploy -- also an mir. So laedt Filipe es im Regiepult hoch,
- * und bis dahin steht eine gezeichnete Muenze da, die in jeder
- * Farbe funktioniert. Ein leerer Kasten waere die schlechtere
- * Zwischenloesung: Man haelt ihn fuer kaputt. */
+ * Es bleibt neben den drei Saetzen bestehen und ist der vierte
+ * („eigen"). Anders als die Saetze gilt es fuer alle drei Stufen
+ * gleich -- wer ein einzelnes Bild hochlaedt, meint ein Zeichen und
+ * keine Rangfolge. */
export const ZEICHEN_NAME = "dogen_zeichen";
+/** Unter welchem Namen steht, dass die Muenzen schon einmal an die
+ * Stufen verteilt wurden.
+ *
+ * Filipe: „aber pass sie sofort diesen 3 kategorien an." Das
+ * passiert EINMAL und nie wieder: Wer danach eine Stufe auf ein
+ * Herz zurueckstellt, soll sie beim naechsten Neustart nicht wieder
+ * als Muenze vorfinden. Eine Einstellung, die sich gegen den
+ * Benutzer durchsetzt, wird abgeschafft. */
+export const MUENZEN_VERTEILT = "dogen_muenzen_verteilt";
+
/** Die Stufen, die es ab Werk gibt — „paar fertige" aus Filipes
* Ansage. Sie stehen hier und nicht in der Datenbank, damit ein
* frisches Haus sofort etwas anzuzeigen hat; geaendert werden sie
@@ -358,4 +422,37 @@ export function spendenTabellen(d) {
VALUES (?,?,?,?,?,?,?)`)
.run(s.ab_cent, s.name, s.vorlage, s.farbe, s.dauer_ms, s.groesse, nun);
}
+
+ /* ==== DIE MUENZEN EINMAL AN DIE DREI STUFEN VERTEILEN ==========
+
+ Filipe: „aber pass sie sofort diesen 3 kategorien an."
+
+ NUR EINMAL, UND NUR WO NICHTS EIGENES STEHT. Geaendert wird eine
+ Stufe genau dann, wenn sie noch ihre WERKSVORLAGE traegt (Herz,
+ Welle, Krone) und kein eigenes Bild hochgeladen ist. Wer selbst
+ einen Stern gewaehlt hat, behaelt ihn.
+
+ Und der Merker sorgt dafuer, dass es beim naechsten Start nicht
+ wieder passiert: Wer eine Stufe zurueck auf ein Herz stellt,
+ soll sie morgen nicht wieder als Muenze vorfinden. Eine
+ Einstellung, die sich gegen den Benutzer durchsetzt, wird
+ abgeschafft -- und dann ist auch die gute Absicht dahinter weg. */
+ try {
+ const schon = d.prepare("SELECT wert FROM einstellungen WHERE schluessel = ?")
+ .get(MUENZEN_VERTEILT);
+ if (!schon) {
+ const werk = new Set(STUFEN_START.map((x) => x.vorlage));
+ const a = d.prepare(`UPDATE spenden_stufen SET vorlage = 'dogen'
+ WHERE bild = '' AND vorlage IN (${[...werk].map(() => "?").join(",")})`)
+ .run(...werk);
+ d.prepare(`INSERT OR REPLACE INTO einstellungen (schluessel, wert, geaendert)
+ VALUES (?,?,?)`).run(MUENZEN_VERTEILT, "ja", new Date().toISOString());
+ if (a.changes) console.log(`[spenden] Muenzen an ${a.changes} Stufen verteilt.`);
+ }
+ } catch (fehler) {
+ /* Eine nicht verteilte Muenze ist ein Schoenheitsfehler, kein
+ Grund, den Start abzubrechen. */
+ console.error("[spenden] Muenzen verteilen:", fehler?.message);
+ }
+
}
diff --git a/server/workspace-spenden.js b/server/workspace-spenden.js
index 57200c21..d19e105f 100644
--- a/server/workspace-spenden.js
+++ b/server/workspace-spenden.js
@@ -46,6 +46,7 @@ import {
VORLAGEN, STUFEN_MAX, DAUER_MS_MIN, DAUER_MS_MAX,
GROESSE_MIN, GROESSE_MAX, BILD_MAX_BYTES,
centInMille, milleInCent, ZEICHEN_NAME,
+ DOGEN_SAETZE, DOGEN_STUFEN, SATZ_NAME, muenzeAdresse,
} from "./spenden-tabellen.js";
const BILDER = join(DATEN_ORDNER, "spendenbilder");
@@ -284,7 +285,7 @@ function alsKarte(z) {
die eine Karte zeigt, an zwei Dinge denken muss. Die Karte
traegt alles, was sie braucht; genau das verspricht
spendenkarte.js in ihrem Kopf. */
- zeichen: zeichenAdresse(),
+ zeichen: zeichenSatz(),
name: z.name,
gruss: z.gruss,
herkunft: z.herkunft,
@@ -307,15 +308,98 @@ function bildAdresse(datei) {
return datei ? `/workspace/api/buehne/spendenbild/${datei}` : "";
}
-/** Das Dogen-Zeichen, wenn eines hochgeladen wurde.
+/* ======================================================================
+ WELCHE MUENZE ZU WELCHER SPENDE
+
+ Filipe: „ich werde die drei varianten schicken fuer niedrige
+ spenden. mittlere und hohe spenden."
+
+ ---------------------------------------------------------------------
+ DIE GRENZEN WERDEN ABGELEITET UND NICHT GEPFLEGT
+
+ „Niedrig, mittel, hoch" gibt es im Haus schon -- das ist die
+ Stufenleiter. Zwei eigene Grenzen daneben waeren eine zweite
+ Antwort auf dieselbe Frage, und spaetestens wenn Filipe eine Stufe
+ verschiebt, zeigte die Muenze etwas anderes an als der Name auf
+ der Karte.
+
+ Die Leiter wird deshalb in DRITTEL geteilt. Bei den drei Stufen ab
+ Werk ist das genau eine je Muenze. Bei sechs Stufen sind es zwei
+ je Muenze -- und bei einer einzigen gibt es kein Oben und Unten,
+ dann steht ueberall die mittlere.
+ ====================================================================== */
+function zeichenGrenzen() {
+ try {
+ const leiter = db().prepare("SELECT ab_cent FROM spenden_stufen ORDER BY ab_cent")
+ .all().map((z) => centInMille(z.ab_cent));
+ const n = leiter.length;
+ /* Ohne Leiter und bei genau einer Stufe gibt es keine Rangfolge:
+ alles bekommt die mittlere Muenze. Die Untergrenze 0 erreicht
+ niemand (Betraege sind immer groesser), die Obergrenze ist
+ unerreichbar gross -- also faellt alles dazwischen. */
+ if (n < 2) return [0, Number.MAX_SAFE_INTEGER];
+ if (n === 2) return [leiter[1], leiter[1]];
+ return [leiter[Math.floor(n / 3)], leiter[Math.floor((2 * n) / 3)]];
+ } catch { return [0, Number.MAX_SAFE_INTEGER]; }
+}
+
+/** Der gewaehlte Satz -- mit allen drei Muenzen und den Grenzen.
*
- * Filipe: „die symbole schick ich dir spaeter."
+ * ==== EINE FORM UEBERALL
*
- * Bis dahin ist die Antwort leer, und die Oberflaeche zeichnet
- * selbst eine Muenze. Ein Platzhalterbild, das wie eine kaputte
- * Datei aussieht, waere schlechter als eine saubere Zeichnung. */
-function zeichenAdresse() {
- return bildAdresse(einstellung(ZEICHEN_NAME, ""));
+ * Die Karte, die Listen, die Knoepfe und der eigene Stand zeigen
+ * alle dieselbe Muenze zur selben Zahl. Wuerde der Server fuer die
+ * Karte EINE Adresse schicken und fuer den Rest die Auswahl, gaebe
+ * es zwei Formen desselben Dinges -- und die Oberflaeche muesste an
+ * jeder Stelle wissen, welche gerade kommt. Also immer die Auswahl;
+ * wer sie benutzt, rechnet aus der Zahl.
+ *
+ * „eigen" ist der vierte Satz: ein hochgeladenes Bild, das fuer
+ * alle drei Stufen gilt. Wer ein einzelnes Zeichen hochlaedt, meint
+ * ein Zeichen und keine Rangfolge. */
+function zeichenSatz() {
+ const eigen = einstellung(ZEICHEN_NAME, "");
+ const gewuenscht = einstellung(SATZ_NAME, "") || DOGEN_SAETZE[0].schluessel;
+ /* WAS NICHT GEHT, FAELLT AUF DEN ERSTEN SATZ ZURUECK -- etwa
+ „eigen", nachdem das Bild geloescht wurde. Ein leeres Zeichen
+ waere sonst ein kaputtes Bild neben jeder Zahl. */
+ const satz = gewuenscht === "eigen" && eigen ? "eigen"
+ : DOGEN_SAETZE.some((x) => x.schluessel === gewuenscht) ? gewuenscht
+ : DOGEN_SAETZE[0].schluessel;
+
+ const bild = (stufe) => (satz === "eigen"
+ ? bildAdresse(eigen)
+ : muenzeAdresse(satz, stufe));
+ return {
+ satz,
+ eigen: eigen ? bildAdresse(eigen) : "",
+ niedrig: bild("niedrig"),
+ mittel: bild("mittel"),
+ hoch: bild("hoch"),
+ grenzen: zeichenGrenzen(),
+ };
+}
+
+/** Alle Saetze zum Aussuchen -- Filipe: „alle drei varianten will
+ * ich drauf so dass ich sie aussuchen kann wie ich will." */
+function alleSaetze() {
+ const eigen = einstellung(ZEICHEN_NAME, "");
+ const liste = DOGEN_SAETZE.map((x) => ({
+ ...x,
+ bilder: Object.fromEntries(DOGEN_STUFEN.map((st) => [st, muenzeAdresse(x.schluessel, st)])),
+ }));
+ /* Der eigene Satz steht nur da, wenn es ihn gibt. Ein leerer
+ vierter Knopf waere eine Auswahl, die nichts auswaehlt. */
+ if (eigen) {
+ const a = bildAdresse(eigen);
+ liste.push({
+ schluessel: "eigen",
+ name: "Eigenes Zeichen",
+ wort: "Dein hochgeladenes Bild, fuer alle drei Stufen",
+ bilder: Object.fromEntries(DOGEN_STUFEN.map((st) => [st, a])),
+ });
+ }
+ return liste;
}
/** Wen die Leitung einer Spende zuordnen kann.
@@ -374,9 +458,12 @@ spendenRouter.get("/workspace/api/spenden", (req, res) => {
umgestellt wurden, waere genau diese Kopie die stehen
gebliebene gewesen. */
stufen: stufenLesen(),
- /* Das Zeichen, das neben jeder Zahl steht. Leer, solange
- keines hochgeladen ist. */
- zeichen: zeichenAdresse(),
+ /* Die Muenzen, die neben jeder Zahl stehen -- und die zwei
+ Grenzen, an denen sich entscheidet, welche. */
+ zeichen: zeichenSatz(),
+ /* Zum Aussuchen: nur fuer die Leitung. Ein Zuschauer soll das
+ Bild sehen, nicht die Auswahl daneben. */
+ saetze: darf ? alleSaetze() : null,
/* WAS SICH EINSTELLEN LAESST, KOMMT VOM SERVER. Stuenden die
Vorlagennamen und die Grenzen auch in der Oberflaeche, waeren
es zwei Listen -- und die eine liefe irgendwann von der
@@ -843,6 +930,38 @@ spendenRouter.post("/workspace/api/spenden/stufe/:id(\\d+)/bild", nurGeld,
ohne Schranke ausliefert. Ein Zeichen, das nur angemeldet
sichtbar waere, fehlte ausgerechnet im Stream.
====================================================================== */
+/** Einen Satz auswaehlen. */
+spendenRouter.put("/workspace/api/spenden/zeichen/satz", nurGeld,
+ express.json({ limit: "2kb" }), (req, res) => {
+ try {
+ const satz = String(req.body?.satz ?? "");
+ const geht = DOGEN_SAETZE.some((x) => x.schluessel === satz)
+ || (satz === "eigen" && einstellung(ZEICHEN_NAME, ""));
+ /* EIN SATZ, DEN ES NICHT GIBT, WIRD ABGELEHNT -- und „eigen"
+ ohne hochgeladenes Bild gibt es nicht. Sonst stuende neben
+ jeder Zahl ein Bild, das 404 antwortet. */
+ if (!geht) return res.status(400).json({ fehler: "satz_unbekannt" });
+ einstellungSetzen(SATZ_NAME, satz, req.person);
+ /* ==== UND ALLE ERFAHREN ES ================================
+ Die KARTEN tragen ihren Satz selbst mit und waren immer
+ richtig. Die Muenzen an den Spendenknoepfen und beim eigenen
+ Stand aber nicht: Die holt sich eine Seite nur bei einer
+ Spende neu, und bis dahin stuende beim Zuschauer noch der
+ alte Satz -- und in einem zweiten Regiefenster auch.
+ Eine Einstellung, die nur dort ankommt, wo sie gemacht
+ wurde, ist keine Einstellung des Hauses. */
+ stromAn(empfaenger(), "spende", { was: "zeichen" });
+ buehneMelden("spende", { was: "zeichen" });
+ protokolliere("dogen_satz", {
+ personId: req.person.id, detail: satz, ip: echteIp(req),
+ });
+ res.json({ ok: true, zeichen: zeichenSatz(), saetze: alleSaetze() });
+ } catch (fehler) {
+ console.error("[spenden] Satz:", fehler?.message);
+ res.status(503).json({ fehler: "nicht_verfuegbar" });
+ }
+ });
+
spendenRouter.post("/workspace/api/spenden/zeichen", nurGeld,
express.raw({ type: "*/*", limit: BILD_MAX_BYTES }), zuGross, (req, res) => {
try {
@@ -855,6 +974,11 @@ spendenRouter.post("/workspace/api/spenden/zeichen", nurGeld,
const alt = einstellung(ZEICHEN_NAME, "");
einstellungSetzen(ZEICHEN_NAME, name, req.person);
+ /* WER EIN ZEICHEN HOCHLAEDT, WILL ES AUCH SEHEN. Ohne diese
+ Zeile laege es in den Einstellungen und neben den Zahlen
+ stuende weiter die Muenze -- und Filipe suchte den Fehler
+ beim Hochladen. */
+ einstellungSetzen(SATZ_NAME, "eigen", req.person);
/* Das alte wegraeumen -- sonst bleibt bei jedem Wechsel eine
Datei liegen, die niemand mehr findet. */
if (alt && alt !== name) {
@@ -865,7 +989,7 @@ spendenRouter.post("/workspace/api/spenden/zeichen", nurGeld,
detail: `${Math.round(req.body.length / 1024)} KB`,
ip: echteIp(req),
});
- res.json({ ok: true, zeichen: zeichenAdresse() });
+ res.json({ ok: true, zeichen: zeichenSatz(), saetze: alleSaetze() });
} catch (fehler) {
console.error("[spenden] Zeichen:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
@@ -876,8 +1000,13 @@ spendenRouter.delete("/workspace/api/spenden/zeichen", nurGeld, (req, res) => {
try {
const alt = einstellung(ZEICHEN_NAME, "");
einstellungSetzen(ZEICHEN_NAME, "", req.person);
+ /* UND ZURUECK AUF EINEN SATZ, DEN ES GIBT. Bliebe „eigen"
+ stehen, zeigte jede Zahl ein Bild, das es nicht mehr gibt. */
+ if (einstellung(SATZ_NAME, "") === "eigen") {
+ einstellungSetzen(SATZ_NAME, DOGEN_SAETZE[0].schluessel, req.person);
+ }
if (alt) { try { unlinkSync(join(BILDER, alt)); } catch { /* war weg */ } }
- res.json({ ok: true, zeichen: "" });
+ res.json({ ok: true, zeichen: zeichenSatz(), saetze: alleSaetze() });
} catch (fehler) {
console.error("[spenden] Zeichen loeschen:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
@@ -923,7 +1052,7 @@ spendenRouter.post("/workspace/api/spenden/probe", nurGeld,
const karte = {
id: 0,
dogen_mille: centInMille(cent),
- zeichen: zeichenAdresse(),
+ zeichen: zeichenSatz(),
/* KENNTLICH ALS PROBE. Stuende hier ein Name, waere auf dem
Bildschirmfoto nicht mehr zu unterscheiden, ob jemand
gespendet hat. */
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index a2ad7290..7b851ceb 100644
--- a/workspace/anruf-probe.html
+++ b/workspace/anruf-probe.html
@@ -16,8 +16,8 @@
verdeckt. Gefunden hat es pruef-struktur.
Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. -->
-
-
+
+
-
+