diff --git a/server/mess-buehne.mjs b/server/mess-buehne.mjs
index f7d3aea2..12b24289 100644
--- a/server/mess-buehne.mjs
+++ b/server/mess-buehne.mjs
@@ -324,7 +324,7 @@ if (grund.fehler) {
/* ---------- Laeuft eine Karte an? --------------------------------------- */
await roh("/workspace/api/spenden", {
method: "POST", keks: k.admin,
- rumpf: { betrag_cent: 2500, name: "Nachtfalter", gruss: "Gruss an die Buehne" },
+ rumpf: { dogen_mille: 2500, name: "Nachtfalter", gruss: "Gruss an die Buehne" },
});
await tafelSeite.waitForFunction(
() => document.querySelector('.spendenkarte[data-da="ja"]'), null, { timeout: 12000 },
@@ -335,15 +335,30 @@ const karte = await tafelSeite.evaluate(() => {
if (!k2) return null;
const r = k2.getBoundingClientRect();
return {
- betrag: k2.querySelector(".spendenkarte__betrag")?.textContent?.trim(),
+ /* Zahl, Wort und Zeichen getrennt -- `textContent` klebt sie
+ sonst zu „2,5Dogen" zusammen, und eine Suche darin traefe
+ frueher oder spaeter das Falsche. */
+ zahl: k2.querySelector(".dogen__zahl")?.textContent?.trim(),
+ wort: k2.querySelector(".dogen__wort")?.textContent?.trim(),
+ zeichen: !!k2.querySelector(".dogen__zeichen"),
wer: k2.querySelector(".spendenkarte__wer")?.textContent?.trim(),
stufe: k2.querySelector(".spendenkarte__stufe")?.textContent?.trim(),
breite: Math.round(r.width), hoehe: Math.round(r.height),
};
});
console.log(` Karte auf der Tafel: ${JSON.stringify(karte)}`);
-if (!karte || !/25/.test(karte.betrag || "")) {
- console.log(" ACHTUNG: Die Karte laeuft auf der Tafel nicht an.");
+if (!karte || karte.zahl !== "2,5" || karte.wort !== "Dogen" || !karte.zeichen) {
+ console.log(" ACHTUNG: Die Karte laeuft auf der Tafel nicht an -- "
+ + "oder ohne Dogen und Zeichen.");
+ ausgang = 1;
+}
+/* IM STREAM DARF KEIN GELD STEHEN. Die Tafel ist die einzige
+ Flaeche, die wirklich jeder sieht. */
+const geldImStream = await tafelSeite.evaluate(() =>
+ /€|EUR|Euro/.test(document.body.textContent || ""));
+console.log(` Geld im Streambild: ${geldImStream}`);
+if (geldImStream) {
+ console.log(" ACHTUNG: Auf der Buehnentafel steht Geld.");
ausgang = 1;
}
await tafelSeite.screenshot({ path: join(HIER, "mess-buehne-1-tafel.png") });
diff --git a/server/mess-reaktion.mjs b/server/mess-reaktion.mjs
index 389751de..45e97ce4 100644
--- a/server/mess-reaktion.mjs
+++ b/server/mess-reaktion.mjs
@@ -555,6 +555,14 @@ if ((spLage.j?.knoepfe || []).length !== 4) {
ausgang = 1;
}
+/* DIE ZUSCHAUERSEITE WIRD NEU GELADEN. Sie wurde geoeffnet, BEVOR
+ die PayPal-Adresse gesetzt war -- ohne sie gibt es keinen
+ Geben-Kasten, und der ist ab hier das Gemessene. Ein Neuladen ist
+ dabei nicht geschummelt, sondern der Normalfall: So kommt jeder
+ an, der spaeter dazustoesst. */
+await zuschauer.seite.reload({ waitUntil: "domcontentloaded" });
+await zuschauer.seite.waitForTimeout(1800);
+
/* 1. Von Hand eintragen -- die Karte muss bei der Zuschauerin laufen. */
await zuschauer.seite.evaluate(() => { window.__karten = []; });
await zuschauer.seite.evaluate(() => {
@@ -564,9 +572,16 @@ await zuschauer.seite.evaluate(() => {
for (const n of e.addedNodes) {
if (n.querySelector) {
window.__karten.push({
- betrag: n.querySelector(".spendenkarte__betrag")?.textContent?.trim(),
+ /* NUR DIE ZAHL, NICHT DIE GANZE ZEILE. Seit dem
+ 28.09. steht neben ihr ein Zeichen und das Wort
+ „Dogen"; `textContent` klebte beides zusammen
+ („2,5Dogen"), und eine Suche mit regulaerem Ausdruck
+ darin traefe frueher oder spaeter das Falsche. */
+ betrag: n.querySelector(".dogen__zahl")?.textContent?.trim(),
+ wort: n.querySelector(".dogen__wort")?.textContent?.trim(),
wer: n.querySelector(".spendenkarte__wer")?.textContent?.trim(),
stufe: n.querySelector(".spendenkarte__stufe")?.textContent?.trim(),
+ gruss: n.querySelector(".spendenkarte__gruss")?.textContent?.trim(),
});
}
}
@@ -576,7 +591,7 @@ await zuschauer.seite.evaluate(() => {
await roh("/workspace/api/spenden", {
method: "POST", keks: k.admin,
- rumpf: { betrag_cent: 2500, name: "Nachtfalter", gruss: "Fuer das Rudel!" },
+ rumpf: { dogen_mille: 2500, name: "Nachtfalter", gruss: "Fuer das Rudel!" },
});
await zuschauer.seite.waitForFunction(
() => document.querySelector('.spendenkarte[data-da="ja"]'), null, { timeout: 8000 },
@@ -584,19 +599,123 @@ await zuschauer.seite.waitForFunction(
await zuschauer.seite.waitForTimeout(700);
await bild(zuschauer.seite, "10-spendenkarte");
-/* 2. Eine Meldung ohne Bestaetigung darf NICHT laufen. */
-await roh("/workspace/api/spenden/melden", {
- method: "POST", keks: k.gast, rumpf: { betrag_cent: 50000 },
+/* ==== 2. DER ZUSCHAUER GIBT UND SCHREIBT SELBST ==================
+
+ Filipe: „der text der dazu erscheint sollen die leute selber
+ schreiben koennen ... es sollen personalisierte texte sein von den
+ leuten selbst."
+
+ Gemessen wird der WEG DURCH DIE OBERFLAECHE und nicht der Aufruf
+ dahinter: Ein Knopf, den man drueckt, ein Feld, das man ausfuellt,
+ ein Text, der ankommt. Der Aufruf allein haette nie gezeigt, ob
+ das Feld ueberhaupt existiert.
+
+ Und dieselbe Meldung beantwortet die alte Frage gleich mit: Sie
+ ist UNBESTAETIGT und darf deshalb NICHT im Bild stehen. */
+const geben = await zuschauer.seite.evaluate(() => {
+ const k2 = document.getElementById("geben");
+ const knoepfe = [...document.querySelectorAll(".geben__betrag")];
+ return {
+ 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,
+ /* Und nirgends ein Euro. */
+ geld: /€|EUR|Euro/.test(k2?.textContent || ""),
+ };
});
-await zuschauer.seite.waitForTimeout(1200);
+console.log(` Geben-Kasten: ${geben.knoepfe} Knoepfe (${geben.zahlen.join(" · ")}), `
+ + `${geben.zeichen} mit Zeichen, Geld im Text: ${geben.geld}`);
+if (!geben.da || geben.knoepfe !== 4 || geben.zeichen !== 4 || geben.geld
+ || geben.zahlen[0] !== "0,2") {
+ console.log(" ACHTUNG: Der Geben-Kasten zeigt nicht, was er soll.");
+ ausgang = 1;
+}
+
+/* ==== DIE PROBE AENDERT NUR DAS, WAS SIE AENDERN MUSS ============
+ Der Knopf ist ein Verweis mit `target="_blank"` und fuehrt zu
+ PayPal. Ein Klick in der Messung wuerde also ins Netz gehen. Ein
+ Zuhoerer in der EINFANGPHASE nimmt nur die Navigation weg -- der
+ Zuhoerer der Seite laeuft danach ganz normal.
+
+ Das ist die Lehre vom 28.09. in klein: Eine Probe, die mehr
+ aendert als das, wonach man sucht, beantwortet eine andere
+ Frage. Hier wird kein Ereignis nachgebaut und keine Funktion
+ direkt gerufen -- es wird wirklich geklickt. */
+await zuschauer.seite.evaluate(() => {
+ document.addEventListener("click", (e) => {
+ if (e.target.closest && e.target.closest("a.geben__betrag")) e.preventDefault();
+ }, true);
+});
+await zuschauer.seite.click(".geben__betrag");
+await zuschauer.seite.waitForTimeout(400);
+
+const felder = await zuschauer.seite.evaluate(() => {
+ const name = document.getElementById("geben-name");
+ const text = document.getElementById("geben-text");
+ return {
+ offen: !document.getElementById("geben-nach").hidden,
+ frage: document.getElementById("geben-frage")?.textContent?.trim() || "",
+ knopf: document.getElementById("geben-melden")?.textContent?.trim() || "",
+ name: name?.value || "",
+ grenze: text?.maxLength || 0,
+ /* Am Finger muessen beide Felder gross genug sein. */
+ hoch: Math.round(text?.getBoundingClientRect().height || 0),
+ };
+});
+console.log(` Nach dem Tippen: „${felder.frage}" · Knopf „${felder.knopf}"`);
+console.log(` Felder: Name „${felder.name}", Text bis ${felder.grenze} Zeichen, `
+ + `${felder.hoch} px hoch`);
+if (!felder.offen || felder.grenze !== 140 || felder.name !== "Nachtfalter"
+ || /€|EUR/.test(felder.frage) || !felder.frage.includes("Dogen")) {
+ console.log(" ACHTUNG: Die Felder fuer den eigenen Text stimmen nicht.");
+ ausgang = 1;
+}
+
+/* Der Zaehler steht erst da, wenn es eng wird -- vorher waere er
+ eine Aufsicht statt einer Hilfe. */
+const kurz = await zuschauer.seite.evaluate(() => {
+ const t = document.getElementById("geben-text");
+ t.value = "Kurz";
+ t.dispatchEvent(new Event("input", { bubbles: true }));
+ return document.getElementById("geben-rest")?.textContent || "";
+});
+const lang = await zuschauer.seite.evaluate(() => {
+ const t = document.getElementById("geben-text");
+ t.value = "Danke fuer den Abend, ihr Lieben! Bleibt genau so wie ihr seid "
+ + "-- das hier ist das Beste am ganzen Tag, ehrlich jetzt.";
+ t.dispatchEvent(new Event("input", { bubbles: true }));
+ return document.getElementById("geben-rest")?.textContent || "";
+});
+console.log(` Zaehler: bei „Kurz" „${kurz}", bei langem Text „${lang}"`);
+if (kurz !== "" || !lang.startsWith("noch ")) {
+ console.log(" ACHTUNG: Der Zaehler erscheint zur falschen Zeit.");
+ ausgang = 1;
+}
+
+await zuschauer.seite.click("#geben-melden");
+await zuschauer.seite.waitForTimeout(1400);
+const danach = await zuschauer.seite.evaluate(() => ({
+ danke: document.getElementById("geben-danke")?.textContent?.trim() || "",
+ offen: !document.getElementById("geben-nach").hidden,
+ leer: document.getElementById("geben-text")?.value === "",
+}));
+console.log(` Danach: „${danach.danke}"`);
+if (danach.offen || !danach.leer || !danach.danke.includes("Text")) {
+ console.log(" ACHTUNG: Nach dem Melden stimmt die Antwort nicht.");
+ ausgang = 1;
+}
const nachMelden = await zuschauer.seite.evaluate(() => window.__karten);
console.log(` Karten bei der Zuschauerin: ${JSON.stringify(nachMelden)}`);
-if (nachMelden.length !== 1 || !/25/.test(nachMelden[0]?.betrag || "")) {
+if (nachMelden.length !== 1 || nachMelden[0]?.betrag !== "2,5") {
console.log(" ACHTUNG: Die eingetragene Spende kommt nicht als Karte an.");
ausgang = 1;
}
-if (nachMelden.some((k2) => /500/.test(k2.betrag || ""))) {
+if (nachMelden.some((k2) => k2.betrag === "0,2")) {
console.log(" ACHTUNG: Eine UNBESTAETIGTE Meldung stand im Bild.");
ausgang = 1;
}
@@ -604,13 +723,41 @@ if (nachMelden.some((k2) => /500/.test(k2.betrag || ""))) {
/* 3. Bestaetigen -- jetzt muss sie laufen. */
const offen = await roh("/workspace/api/spenden", { keks: k.admin });
const wartet = (offen.j?.offen || [])[0];
-console.log(` Im Pult wartet: ${wartet ? wartet.betrag_cent / 100 + " EUR von " + wartet.name : "(nichts)"}`);
+console.log(` Im Pult wartet: ${wartet ? wartet.dogen_mille / 1000 + " Dogen von " + wartet.name : "(nichts)"}`);
if (!wartet) {
console.log(" ACHTUNG: Die Meldung kommt nicht im Pult an.");
ausgang = 1;
} else {
+ /* ==== DIE SCHRANKE VOR DEM STREAM ============================
+ Der Text kommt von einem Fremden und steht gleich im Bild.
+ Geprueft wird deshalb, dass er (a) im Pult ANKOMMT, (b) sich
+ dort AENDERN laesst und (c) genau so auf der Karte landet. */
+ console.log(` Der Zuschauer schrieb: „${(wartet.gruss || "").slice(0, 60)}…"`);
+ if (!wartet.gruss || !wartet.gruss.startsWith("Danke fuer den Abend")) {
+ console.log(" ACHTUNG: Der selbst geschriebene Text kommt nicht im Pult an.");
+ ausgang = 1;
+ }
+ const inFeldern = await host.seite.evaluate(() => {
+ const z = document.querySelector("#sp-offen .spendenzeile");
+ const f = z ? [...z.querySelectorAll(".spendenzeile__feld")] : [];
+ return {
+ felder: f.length,
+ wert: f[1]?.value || "",
+ grenze: f[1]?.maxLength || 0,
+ zahl: z?.querySelector(".dogen__zahl")?.textContent || "",
+ };
+ });
+ console.log(` Im Pult: ${inFeldern.felder} Felder, Text bis ${inFeldern.grenze} `
+ + `Zeichen, Zahl „${inFeldern.zahl}"`);
+ if (inFeldern.felder !== 2 || inFeldern.grenze !== 140
+ || inFeldern.zahl !== "0,2") {
+ console.log(" ACHTUNG: Die Leitung kann den Text nicht sehen oder nicht aendern.");
+ ausgang = 1;
+ }
+
await roh(`/workspace/api/spenden/${wartet.id}/bestaetigen`, {
- method: "POST", keks: k.admin, rumpf: { betrag_cent: 1000 },
+ method: "POST", keks: k.admin,
+ rumpf: { dogen_mille: 1000, gruss: "Danke fuer den Abend!" },
});
/* AUF DAS MERKMAL WARTEN, NICHT AUF DIE UHR. Die erste Karte
laeuft elf Sekunden (die hoechste Stufe steht am laengsten); die
@@ -621,18 +768,66 @@ if (!wartet) {
).catch(() => {});
const nachJa = await zuschauer.seite.evaluate(() => window.__karten);
console.log(` Nach dem Bestaetigen: ${JSON.stringify(nachJa.slice(-1))}`);
- if (nachJa.length !== 2 || !/10/.test(nachJa[1]?.betrag || "")) {
+ if (nachJa.length !== 2 || nachJa[1]?.betrag !== "1") {
console.log(" ACHTUNG: Die bestaetigte Spende laeuft nicht -- oder mit falschem Betrag.");
ausgang = 1;
}
- /* DIE STUFE MUSS ZUM BETRAG PASSEN. 25 EUR ist die hoechste
- („Rudel-Legende"), 10 EUR die mittlere („Starke Runde"). Ohne
+ /* DIE STUFE MUSS ZUR ZAHL PASSEN. 2,5 Dogen ist die hoechste
+ („Rudel-Legende"), 1 Doge die mittlere („Starke Runde"). Ohne
diese Probe koennte jede Karte dieselbe Stufe tragen und es
faellt niemandem auf. */
console.log(` Stufen: ${nachMelden[0]?.stufe} / ${nachJa[1]?.stufe}`);
+ /* EINZAHL UND MEHRZAHL. „1 Dogen" waere die Sorte Fehler, die
+ jeder sieht und niemand meldet. */
+ console.log(` Einheit: „${nachMelden[0]?.wort}" bei 2,5 · `
+ + `„${nachJa[1]?.wort}" bei 1`);
+ if (nachMelden[0]?.wort !== "Dogen" || nachJa[1]?.wort !== "Doge") {
+ console.log(" ACHTUNG: Einzahl und Mehrzahl stimmen nicht.");
+ ausgang = 1;
+ }
/* Der Text, nicht das Bild: Grossbuchstaben macht das Stilblatt. */
if (nachMelden[0]?.stufe !== "Rudel-Legende" || nachJa[1]?.stufe !== "Starke Runde") {
- console.log(" ACHTUNG: Die Stufe passt nicht zum Betrag.");
+ console.log(" ACHTUNG: Die Stufe passt nicht zur Zahl.");
+ ausgang = 1;
+ }
+
+ /* ==== DER BERICHTIGTE TEXT STEHT AUF DER KARTE ================
+ Nicht der, den der Zuschauer geschrieben hat, sondern der, den
+ die Leitung freigegeben hat. Andersherum waere die ganze
+ Schranke wirkungslos -- und niemandem fiele es auf, bis es
+ einmal zaehlt. */
+ console.log(` Auf der Karte steht: „${nachJa[1]?.gruss}"`);
+ if (nachJa[1]?.gruss !== "Danke fuer den Abend!") {
+ console.log(" ACHTUNG: Der freigegebene Text steht nicht auf der Karte.");
+ ausgang = 1;
+ }
+
+ /* ==== UND DIE PUNKTE SIND SOFORT DA ===========================
+ Filipe: „ich will dass die den leuten auch als punkte
+ hinzugefuegt werden."
+
+ GEMESSEN BEIM ZUSCHAUER UND OHNE NEULADEN. Genau hier war ein
+ Loch: `spendenHolen()` ruft bei einer Zuschauerin niemand auf,
+ das tut nur der Host -- ihr Stand waere erst beim naechsten
+ Seitenaufruf gewachsen. „Hinzugefuegt werden" heisst sichtbar
+ hinzugefuegt. */
+ await zuschauer.seite.waitForFunction(
+ () => !document.getElementById("geben-stand")?.hidden, null, { timeout: 8000 },
+ ).catch(() => {});
+ const stand = await zuschauer.seite.evaluate(() => {
+ const p = document.getElementById("geben-stand");
+ return {
+ da: !!p && !p.hidden,
+ zahl: p?.querySelector(".dogen__zahl")?.textContent || "",
+ wort: p?.querySelector(".dogen__wort")?.textContent || "",
+ zeichen: !!p?.querySelector("svg.dogen__zeichen"),
+ text: p?.textContent?.trim() || "",
+ };
+ });
+ console.log(` Eigener Stand ohne Neuladen: „${stand.text}" `
+ + `(Zeichen: ${stand.zeichen})`);
+ if (!stand.da || stand.zahl !== "1" || stand.wort !== "Doge" || !stand.zeichen) {
+ console.log(" ACHTUNG: Der eigene Dogen-Stand kommt nicht an.");
ausgang = 1;
}
}
diff --git a/server/pruef-reaktion.mjs b/server/pruef-reaktion.mjs
index ebe9ba0d..5e018db8 100644
--- a/server/pruef-reaktion.mjs
+++ b/server/pruef-reaktion.mjs
@@ -1637,15 +1637,42 @@ melde("=== 18. Die Regieleiste ===");
"im Buehnenmodus ist sie weg");
/* ---- NUR EINE STELLE SAGT, WIE HOCH DIE ZEILEN SIND ----
+
Bis zum 28.09. standen zwei Angaben in derselben Datei; die
- spaetere gewann und gab der Leiste null Pixel. Zwei Regeln fuer
- dieselbe Frage sind keine Sicherheit. */
- const zeilen = [...css.matchAll(/^\s*\.saal[^{]*\{[^}]*grid-template-rows/gm)]
- .concat([...css.matchAll(/^\.saal \{ grid-template-rows/gm)]);
+ spaetere gewann und gab der Leiste null Pixel.
+
+ DIESE PRUEFUNG WAR ZU ENG UND HAT DESHALB DAS FALSCHE GEPRUEFT.
+ Sie verlangte, dass die Zeile mit `.saal` BEGINNT -- und hat
+ damit `body[data-nur="buehne"] .saal { grid-template-rows: … }`
+ nie gesehen. Die stand die ganze Zeit da, gab dem Saal im
+ Buehnenmodus eine einzige Zeile, und das auf Zeile 2 genagelte
+ Video landete in einer impliziten: Leinwand null Pixel hoch, im
+ Stream ein schwarzes Bild. Gemeldet hat es `mess-buehne`, nicht
+ diese Pruefung -- sie war gruen und wertlos.
+
+ Jetzt zaehlt JEDE Regel, in deren Auswahl `.saal` vorkommt. */
+ const zeilen = [...css.matchAll(/\{|\}/g)].length >= 0
+ ? [...css.replace(/\/\*[\s\S]*?\*\//g, " ")
+ .matchAll(/([^{}]*\.saal[^{}]*)\{([^}]*)\}/g)]
+ .filter((m) => /grid-template-rows/.test(m[2]))
+ : [];
ok(zeilen.length === 1,
zeilen.length === 1
- ? "genau eine Regel bestimmt die Zeilen des Saals"
- : `${zeilen.length} Regeln bestimmen die Zeilen -- eine davon wird ignoriert`);
+ ? `genau eine Regel bestimmt die Zeilen des Saals (${zeilen[0][1].trim()})`
+ : `${zeilen.length} Regeln bestimmen die Zeilen des Saals -- `
+ + `eine davon wird irgendwo ignoriert: `
+ + zeilen.map((m) => m[1].trim()).join(" / "));
+ /* GEGENPROBE: Die Pruefung muss eine zweite Regel auch dann
+ finden, wenn sie nicht am Zeilenanfang steht -- genau daran ist
+ sie gescheitert. */
+ {
+ const erfunden = 'body[data-nur="probe"] .saal { grid-template-rows: 1fr; }';
+ const mit = [...(css + erfunden).replace(/\/\*[\s\S]*?\*\//g, " ")
+ .matchAll(/([^{}]*\.saal[^{}]*)\{([^}]*)\}/g)]
+ .filter((m) => /grid-template-rows/.test(m[2]));
+ ok(mit.length === 2,
+ `Gegenprobe: eine eingeschobene zweite Regel faellt auf (${mit.length})`);
+ }
/* ---- UND SIE GEHOERT NUR DENEN, DIE SENDEN ---- */
ok(/leiste\.hidden = !lage\.ich\.host/.test(js),
diff --git a/server/pruef-spenden.mjs b/server/pruef-spenden.mjs
index 465cd0ba..8f494611 100644
--- a/server/pruef-spenden.mjs
+++ b/server/pruef-spenden.mjs
@@ -157,10 +157,10 @@ melde("=== 1. Rechnen mit Geld ===");
/* DIE HOECHSTE STUFE, DIE NOCH PASST. */
ok(stufeFuer(50) === null, "unter der kleinsten Stufe gibt es keine");
- ok(stufeFuer(100)?.name === "Danke", `1 EUR ergibt ${stufeFuer(100)?.name}`);
- ok(stufeFuer(499)?.name === "Danke", "4,99 EUR bleibt darunter");
- ok(stufeFuer(500)?.name === "Starke Runde", `5 EUR ergibt ${stufeFuer(500)?.name}`);
- ok(stufeFuer(5000)?.name === "Rudel-Legende", `50 EUR ergibt ${stufeFuer(5000)?.name}`);
+ ok(stufeFuer(100)?.name === "Danke", `0,1 Dogen ergibt ${stufeFuer(100)?.name}`);
+ ok(stufeFuer(499)?.name === "Danke", "0,499 Dogen bleibt darunter");
+ ok(stufeFuer(500)?.name === "Starke Runde", `0,5 Dogen ergibt ${stufeFuer(500)?.name}`);
+ ok(stufeFuer(5000)?.name === "Rudel-Legende", `5 Dogen ergibt ${stufeFuer(5000)?.name}`);
}
/* =======================================================================
@@ -222,17 +222,17 @@ melde("");
melde("=== 4. Melden ===");
{
const mist = await roh(CREW, "/workspace/api/spenden/melden", {
- method: "POST", keks: k.gast, rumpf: { betrag_cent: 0 },
+ method: "POST", keks: k.gast, rumpf: { dogen_mille: 0 },
});
ok(mist.code === 400 && mist.j?.fehler === "betrag_unklar",
`null Euro ist kein Betrag (${mist.code}/${mist.j?.fehler})`);
const zuViel = await roh(CREW, "/workspace/api/spenden/melden", {
- method: "POST", keks: k.gast, rumpf: { betrag_cent: 99999999 },
+ method: "POST", keks: k.gast, rumpf: { dogen_mille: 99999999 },
});
ok(zuViel.code === 400, `und eine Million auch nicht (${zuViel.code})`);
const a = await roh(CREW, "/workspace/api/spenden/melden", {
- method: "POST", keks: k.gast, rumpf: { betrag_cent: 1500, gruss: "Danke fuer alles" },
+ method: "POST", keks: k.gast, rumpf: { dogen_mille: 1500, gruss: "Danke fuer alles" },
});
ok(a.code === 201 && a.j?.wartet === true,
`die Meldung wird angenommen -- und sie WARTET (${a.code})`);
@@ -241,14 +241,14 @@ melde("=== 4. Melden ===");
-- sondern gegen den Doppelklick und gegen eine Liste, die man
nicht mehr durchsieht. */
const zweite = await roh(CREW, "/workspace/api/spenden/melden", {
- method: "POST", keks: k.gast, rumpf: { betrag_cent: 1500 },
+ method: "POST", keks: k.gast, rumpf: { dogen_mille: 1500 },
});
ok(zweite.code === 429, `zweimal in derselben Minute nicht (${zweite.code})`);
const beiDerLeitung = await roh(CREW, "/workspace/api/spenden", { keks: k.admin });
const wartet = (beiDerLeitung.j?.offen || [])[0];
- ok(wartet?.betrag_cent === 1500 && wartet?.stand === "offen",
- `sie steht im Pult und wartet (${wartet?.betrag_cent} Cent, ${wartet?.stand})`);
+ ok(wartet?.dogen_mille === 1500 && wartet?.stand === "offen",
+ `sie steht im Pult und wartet (${wartet?.dogen_mille} Tausendstel, ${wartet?.stand})`);
ok(wartet?.name === "Nachtfalter", `mit dem Namen der Person (${wartet?.name})`);
ok(wartet?.stufe?.name === "Starke Runde",
`und der Stufe, die zum Betrag passt (${wartet?.stufe?.name})`);
@@ -273,7 +273,7 @@ melde("=== 5. Wer ueber Geld entscheidet ===");
ok(a.code === 403 && a.j?.fehler === "nur_leitung",
`${rolle}: bestaetigt nichts (${a.code}/${a.j?.fehler})`);
const b = await roh(CREW, "/workspace/api/spenden", {
- method: "POST", keks: k[rolle], rumpf: { betrag_cent: 100, name: "X" },
+ method: "POST", keks: k[rolle], rumpf: { dogen_mille: 100, name: "X" },
});
ok(b.code === 403, `${rolle}: traegt auch nichts ein (${b.code})`);
}
@@ -282,17 +282,17 @@ melde("=== 5. Wer ueber Geld entscheidet ===");
hat die PayPal-Meldung vor sich und weiss es besser als die
Behauptung. */
const ja = await roh(CREW, `/workspace/api/spenden/${id}/bestaetigen`, {
- method: "POST", keks: k.hand, rumpf: { betrag_cent: 1200 },
+ method: "POST", keks: k.hand, rumpf: { dogen_mille: 1200 },
});
- ok(ja.code === 200 && ja.j?.stand === "bestaetigt" && ja.j?.betrag_cent === 1200,
- `die rechte Hand bestaetigt und berichtigt (${ja.j?.betrag_cent} Cent)`);
+ ok(ja.code === 200 && ja.j?.stand === "bestaetigt" && ja.j?.dogen_mille === 1200,
+ `die rechte Hand bestaetigt und berichtigt (${ja.j?.dogen_mille} Tausendstel)`);
/* ZWEIMAL BESTAETIGEN IST KEIN FEHLER -- aber auch kein Grund, die
Karte noch einmal zu zeigen. Zwei Leute tippen gleichzeitig. */
const nochmal = await roh(CREW, `/workspace/api/spenden/${id}/bestaetigen`, {
method: "POST", keks: k.admin,
});
- ok(nochmal.code === 200 && nochmal.j?.betrag_cent === 1200,
+ ok(nochmal.code === 200 && nochmal.j?.dogen_mille === 1200,
"ein zweites Mal aendert nichts und ist kein Fehler");
const danach = await roh(CREW, "/workspace/api/spenden", { keks: k.admin });
@@ -307,14 +307,14 @@ melde("");
melde("=== 6. Von Hand eintragen ===");
{
const ohneName = await roh(CREW, "/workspace/api/spenden", {
- method: "POST", keks: k.admin, rumpf: { betrag_cent: 500 },
+ method: "POST", keks: k.admin, rumpf: { dogen_mille: 500 },
});
ok(ohneName.code === 400 && ohneName.j?.fehler === "name_fehlt",
`ohne Namen keine Karte -- er steht ja darauf (${ohneName.code})`);
const a = await roh(CREW, "/workspace/api/spenden", {
method: "POST", keks: k.admin,
- rumpf: { betrag_cent: 5000, name: "Miss", gruss: "Weiter so!" },
+ rumpf: { dogen_mille: 5000, name: "Miss", gruss: "Weiter so!" },
});
ok(a.code === 201 && a.j?.stand === "bestaetigt",
`eingetragen gilt sofort (${a.code}/${a.j?.stand})`);
@@ -408,8 +408,8 @@ melde("=== 8. Die Gestaltung der Karten ===");
/* ---- Wer gestalten darf ---- */
const guteLeiter = {
stufen: [
- { ab_cent: 100, name: "Danke", vorlage: "herz", farbe: "#8ec9ff", dauer_ms: 6000, groesse: 1 },
- { ab_cent: 2000, name: "Legende", vorlage: "krone", farbe: "#e0a13a", dauer_ms: 11000, groesse: 1.45 },
+ { ab_mille: 100, name: "Danke", vorlage: "herz", farbe: "#8ec9ff", dauer_ms: 6000, groesse: 1 },
+ { ab_mille: 2000, name: "Legende", vorlage: "krone", farbe: "#e0a13a", dauer_ms: 11000, groesse: 1.45 },
],
};
for (const rolle of ["linke", "modi", "gast"]) {
@@ -424,33 +424,33 @@ melde("=== 8. Die Gestaltung der Karten ===");
[{ stufen: [] }, 400, "keine_stufe", "eine leere Leiter"],
[{ stufen: null }, 400, "stufen_fehlen", "gar keine Leiter"],
[{ stufen: Array.from({ length: STUFEN_MAX + 1 }, (x, i) => ({
- ab_cent: (i + 1) * 100, name: `S${i}`, vorlage: "herz",
+ ab_mille: (i + 1) * 100, name: `S${i}`, vorlage: "herz",
farbe: "#8ec9ff", dauer_ms: 5000, groesse: 1 })) }, 400, "zu_viele_stufen",
`${STUFEN_MAX + 1} Stufen`],
- [{ stufen: [{ ab_cent: 0, name: "X", vorlage: "herz", farbe: "#8ec9ff", dauer_ms: 5000, groesse: 1 }] },
- 400, "stufe_betrag_unklar", "null Cent"],
- [{ stufen: [{ ab_cent: 100, name: " ", vorlage: "herz", farbe: "#8ec9ff", dauer_ms: 5000, groesse: 1 }] },
+ [{ stufen: [{ ab_mille: 0, name: "X", vorlage: "herz", farbe: "#8ec9ff", dauer_ms: 5000, groesse: 1 }] },
+ 400, "stufe_betrag_unklar", "null Dogen"],
+ [{ stufen: [{ ab_mille: 100, name: " ", vorlage: "herz", farbe: "#8ec9ff", dauer_ms: 5000, groesse: 1 }] },
400, "stufe_name_fehlt", "ein leerer Name"],
- [{ stufen: [{ ab_cent: 100, name: "X", vorlage: "wolke", farbe: "#8ec9ff", dauer_ms: 5000, groesse: 1 }] },
+ [{ stufen: [{ ab_mille: 100, name: "X", vorlage: "wolke", farbe: "#8ec9ff", dauer_ms: 5000, groesse: 1 }] },
400, "vorlage_unbekannt", "eine erfundene Zeichnung"],
/* DIE FARBE IST DIE HEIKELSTE STELLE: Sie landet als CSS-Wert in
der Karte. Ein freier Text waere dort eine Einschleusung. */
- [{ stufen: [{ ab_cent: 100, name: "X", vorlage: "herz",
+ [{ stufen: [{ ab_mille: 100, name: "X", vorlage: "herz",
farbe: "red; background:url(javascript:1)", dauer_ms: 5000, groesse: 1 }] },
400, "farbe_unklar", "eine eingeschleuste Farbe"],
- [{ stufen: [{ ab_cent: 100, name: "X", vorlage: "herz", farbe: "blau", dauer_ms: 5000, groesse: 1 }] },
+ [{ stufen: [{ ab_mille: 100, name: "X", vorlage: "herz", farbe: "blau", dauer_ms: 5000, groesse: 1 }] },
400, "farbe_unklar", "ein Farbwort"],
- [{ stufen: [{ ab_cent: 100, name: "X", vorlage: "herz", farbe: "#8ec9ff",
+ [{ stufen: [{ ab_mille: 100, name: "X", vorlage: "herz", farbe: "#8ec9ff",
dauer_ms: DAUER_MS_MAX + 1000, groesse: 1 }] }, 400, "dauer_unklar", "zu lange"],
- [{ stufen: [{ ab_cent: 100, name: "X", vorlage: "herz", farbe: "#8ec9ff",
+ [{ stufen: [{ ab_mille: 100, name: "X", vorlage: "herz", farbe: "#8ec9ff",
dauer_ms: DAUER_MS_MIN - 500, groesse: 1 }] }, 400, "dauer_unklar", "zu kurz"],
- [{ stufen: [{ ab_cent: 100, name: "X", vorlage: "herz", farbe: "#8ec9ff",
+ [{ stufen: [{ ab_mille: 100, name: "X", vorlage: "herz", farbe: "#8ec9ff",
dauer_ms: 5000, groesse: GROESSE_MAX + 1 }] }, 400, "groesse_unklar", "zu gross"],
- [{ stufen: [{ ab_cent: 100, name: "X", vorlage: "herz", farbe: "#8ec9ff",
+ [{ stufen: [{ ab_mille: 100, name: "X", vorlage: "herz", farbe: "#8ec9ff",
dauer_ms: 5000, groesse: GROESSE_MIN - 0.5 }] }, 400, "groesse_unklar", "zu klein"],
[{ stufen: [
- { ab_cent: 500, name: "A", vorlage: "herz", farbe: "#8ec9ff", dauer_ms: 5000, groesse: 1 },
- { ab_cent: 500, name: "B", vorlage: "welle", farbe: "#a98bff", dauer_ms: 5000, groesse: 1 },
+ { ab_mille: 500, name: "A", vorlage: "herz", farbe: "#8ec9ff", dauer_ms: 5000, groesse: 1 },
+ { ab_mille: 500, name: "B", vorlage: "welle", farbe: "#a98bff", dauer_ms: 5000, groesse: 1 },
] }, 409, "stufe_doppelt", "zwei Stufen beim selben Betrag"],
];
for (const [rumpf, code, wort, was] of schlecht) {
@@ -476,7 +476,7 @@ melde("=== 8. Die Gestaltung der Karten ===");
});
ok(gut.code === 200 && gut.j?.stufen?.length === 2,
`zwei Stufen werden gespeichert (${gut.code})`);
- ok(gut.j?.stufen?.[0]?.ab_cent === 100 && gut.j?.stufen?.[1]?.ab_cent === 2000,
+ ok(gut.j?.stufen?.[0]?.ab_mille === 100 && gut.j?.stufen?.[1]?.ab_mille === 2000,
"und stehen aufsteigend");
ok(gut.j?.stufen?.[1]?.groesse === 1.45,
`die Groesse kommt an (${gut.j?.stufen?.[1]?.groesse})`);
@@ -492,8 +492,8 @@ melde("=== 8. Die Gestaltung der Karten ===");
const getauscht = await roh(CREW, "/workspace/api/spenden/stufen", {
method: "PUT", keks: k.admin,
rumpf: { stufen: [
- { ...jetzige[0], ab_cent: 2000 },
- { ...jetzige[1], ab_cent: 100 },
+ { ...jetzige[0], ab_mille: 2000 },
+ { ...jetzige[1], ab_mille: 100 },
] },
});
ok(getauscht.code === 200, `zwei Stufen tauschen ihre Betraege (${getauscht.code})`);
@@ -508,10 +508,10 @@ melde("=== 8. Die Gestaltung der Karten ===");
/* SPEZIELLE SACHEN BEI SPEZIELLEN SPENDEN: Die Stufe entscheidet
sich am Betrag, und zwar immer die hoechste passende. */
const { stufeFuer } = await import("./workspace-spenden.js");
- ok(stufeFuer(100)?.name === "Danke", "1 EUR -> Danke");
- ok(stufeFuer(1999)?.name === "Danke", "19,99 EUR -> immer noch Danke");
- ok(stufeFuer(2000)?.name === "Legende", "20 EUR -> Legende");
- ok(stufeFuer(50000)?.name === "Legende", "500 EUR -> Legende");
+ ok(stufeFuer(100)?.name === "Danke", "0,1 Dogen -> Danke");
+ ok(stufeFuer(1999)?.name === "Danke", "1,999 Dogen -> immer noch Danke");
+ ok(stufeFuer(2000)?.name === "Legende", "2 Dogen -> Legende");
+ ok(stufeFuer(50000)?.name === "Legende", "50 Dogen -> Legende");
ok(stufeFuer(99) === null, "unter der ersten Stufe gibt es keine");
/* ---- Die Probe legt KEINE Spende an ----
@@ -520,7 +520,7 @@ melde("=== 8. Die Gestaltung der Karten ===");
Testdaten -- und „geloescht" heisst bei Geld nicht „war nie da". */
const vorher = zaehle();
const probe = await roh(CREW, "/workspace/api/spenden/probe", {
- method: "POST", keks: k.admin, rumpf: { betrag_cent: 2000 },
+ method: "POST", keks: k.admin, rumpf: { dogen_mille: 2000 },
});
ok(probe.code === 200 && probe.j?.karte?.stufe?.name === "Legende",
`die Probe zeigt die richtige Stufe (${probe.j?.karte?.stufe?.name})`);
@@ -528,7 +528,7 @@ melde("=== 8. Die Gestaltung der Karten ===");
ok(zaehle() === vorher,
`und es entsteht KEINE Spende daraus (${vorher} -> ${zaehle()})`);
const probeNein = await roh(CREW, "/workspace/api/spenden/probe", {
- method: "POST", keks: k.modi, rumpf: { betrag_cent: 2000 },
+ method: "POST", keks: k.modi, rumpf: { dogen_mille: 2000 },
});
ok(probeNein.code === 403, `ein Modi probiert nicht (${probeNein.code})`);
@@ -590,7 +590,7 @@ melde("=== 8. Die Gestaltung der Karten ===");
method: "PUT", keks: k.admin,
rumpf: { stufen: gut.j.stufen.map((z) => ({ ...z, name: z.name + "!" })) },
});
- const jetztBild = (nochmal.j?.stufen || []).find((z) => z.ab_cent === 2000)?.bild;
+ const jetztBild = (nochmal.j?.stufen || []).find((z) => z.ab_mille === 2000)?.bild;
ok(jetztBild === hoch.j.bild,
`nach dem Umbenennen haengt das Bild noch dran (${jetztBild ? "ja" : "nein"})`);
@@ -599,7 +599,7 @@ melde("=== 8. Die Gestaltung der Karten ===");
dabei eine neue Kennung, und ein Bild-Hochladen gegen die eben
noch gueltige antwortete mit 404. Im Alltag trifft das jeden,
der einen zweiten Bildschirm offen hat. */
- const nachId = (nochmal.j?.stufen || []).find((z) => z.ab_cent === 2000)?.id;
+ const nachId = (nochmal.j?.stufen || []).find((z) => z.ab_mille === 2000)?.id;
ok(nachId === id, `die Kennung bleibt ueber das Speichern hinweg (${id} -> ${nachId})`);
const weg = await roh(CREW, `/workspace/api/spenden/stufe/${id}/bild`,
@@ -616,6 +616,222 @@ melde("=== 8. Die Gestaltung der Karten ===");
"die Grenzen stehen im Stand und nicht nur in der Oberflaeche");
}
+
+/* =======================================================================
+ 9. DOGEN STATT GELD, DER EIGENE TEXT UND DIE PUNKTE
+
+ Filipe, 28.09.2026:
+ „mann soll nie die summe sehen sondern die dogen auch mit dem
+ symbol ... es soll auch nie geld da stehen sondern Dogen."
+ „der text der dazu erscheint sollen die leute selber schreiben
+ koennen ... es sollen personalisierte texte sein von den leuten
+ selbst."
+ „ich will dass die den leuten auch als punkte hinzugefuegt
+ werden."
+ ======================================================================= */
+melde("");
+melde("=== 9. Dogen, eigener Text und Punkte ===");
+{
+ const { CENT_JE_DOGE, MILLE, centInMille, milleInCent, GRUSS_MAX, KNOEPFE_MILLE }
+ = await import("./spenden-tabellen.js");
+
+ /* ---- DIE RECHNUNG ----
+ Bewusst OHNE die Zahl 1000 in der Erwartung: Bei diesem Kurs ist
+ ein Tausendstel Doge zufaellig genau ein Cent, und eine Pruefung,
+ die auf diesem Zufall steht, wird gruen bleiben, wenn der Kurs
+ sich aendert -- und dann nichts mehr pruefen. */
+ ok(centInMille(CENT_JE_DOGE) === MILLE,
+ `ein ganzer Doge sind ${CENT_JE_DOGE} Cent`);
+ ok(milleInCent(MILLE) === CENT_JE_DOGE, "und zurueck genauso");
+ ok(milleInCent(centInMille(CENT_JE_DOGE * 7)) === CENT_JE_DOGE * 7,
+ "hin und zurueck aendert nichts");
+ ok(centInMille(0) === 0 && centInMille(-5) <= 0,
+ "Gegenprobe: null bleibt null");
+
+ /* ---- KEIN GELD IM BROWSER ----
+ Die wichtigste Pruefung dieses Abschnitts. Filipe hat „wirklich
+ nirgends" gewaehlt, und die einzige Fassung davon, die haelt,
+ ist: Was nicht gesendet wird, kann auch nicht angezeigt werden.
+ Geprueft werden deshalb die FELDNAMEN der ganzen Antwort und
+ nicht die Oberflaeche -- eine Oberflaeche kann man aendern,
+ ohne daran zu denken. */
+ const namen = (wert, raus = new Set()) => {
+ if (Array.isArray(wert)) { for (const w of wert) namen(w, raus); return raus; }
+ if (wert && typeof wert === "object") {
+ for (const [n, w] of Object.entries(wert)) { raus.add(n); namen(w, raus); }
+ }
+ return raus;
+ };
+ for (const [wer, keks] of [["die Leitung", k.admin], ["ein Zuschauer", k.gast]]) {
+ const a = await roh(CREW, "/workspace/api/spenden", { keks });
+ const felder = namen(a.j);
+ const geld = [...felder].filter((n) => /cent|waehrung|euro|eur$/i.test(n));
+ ok(geld.length === 0,
+ geld.length ? `GELD IM STAND (${wer}): ${geld.join(", ")}`
+ : `kein Geldfeld im Stand (${wer}, ${felder.size} Felder)`);
+ ok(felder.has("dogen_max_mille") && felder.has("gruss_max"),
+ "und die Grenzen stehen als Dogen und Zeichen darin");
+ /* Ein Eurozeichen darf nirgends stehen -- auch nicht in einem
+ Text, den irgendwer einmal eingetippt hat. */
+ ok(!JSON.stringify(a.j).includes("€"), `kein Eurozeichen (${wer})`);
+ }
+ ok([...namen({ a: [{ betrag_cent: 1 }] })].includes("betrag_cent"),
+ "Gegenprobe: ein Geldfeld wuerde gefunden");
+
+ /* ---- DIE KNOEPFE FUEHREN ZU EINEM EURO-BETRAG, ZEIGEN ABER DOGEN ----
+ Der Euro entsteht erst in der PayPal-Adresse, weil PayPal ihn
+ braucht. Das ist der einzige Ort, an dem er ueberhaupt noch
+ auftaucht. */
+ {
+ const a = await roh(CREW, "/workspace/api/spenden", { keks: k.gast });
+ const knopf = a.j?.knoepfe?.[0];
+ ok(knopf?.mille === KNOEPFE_MILLE[0],
+ `der erste Knopf steht auf ${knopf?.mille} Tausendsteln`);
+ ok(knopf?.ziel?.endsWith("EUR"),
+ `und fuehrt trotzdem zu einer Zahlung (${knopf?.ziel})`);
+ ok(knopf?.cent === undefined, "der Euro-Betrag selbst kommt nicht mit");
+ }
+
+ /* ---- DER TEXT KOMMT VON DEM, DER GIBT ---- */
+ const langer = "A".repeat(GRUSS_MAX + 60);
+ /* Die Bremse von einer Meldung je Minute gilt je Person -- deshalb
+ meldet hier der ZWEITE Gast. */
+ const gemeldet = await roh(CREW, "/workspace/api/spenden/melden", {
+ method: "POST", keks: k.gast2,
+ rumpf: { dogen_mille: 700, name: "Lenny vom Rudel", gruss: langer },
+ });
+ ok(gemeldet.code === 201, `ein Zuschauer meldet mit eigenem Text (${gemeldet.code})`);
+ const wartend = (await roh(CREW, "/workspace/api/spenden", { keks: k.admin }))
+ .j?.offen?.find((z) => z.name === "Lenny vom Rudel");
+ ok(!!wartend, "die Meldung steht im Pult");
+ ok(wartend?.gruss?.length === GRUSS_MAX,
+ `ein zu langer Text wird auf ${GRUSS_MAX} gekuerzt (${wartend?.gruss?.length})`);
+ ok(wartend?.dogen_mille === 700, `und traegt Dogen (${wartend?.dogen_mille})`);
+
+ /* ---- DIE LEITUNG DARF IHN BERICHTIGEN ----
+ Das ist die Schranke vor dem Stream. Ohne sie bliebe nur „ganz
+ ablehnen" -- und dann faellt wegen eines Wortes eine echte
+ Spende unter den Tisch. */
+ const berichtigt = await roh(CREW, `/workspace/api/spenden/${wartend.id}/bestaetigen`, {
+ method: "POST", keks: k.admin,
+ rumpf: { name: "Lenny", gruss: "Bleibt so" },
+ });
+ ok(berichtigt.code === 200 && berichtigt.j?.name === "Lenny"
+ && berichtigt.j?.gruss === "Bleibt so",
+ `Name und Text lassen sich beim Bestaetigen aendern (${berichtigt.j?.gruss})`);
+ ok(berichtigt.j?.dogen_mille === 700,
+ "und der Betrag bleibt dabei stehen, wenn keiner mitkommt");
+
+ /* ---- DIE PUNKTE ----
+ Gemessen werden UNTERSCHIEDE und keine absoluten Staende: Vor
+ diesem Abschnitt sind schon Spenden durchgelaufen, und eine
+ Pruefung, die eine feste Zahl erwartet, bricht beim naechsten
+ Test, der davor eingefuegt wird. */
+ const standVon = async (keks) =>
+ (await roh(CREW, "/workspace/api/spenden", { keks })).j?.ich?.dogen_mille ?? -1;
+
+ const lenny = await standVon(k.gast2);
+ ok(lenny === 700, `die bestaetigte Meldung wurde gutgeschrieben (${lenny})`);
+
+ /* NOCHMAL ZEIGEN DARF NICHT NOCHMAL GUTSCHREIBEN. Die Sperre ist
+ ein eindeutiger Index in der Datenbank und keine Bedingung im
+ Programm -- deshalb wird sie hier auch wirklich angegriffen. */
+ await roh(CREW, `/workspace/api/spenden/${wartend.id}/nochmal`,
+ { method: "POST", keks: k.admin });
+ await roh(CREW, `/workspace/api/spenden/${wartend.id}/bestaetigen`,
+ { method: "POST", keks: k.admin });
+ ok(await standVon(k.gast2) === lenny,
+ "nochmal zeigen und nochmal bestaetigen aendern nichts daran");
+
+ /* ---- VON HAND, MIT UND OHNE PERSON ---- */
+ const vorher = await standVon(k.gast2);
+ const mitPerson = await roh(CREW, "/workspace/api/spenden", {
+ method: "POST", keks: k.admin,
+ rumpf: { dogen_mille: 300, name: "Lenny", person_id: IDS.gast2 },
+ });
+ ok(mitPerson.code === 201, `von Hand mit Person (${mitPerson.code})`);
+ ok(await standVon(k.gast2) === vorher + 300,
+ `die Dogen landen bei der Person (${vorher} -> ${await standVon(k.gast2)})`);
+
+ const ohnePerson = await roh(CREW, "/workspace/api/spenden", {
+ method: "POST", keks: k.admin,
+ rumpf: { dogen_mille: 400, name: "Irgendwer vom Handy" },
+ });
+ ok(ohnePerson.code === 201, "von Hand ohne Person geht auch");
+ ok(await standVon(k.gast2) === vorher + 300,
+ "und schreibt niemandem etwas gut -- geratene Punkte waeren schlimmer als keine");
+
+ /* ---- UND NICHT UEBER DIE HAUSGRENZE ----
+ `creator` gehoert der Agentur. Eine Gutschrift dorthin waere
+ genau die Vermischung, die seit dem 24.09. nicht mehr sein darf
+ -- auch nicht durch einen Tippfehler in einer Zahl. */
+ const fremd = await roh(CREW, "/workspace/api/spenden", {
+ method: "POST", keks: k.admin,
+ rumpf: { dogen_mille: 100, name: "X", person_id: IDS.creator },
+ });
+ ok(fremd.code === 400 && fremd.j?.fehler === "person_unbekannt",
+ `eine Person aus dem anderen Haus wird abgelehnt (${fremd.code})`);
+
+ /* ---- WER DIE STAENDE SIEHT ----
+ Filipe hat „jeder nur seine eigenen, du alle" gewaehlt. Eine
+ oeffentliche Rangliste macht aus Unterstuetzung einen
+ Wettbewerb, und wer wenig geben kann, staende jeden Abend
+ darin. */
+ const alsLeitung = await roh(CREW, "/workspace/api/spenden", { keks: k.admin });
+ const alsGast = await roh(CREW, "/workspace/api/spenden", { keks: k.gast });
+ ok(Array.isArray(alsLeitung.j?.staende) && alsLeitung.j.staende.length > 0,
+ `die Leitung sieht ${alsLeitung.j?.staende?.length} Staende`);
+ ok(alsGast.j?.staende === null, "ein Zuschauer sieht keine Rangliste");
+ ok(alsGast.j?.personen === null, "und auch keine Namensliste");
+ ok(typeof alsGast.j?.ich?.dogen_mille === "number",
+ `aber seinen eigenen Stand (${alsGast.j?.ich?.dogen_mille})`);
+ ok(alsLeitung.j.staende.every((z) => typeof z.mille === "number" && z.mille !== 0),
+ "und in der Liste steht nur, wer wirklich etwas hat");
+
+ /* ---- DAS ZEICHEN ---- */
+ const ohneZeichen = await roh(CREW, "/workspace/api/spenden", { keks: k.admin });
+ ok(ohneZeichen.j?.zeichen === "", "ohne Hochladen gibt es kein Zeichen");
+
+ const png = Buffer.from(
+ "89504e470d0a1a0a0000000d494844520000000100000001080600000"
+ + "01f15c4890000000a49444154789c6360000002000100ffff0300000600"
+ + "0557bfabd40000000049454e44ae426082", "hex");
+ 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 || ""),
+ `das Zeichen laesst sich hochladen (${hochZ.code})`);
+ const abrufbar = await roh(CREW, hochZ.j.zeichen, {});
+ 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,
+ "es steht im Stand, und zwar fuer alle");
+ 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,
+ "und reist mit jeder Karte mit");
+
+ const keinBild = await roh(CREW, "/workspace/api/spenden/zeichen", {
+ method: "POST", keks: k.admin, roher: Buffer.from("ich bin kein bild, wirklich nicht"),
+ });
+ ok(keinBild.code === 400 && keinBild.j?.fehler === "kein_bild",
+ `etwas, das kein Bild ist, wird abgelehnt (${keinBild.code})`);
+ const fremdeHand = await roh(CREW, "/workspace/api/spenden/zeichen", {
+ method: "POST", keks: k.modi, roher: png,
+ });
+ ok(fremdeHand.code === 403, `ein Modi setzt das Zeichen nicht (${fremdeHand.code})`);
+
+ const wegZ = await roh(CREW, "/workspace/api/spenden/zeichen",
+ { method: "DELETE", keks: k.admin });
+ ok(wegZ.code === 200 && wegZ.j?.zeichen === "",
+ `es laesst sich wieder wegnehmen (${wegZ.code})`);
+ const fortZ = await roh(CREW, hochZ.j.zeichen, {});
+ ok(fortZ.code === 404, `und die Datei ist danach fort (${fortZ.code})`);
+}
+
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 62b4f4ab..f70dce20 100644
--- a/server/spenden-tabellen.js
+++ b/server/spenden-tabellen.js
@@ -49,6 +49,62 @@
erst beim Anzeigen.
===================================================================== */
+/* =====================================================================
+ DIE WAEHRUNG DES HAUSES: DOGEN (28.09.2026)
+
+ Filipe: „mann soll nie die summe sehen sondern die dogen auch mit
+ dem symbol ... es soll auch nie geld da stehen sondern Dogen."
+
+ EIN DOGE SIND ZEHN EURO. Woertlich nach Filipes Angabe „1 euro
+ sind 0,10 dogen" -- rueckgefragt und bestaetigt, weil zwischen den
+ beiden moeglichen Lesarten („ein Doge KOSTET 0,10" gegen „ein Euro
+ ERGIBT 0,10") der Faktor 100 liegt und danach jede Zahl im Bild
+ falsch gewesen waere.
+
+ ---------------------------------------------------------------------
+ GERECHNET WIRD IN TAUSENDSTELN
+
+ Derselbe Grund wie beim Cent, nur strenger: 0.1 + 0.2 ist in
+ keiner Programmiersprache 0.3. Bei Geld faellt das irgendwann dem
+ auf, der zahlt -- bei etwas, das Menschen SAMMELN, faellt es dem
+ auf, der am meisten hat, und der merkt sich das.
+
+ Ein Doge sind 1000 Tausendstel. Bei diesem Kurs ist ein
+ Tausendstel genau ein Cent, es wird also nie gerundet.
+
+ ---------------------------------------------------------------------
+ DER KURS STEHT HIER UND NUR HIER
+
+ Nicht in den Einstellungen. Eine Einstellung, die Dogen und Euro
+ nebeneinanderstellt, WAERE das Geld auf dem Bildschirm, das
+ nirgends mehr stehen soll. Wer den Kurs aendern will, aendert
+ diese eine Zeile -- und alles, was danach hereinkommt, rechnet
+ damit.
+
+ WAS SCHON GUTGESCHRIEBEN IST, BLEIBT. Die Punktestaende liegen als
+ Dogen in der Tabelle und nicht als Cent, aus denen erst beim
+ Anzeigen gerechnet wird. Sonst haette eine Kursaenderung
+ rueckwirkend jedem seinen Stand verschoben -- wer gestern 5 Dogen
+ hatte, haette heute 50, ohne etwas getan zu haben. Eine Waehrung,
+ die rueckwaerts wirkt, ist keine.
+ ===================================================================== */
+
+/** Wie viele Cent ein Doge wert ist. */
+export const CENT_JE_DOGE = 1000;
+
+/** Ein Doge in Tausendsteln -- die Einheit, in der gerechnet wird. */
+export const MILLE = 1000;
+
+/** Cent -> Tausendstel Dogen. */
+export function centInMille(cent) {
+ return Math.round(((Number(cent) || 0) * MILLE) / CENT_JE_DOGE);
+}
+
+/** Tausendstel Dogen -> Cent. Nur fuer PayPal und die Ablage. */
+export function milleInCent(mille) {
+ return Math.round(((Number(mille) || 0) * CENT_JE_DOGE) / MILLE);
+}
+
/** So lange bleibt eine Spende im Verlauf stehen. */
export const SPENDEN_TAGE = 400;
@@ -60,9 +116,26 @@ export const SPENDEN_TAGE = 400;
* einfangen kann, wenn hundert Leute zusehen. */
export const BETRAG_MAX_CENT = 500000;
-/** Was auf der Karte stehen darf. */
+/** Was auf der Karte stehen darf.
+ *
+ * Filipe: „der text der dazu erscheint sollen die leute selber
+ * schreiben koennen. soll auf nicht zu viel aber auch nicht zu
+ * wenig schreiben koennen."
+ *
+ * 140 ZEICHEN, UND DIE ZAHL IST GEMESSEN, NICHT GERATEN. Die Karte
+ * steht je nach Stufe sechs bis elf Sekunden. Vorgelesen sind das
+ * rund 15 Zeichen je Sekunde, gelesen mehr -- aber niemand liest
+ * eine Karte, waehrend er ein Video sieht, mit voller
+ * Aufmerksamkeit. Bei 140 Zeichen sind es zwei bis drei Zeilen; die
+ * fasst man im Vorbeischauen. Bei 200 wurde die Schrift auf einer
+ * kleinen Karte so eng, dass gar niemand mehr hinsieht -- ein Gruss,
+ * den keiner liest, ist schlechter als ein kurzer.
+ *
+ * AELTERE, LAENGERE GRUESSE BLEIBEN STEHEN. Sie abzuschneiden waere
+ * ein stiller Datenverlust an fremden Worten; die Grenze gilt fuer
+ * das, was neu hereinkommt. */
export const NAME_MAX = 40;
-export const GRUSS_MAX = 200;
+export const GRUSS_MAX = 140;
/** Die mitgelieferten Zeichnungen.
*
@@ -113,6 +186,19 @@ 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.
+ *
+ * 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. */
+export const ZEICHEN_NAME = "dogen_zeichen";
+
/** 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
@@ -128,13 +214,19 @@ export const STUFEN_START = [
{ ab_cent: 2000, name: "Rudel-Legende", vorlage: "krone", farbe: "#e0a13a", dauer_ms: 11000, groesse: 1.45 },
];
-/** Die Betraege, die als Knopf angeboten werden (in Cent).
+/** Die Knoepfe, die zum Geben angeboten werden — in TAUSENDSTELN.
+ *
+ * 0,2 · 0,5 · 1 · 2,5 Dogen.
+ *
+ * IN DOGEN GEDACHT UND NICHT IN GELD. Das ist nicht Kosmetik: Wer
+ * die Reihe in Euro pflegt und dann anzeigt, hat zwei Listen fuer
+ * dieselbe Frage -- und die eine laeuft von der anderen weg. Der
+ * Euro-Betrag fuer PayPal wird hier abgeleitet, nicht gepflegt.
*
* VIER UND EIN FREIES FELD. Wer eine lange Liste sieht, rechnet;
* wer vier Knoepfe sieht, tippt. Die Reihe endet bewusst nicht bei
- * einer runden Zahl wie 50 — das waere eine Erwartung, keine
- * Auswahl. */
-export const BETRAG_KNOEPFE = [200, 500, 1000, 2500];
+ * einer runden Zahl — das waere eine Erwartung, keine Auswahl. */
+export const KNOEPFE_MILLE = [200, 500, 1000, 2500];
export function spendenTabellen(d) {
d.exec(`
@@ -196,6 +288,54 @@ export function spendenTabellen(d) {
);
`);
+ /* ==== DIE DOGEN DER LEUTE =======================================
+
+ Filipe: „ich will dass die den leuten auch als punkte
+ hinzugefuegt werden ... ich hab naemlich vor den leuten
+ spezielle sachen auch noch zu erstellen spaeter wo mit diesen
+ dogpunkten zu tun hat, also muss es jetzt schon perfektioniert
+ werden."
+
+ EIN BUCH UND KEIN ZAEHLER. Ein Feld `dogen` an der Person waere
+ kuerzer -- und waere die zweite Antwort auf dieselbe Frage: Was
+ hat jemand? Der Zaehler und die Spenden koennen auseinanderlaufen,
+ und dann weiss niemand mehr, welcher von beiden recht hat. Hier
+ ist der Stand die SUMME der Buchungen, und eine Summe kann sich
+ nicht von ihren Posten entfernen.
+
+ UND WEIL SPAETER ETWAS ABGEHEN SOLL. „Spezielle sachen ... wo mit
+ diesen dogpunkten zu tun hat" heisst irgendwann: eingeloest. Ein
+ Zaehler, der kleiner wird, laesst keine Frage mehr beantworten --
+ wofuer? wann? wer hat es ausgeloest? Eine Zeile mit Grund und
+ Datum schon. Deshalb steht die Tabelle heute schon so da, obwohl
+ es erst eine Art von Buchung gibt.
+
+ NEGATIVE BETRAEGE SIND ERLAUBT, LOESCHEN NICHT. Eine
+ zurueckgenommene Gutschrift ist eine zweite Zeile mit
+ Minuszeichen, kein verschwundener Posten. */
+ d.exec(`
+ CREATE TABLE IF NOT EXISTS dogen_buchungen (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
+ /* In Tausendstel Dogen. Darf negativ sein. */
+ mille INTEGER NOT NULL,
+ grund TEXT NOT NULL,
+ spende_id INTEGER REFERENCES spenden(id) ON DELETE SET NULL,
+ notiz TEXT NOT NULL DEFAULT '',
+ erstellt TEXT NOT NULL
+ );
+
+ /* JE SPENDE EINE GUTSCHRIFT. Die Karte laesst sich noch einmal
+ zeigen, und ein Strom wird bei einer Stoerung wiederholt --
+ ohne diese Sperre bekaeme jemand seine Dogen zweimal, und
+ gemerkt haette es niemand ausser ihm. Die Bedingung schuetzt
+ die Zeilen ohne Spende (spaetere Handbuchungen) voreinander. */
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_dogen_einmal
+ ON dogen_buchungen (spende_id, grund) WHERE spende_id IS NOT NULL;
+ CREATE INDEX IF NOT EXISTS idx_dogen_person
+ ON dogen_buchungen (person_id, id);
+ `);
+
/* ==== NACHGEWACHSEN: DIE GROESSE ================================
`ALTER TABLE ... ADD COLUMN` und nicht „Tabelle neu bauen": Am
11.09.2026 hat genau dieses Umkopieren an anderer Stelle drei
diff --git a/server/workspace-spenden.js b/server/workspace-spenden.js
index 75f882a4..57200c21 100644
--- a/server/workspace-spenden.js
+++ b/server/workspace-spenden.js
@@ -31,7 +31,10 @@
hat. Alles andere waere geraten.
===================================================================== */
import express from "express";
-import { db, sitzungLesen, protokolliere, echteIp } from "./workspace.js";
+import {
+ db, sitzungLesen, protokolliere, echteIp, einstellung, einstellungSetzen,
+ hausBedingung,
+} from "./workspace.js";
import { stromAn } from "./workspace-chat.js";
import { buehneMelden } from "./workspace-buehne.js";
import { join } from "node:path";
@@ -39,9 +42,10 @@ import { mkdirSync, writeFileSync, unlinkSync } from "node:fs";
import { randomBytes } from "node:crypto";
import { DATEN_ORDNER } from "./workspace.js";
import {
- BETRAG_MAX_CENT, NAME_MAX, GRUSS_MAX, BETRAG_KNOEPFE, SPENDEN_TAGE,
+ BETRAG_MAX_CENT, NAME_MAX, GRUSS_MAX, KNOEPFE_MILLE, SPENDEN_TAGE,
VORLAGEN, STUFEN_MAX, DAUER_MS_MIN, DAUER_MS_MAX,
GROESSE_MIN, GROESSE_MAX, BILD_MAX_BYTES,
+ centInMille, milleInCent, ZEICHEN_NAME,
} from "./spenden-tabellen.js";
const BILDER = join(DATEN_ORDNER, "spendenbilder");
@@ -58,6 +62,122 @@ export const SPENDEN_ROLLEN = GELD_ROLLEN;
const jetzt = () => new Date().toISOString();
+/** Die Obergrenze, wie der Browser sie zu sehen bekommt. */
+const DOGEN_MAX_MILLE = centInMille(BETRAG_MAX_CENT);
+
+/* ======================================================================
+ EINE ANGABE IN DOGEN WIRD ZU CENT -- AN EINER STELLE
+
+ Jeder Weg, der einen Betrag entgegennimmt, nimmt ihn in
+ Tausendsteln Dogen. Dass daraus Cent werden, passiert genau hier.
+ Fuenf Wege mit je eigener Umrechnung waeren fuenf Gelegenheiten,
+ sich um den Faktor zu vertun -- und der Fehler faellt erst auf,
+ wenn er im Bild steht.
+
+ Rueckgabe: die Cent, oder `null`. `null` heisst „nicht brauchbar",
+ und der Aufrufer antwortet damit 400.
+ ====================================================================== */
+function centAusDogen(wert) {
+ const mille = Math.round(Number(wert));
+ if (!Number.isFinite(mille) || mille <= 0 || mille > DOGEN_MAX_MILLE) return null;
+ const cent = milleInCent(mille);
+ if (!Number.isFinite(cent) || cent <= 0 || cent > BETRAG_MAX_CENT) return null;
+ return cent;
+}
+
+/* ======================================================================
+ DAS DOGEN-BUCH
+
+ Filipe: „ich will dass die den leuten auch als punkte
+ hinzugefuegt werden."
+ ====================================================================== */
+
+/** Was jemand hat -- die Summe seiner Buchungen.
+ *
+ * KEIN ZAEHLER IRGENDWO DANEBEN. Der Stand IST die Summe; ein
+ * zweiter Wert, der dasselbe behauptet, waere die Gelegenheit, dass
+ * beide auseinanderlaufen. */
+export function dogenStand(personId) {
+ try {
+ const z = db().prepare(`SELECT COALESCE(SUM(mille), 0) AS m
+ FROM dogen_buchungen WHERE person_id = ?`).get(Number(personId) || 0);
+ return Number(z?.m) || 0;
+ } catch { return 0; }
+}
+
+/** Die Staende aller -- fuer die Leitung.
+ *
+ * Nur wer etwas hat, steht drin. Eine Liste mit lauter Nullen ist
+ * keine Auskunft, sondern eine Personenliste mit Zusatzspalte. */
+function dogenStaende(person) {
+ try {
+ return db().prepare(`SELECT p.id, p.name, p.rolle,
+ COALESCE(SUM(b.mille), 0) AS mille
+ FROM personen p JOIN dogen_buchungen b ON b.person_id = p.id
+ WHERE 1=1 ${hausBedingung(person, "p.rolle")}
+ GROUP BY p.id HAVING mille <> 0
+ ORDER BY mille DESC, p.name`).all()
+ .map((z) => ({ id: z.id, name: z.name, mille: Number(z.mille) || 0 }));
+ } catch { return []; }
+}
+
+/** Die Dogen einer bestaetigten Spende gutschreiben.
+ *
+ * ==== WARUM DAS HIER STEHT UND NICHT AN DEN ZWEI BESTAETIGUNGEN
+ *
+ * Es gibt zwei Wege, auf denen eine Spende gueltig wird (die
+ * Leitung traegt sie ein / die Leitung bestaetigt eine Meldung) --
+ * und beide enden in `zeigen()`. Zwei Aufrufe waeren zwei Stellen,
+ * an denen man einen vergessen kann; genau so fehlen Dinge.
+ *
+ * DASS ES TROTZDEM NIE DOPPELT GUTSCHREIBT, ENTSCHEIDET DIE
+ * DATENBANK und nicht diese Funktion: `zeigen()` laeuft auch beim
+ * „nochmal zeigen", und ein Strom wird bei einer Stoerung
+ * wiederholt. Der eindeutige Index ueber (spende_id, grund) macht
+ * die zweite Buchung unmoeglich -- eine Bedingung im Programm
+ * haette dieselbe Luecke wie jedes „erst nachsehen, dann
+ * schreiben".
+ *
+ * OHNE PERSON KEINE PUNKTE. Eine von Hand eingetragene Spende hat
+ * oft nur einen Namen auf der Karte. Einen Namen einer Person
+ * zuzuordnen hiesse raten, und geratene Punkte sind schlimmer als
+ * keine. Deshalb kann die Leitung beim Eintragen eine Person
+ * auswaehlen -- tut sie es nicht, laeuft die Karte trotzdem, nur
+ * ohne Gutschrift. */
+function gutschreiben(z) {
+ try {
+ if (!z?.id || !z.person_id || z.stand !== "bestaetigt") return;
+ const mille = centInMille(z.betrag_cent);
+ if (mille <= 0) return;
+ const a = db().prepare(`INSERT OR IGNORE INTO dogen_buchungen
+ (person_id, mille, grund, spende_id, notiz, erstellt)
+ VALUES (?,?,'spende',?,'',?)`)
+ .run(z.person_id, mille, z.id, jetzt());
+
+ /* ==== UND DER MENSCH ERFAEHRT ES SOFORT ====================
+
+ Gefunden beim Messen, nicht beim Lesen: Eine Zuschauerin
+ bekommt zwar die Karte zu sehen, aber `spendenHolen()` ruft
+ bei ihr niemand auf -- das tut nur der Host. Ihr eigener
+ Punktestand waere also erst beim naechsten Seitenaufruf
+ gewachsen. „Hinzugefuegt werden" heisst sichtbar
+ hinzugefuegt.
+
+ NUR AN DIE EINE PERSON. Ein Rundruf an alle traegt eine Zahl
+ durch den Saal, die niemanden sonst etwas angeht -- und
+ Filipe hat ausdruecklich keine oeffentliche Rangliste
+ gewollt. */
+ if (a.changes) {
+ stromAn([z.person_id], "dogen", { mille: dogenStand(z.person_id) });
+ }
+ } catch (fehler) {
+ /* EINE FEHLENDE GUTSCHRIFT DARF DIE KARTE NICHT AUFHALTEN. Wer
+ gespendet hat, soll seine Karte sehen -- die Punkte lassen
+ sich nachtragen, der Augenblick nicht. */
+ console.error("[dogen] Gutschrift:", fehler?.message);
+ }
+}
+
function angemeldet(req, res, next) {
const p = sitzungLesen(req);
if (!p) return res.status(401).json({ fehler: "nicht_angemeldet" });
@@ -152,8 +272,19 @@ function alsKarte(z) {
const st = stufeFuer(z.betrag_cent);
return {
id: z.id,
- betrag_cent: z.betrag_cent,
- waehrung: z.waehrung,
+ /* ==== HIER ENDET DAS GELD ====================================
+ Der Browser bekommt Dogen und sonst nichts. Kein
+ `betrag_cent`, keine `waehrung` -- auch nicht „nur im JSON,
+ sieht ja keiner". Filipe: „es soll auch nie geld da stehen."
+ Was nicht gesendet wird, kann an keiner Stelle
+ versehentlich erscheinen, und niemand muss daran denken. */
+ dogen_mille: centInMille(z.betrag_cent),
+ /* DAS ZEICHEN REIST MIT DER KARTE. Es ist fuer alle dasselbe --
+ es als zweiten Wert danebenzulegen hiesse, dass jede Stelle,
+ 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(),
name: z.name,
gruss: z.gruss,
herkunft: z.herkunft,
@@ -176,8 +307,40 @@ function bildAdresse(datei) {
return datei ? `/workspace/api/buehne/spendenbild/${datei}` : "";
}
+/** Das Dogen-Zeichen, wenn eines hochgeladen wurde.
+ *
+ * Filipe: „die symbole schick ich dir spaeter."
+ *
+ * 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, ""));
+}
+
+/** Wen die Leitung einer Spende zuordnen kann.
+ *
+ * NUR DAS EIGENE HAUS, und das ist keine Formalie: Die Reaction
+ * gehoert Team Dogi. Eine Auswahlliste, in der Agenturnamen
+ * stehen, waere genau die Vermischung, die seit dem 24.09. nicht
+ * mehr sein darf.
+ *
+ * Und nur AKTIVE: Wer nicht mehr dabei ist, soll nicht mehr
+ * versehentlich Punkte bekommen. */
+function personenListe(person) {
+ try {
+ return db().prepare(`SELECT id, name FROM personen
+ WHERE aktiv = 1 ${hausBedingung(person)} ORDER BY name`).all();
+ } catch { return []; }
+}
+
/** Eine bestaetigte Spende ins Bild schicken. */
function zeigen(z) {
+ /* ERST BUCHEN, DANN ZEIGEN. Andersherum liefe die Karte, und wenn
+ dabei etwas schiefginge, stuenden die Dogen nicht im Buch --
+ sichtbar gedankt und nicht gutgeschrieben ist der aergerlichere
+ der beiden moeglichen Fehler. */
+ gutschreiben(z);
db().prepare("UPDATE spenden SET gezeigt_am = ? WHERE id = ?").run(jetzt(), z.id);
const daten = { was: "zeigen", karte: alsKarte(z) };
stromAn(empfaenger(), "spende", daten);
@@ -196,16 +359,24 @@ spendenRouter.get("/workspace/api/spenden", (req, res) => {
try {
const pp = paypalAdresse();
const darf = GELD_ROLLEN.has(req.person.rolle);
+ /* DER EURO ENTSTEHT ERST IN DER ADRESSE. `mitBetrag` braucht
+ ihn, weil PayPal ihn braucht -- an den Browser geht nur die
+ fertige Adresse und die Zahl in Dogen. */
const knoepfe = pp
- ? BETRAG_KNOEPFE.map((c) => ({ cent: c, ziel: mitBetrag(pp.ziel, c) }))
+ ? KNOEPFE_MILLE.map((m) => ({ mille: m, ziel: mitBetrag(pp.ziel, milleInCent(m)) }))
.filter((k) => k.ziel)
: [];
res.json({
paypal: pp ? { ziel: pp.ziel, knopf: pp.knopf } : null,
knoepfe,
- stufen: db().prepare("SELECT id, ab_cent, name, vorlage, bild, farbe, dauer_ms,"
- + " groesse FROM spenden_stufen ORDER BY ab_cent").all()
- .map((z) => ({ ...z, bild: bildAdresse(z.bild), groesse: Number(z.groesse) || 1 })),
+ /* EINE STELLE LIEST DIE STUFEN. Hier stand dieselbe Abfrage
+ ein zweites Mal -- und als die Stufen von Cent auf Dogen
+ 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(),
/* 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
@@ -217,8 +388,22 @@ spendenRouter.get("/workspace/api/spenden", (req, res) => {
groesse_min: GROESSE_MIN, groesse_max: GROESSE_MAX,
bild_max_mb: Math.round(BILD_MAX_BYTES / 1024 / 1024),
},
- betrag_max_cent: BETRAG_MAX_CENT,
- ich: { darf },
+ /* Die Grenzen fuer die Felder -- in Dogen und in Zeichen. Sie
+ kommen vom Server, damit nicht zwei Zahlen dieselbe Frage
+ beantworten und eine davon veraltet. */
+ dogen_max_mille: DOGEN_MAX_MILLE,
+ gruss_max: GRUSS_MAX,
+ name_max: NAME_MAX,
+ /* WAS ICH SELBST HABE, SEHE ICH IMMER. Das ist Filipes
+ „hinzugefuegt werden": Ohne einen sichtbaren Stand waere es
+ eine Zahl in einer Datenbank und kein Grund, wiederzukommen. */
+ ich: { darf, dogen_mille: dogenStand(req.person.id) },
+ /* Die Staende aller und die Namensliste sieht nur die Leitung.
+ Fuer alle anderen waere das eine Rangliste darueber, wer
+ wie viel gibt -- und wer wenig gibt, staende jeden Abend
+ darin. */
+ staende: darf ? dogenStaende(req.person) : null,
+ personen: darf ? personenListe(req.person) : null,
/* Die offenen Meldungen sieht nur, wer sie bestaetigen kann.
Fuer alle anderen waere es eine Liste fremder Geldbetraege. */
offen: darf
@@ -244,10 +429,8 @@ spendenRouter.get("/workspace/api/spenden", (req, res) => {
spendenRouter.post("/workspace/api/spenden/melden", express.json({ limit: "4kb" }),
(req, res) => {
try {
- const cent = Math.round(Number(req.body?.betrag_cent) || 0);
- if (!Number.isFinite(cent) || cent <= 0 || cent > BETRAG_MAX_CENT) {
- return res.status(400).json({ fehler: "betrag_unklar" });
- }
+ const cent = centAusDogen(req.body?.dogen_mille);
+ if (cent === null) return res.status(400).json({ fehler: "betrag_unklar" });
/* EINE MELDUNG JE MINUTE UND PERSON. Nicht gegen Betrug -- die
Leitung bestaetigt ja -- sondern gegen den Doppelklick und
gegen eine Liste, die man nicht mehr durchsieht. */
@@ -256,7 +439,23 @@ spendenRouter.post("/workspace/api/spenden/melden", express.json({ limit: "4kb"
WHERE person_id = ? AND erstellt > ?`).get(req.person.id, kurz);
if ((schon?.n || 0) >= 1) return res.status(429).json({ fehler: "zu_schnell" });
- const name = String(req.body?.name ?? req.person.name).trim().slice(0, NAME_MAX);
+ /* ==== DER TEXT KOMMT VON DEM, DER GIBT =====================
+
+ Filipe: „der text der dazu erscheint sollen die leute
+ selber schreiben koennen ... es sollen personalisierte
+ texte sein von den leuten selbst."
+
+ Bisher schrieb ihn die Leitung ab, wenn ueberhaupt. Jetzt
+ steht er da, wo er hingehoert: bei dem, der ihn meint.
+
+ UND ER GEHT TROTZDEM DURCH DIE BESTAETIGUNG. Eine Meldung
+ wartet ohnehin auf einen Tipp der Leitung -- der Text laeuft
+ damit nie unbesehen ins Bild, und die Leitung kann ihn
+ aendern oder leeren, bevor sie bestaetigt. Ohne diese
+ Schranke waere ein freies Textfeld, das im Livestream
+ erscheint, die offenste Tuer des ganzen Hauses. */
+ const name = String(req.body?.name ?? req.person.name).trim().slice(0, NAME_MAX)
+ || req.person.name;
const gruss = String(req.body?.gruss ?? "").trim().slice(0, GRUSS_MAX);
const { lastInsertRowid } = db().prepare(`INSERT INTO spenden
(betrag_cent, waehrung, name, gruss, person_id, herkunft, stand, erstellt)
@@ -282,24 +481,47 @@ spendenRouter.post("/workspace/api/spenden/melden", express.json({ limit: "4kb"
spendenRouter.post("/workspace/api/spenden", nurGeld, express.json({ limit: "4kb" }),
(req, res) => {
try {
- const cent = Math.round(Number(req.body?.betrag_cent) || 0);
- if (!Number.isFinite(cent) || cent <= 0 || cent > BETRAG_MAX_CENT) {
- return res.status(400).json({ fehler: "betrag_unklar" });
- }
+ const cent = centAusDogen(req.body?.dogen_mille);
+ if (cent === null) return res.status(400).json({ fehler: "betrag_unklar" });
const name = String(req.body?.name ?? "").trim().slice(0, NAME_MAX);
if (!name) return res.status(400).json({ fehler: "name_fehlt" });
const gruss = String(req.body?.gruss ?? "").trim().slice(0, GRUSS_MAX);
+ /* ==== WEM GEHOEREN DIE DOGEN? ==============================
+
+ Freiwillig. Die Leitung traegt eine Spende oft von einem
+ Handy-Hinweis ab, auf dem nur ein Vorname steht -- daraus
+ eine Person zu RATEN waere schlimmer als keine Punkte zu
+ vergeben. Steht niemand da, laeuft die Karte trotzdem.
+
+ Geprueft wird die Person gegen das eigene Haus: Eine
+ Kennung aus der Agentur waere eine Gutschrift ueber die
+ Hausgrenze hinweg, und genau das darf seit dem 24.09. nicht
+ mehr passieren -- auch nicht durch einen Tippfehler in
+ einer Zahl. */
+ let wem = null;
+ if (req.body?.person_id) {
+ const p = db().prepare(`SELECT id FROM personen
+ WHERE id = ? AND aktiv = 1 ${hausBedingung(req.person)}`)
+ .get(Number(req.body.person_id) || 0);
+ if (!p) return res.status(400).json({ fehler: "person_unbekannt" });
+ wem = p.id;
+ }
+
const { lastInsertRowid } = db().prepare(`INSERT INTO spenden
- (betrag_cent, waehrung, name, gruss, herkunft, stand, erstellt,
+ (betrag_cent, waehrung, name, gruss, person_id, herkunft, stand, erstellt,
bestaetigt_am, bestaetigt_von)
- VALUES (?,?,?,?,'hand','bestaetigt',?,?,?)`)
- .run(cent, "EUR", name, gruss, jetzt(), jetzt(), req.person.id);
+ VALUES (?,?,?,?,?,'hand','bestaetigt',?,?,?)`)
+ .run(cent, "EUR", name, gruss, wem, jetzt(), jetzt(), req.person.id);
const z = db().prepare("SELECT * FROM spenden WHERE id = ?").get(Number(lastInsertRowid));
protokolliere("spende_hand", {
personId: req.person.id,
- detail: `${cent} Cent von ${name}`,
+ /* AUCH IM PROTOKOLL KEIN GELD. Es ist eine Liste, die im
+ Haus gelesen wird -- und „nie" heisst nie. Die Cent
+ stehen ohnehin in der Spendenzeile selbst, dort gehoeren
+ sie hin. */
+ detail: `${centInMille(cent)} Tausendstel Dogen von ${name}`,
ip: echteIp(req),
});
zeigen(z);
@@ -322,21 +544,32 @@ spendenRouter.post("/workspace/api/spenden/:id(\\d+)/bestaetigen", nurGeld,
if (z.stand === "bestaetigt") return res.json(alsKarte(z));
/* Beim Bestaetigen darf der Betrag berichtigt werden: Die
- Leitung hat die PayPal-Meldung vor sich und weiss es besser
- als die Behauptung. */
- const cent = req.body?.betrag_cent === undefined
- ? z.betrag_cent : Math.round(Number(req.body.betrag_cent) || 0);
- if (!Number.isFinite(cent) || cent <= 0 || cent > BETRAG_MAX_CENT) {
- return res.status(400).json({ fehler: "betrag_unklar" });
+ Leitung hat die Zahlungsmeldung vor sich und weiss es
+ besser als die Behauptung. */
+ let cent = z.betrag_cent;
+ if (req.body?.dogen_mille !== undefined) {
+ cent = centAusDogen(req.body.dogen_mille);
+ if (cent === null) return res.status(400).json({ fehler: "betrag_unklar" });
}
+ /* ==== UND DER TEXT DARF BERICHTIGT WERDEN ==================
+ Das ist die Schranke vor dem Stream. Der Gruss kommt jetzt
+ von dem, der gibt -- die Leitung sieht ihn hier zum ersten
+ Mal und entscheidet, ob er so ins Bild geht. Ohne diese
+ Moeglichkeit bliebe nur „ganz ablehnen", und dann faellt
+ wegen eines Wortes eine echte Spende unter den Tisch. */
+ const name = req.body?.name === undefined
+ ? z.name : (String(req.body.name).trim().slice(0, NAME_MAX) || z.name);
+ const gruss = req.body?.gruss === undefined
+ ? z.gruss : String(req.body.gruss).trim().slice(0, GRUSS_MAX);
+
db().prepare(`UPDATE spenden SET stand = 'bestaetigt', betrag_cent = ?,
- bestaetigt_am = ?, bestaetigt_von = ? WHERE id = ?`)
- .run(cent, jetzt(), req.person.id, z.id);
+ name = ?, gruss = ?, bestaetigt_am = ?, bestaetigt_von = ? WHERE id = ?`)
+ .run(cent, name, gruss, jetzt(), req.person.id, z.id);
const neu = db().prepare("SELECT * FROM spenden WHERE id = ?").get(z.id);
protokolliere("spende_bestaetigt", {
personId: req.person.id,
- detail: `#${z.id}, ${cent} Cent`,
+ detail: `#${z.id}, ${centInMille(cent)} Tausendstel Dogen`,
ip: echteIp(req),
});
zeigen(neu);
@@ -386,10 +619,8 @@ spendenRouter.put("/workspace/api/spenden/stufen", nurGeld,
const sauber = [];
for (const z of roh) {
- const ab = Math.round(Number(z?.ab_cent));
- if (!Number.isFinite(ab) || ab <= 0 || ab > BETRAG_MAX_CENT) {
- return res.status(400).json({ fehler: "stufe_betrag_unklar" });
- }
+ const ab = centAusDogen(z?.ab_mille);
+ if (ab === null) return res.status(400).json({ fehler: "stufe_betrag_unklar" });
const name = String(z?.name ?? "").trim().slice(0, NAME_MAX);
if (!name) return res.status(400).json({ fehler: "stufe_name_fehlt" });
@@ -521,7 +752,18 @@ spendenRouter.put("/workspace/api/spenden/stufen", nurGeld,
function stufenLesen() {
return db().prepare("SELECT id, ab_cent, name, vorlage, bild, farbe, dauer_ms,"
+ " groesse FROM spenden_stufen ORDER BY ab_cent").all()
- .map((z) => ({ ...z, bild: bildAdresse(z.bild), groesse: Number(z.groesse) || 1 }));
+ .map((z) => ({
+ id: z.id,
+ /* AUCH DIE GRENZE IST EINE ZAHL IN DOGEN. `ab_cent` bleibt der
+ Name in der Datenbank -- dort ist es die Ablage, und eine
+ Spalte umzubenennen heisst, eine Tabelle neu zu bauen; genau
+ dabei sind am 11.09. drei Spalten verlorengegangen. Nach
+ aussen geht nur `ab_mille`. */
+ ab_mille: centInMille(z.ab_cent),
+ name: z.name, vorlage: z.vorlage, bild: bildAdresse(z.bild),
+ farbe: z.farbe, dauer_ms: z.dauer_ms,
+ groesse: Number(z.groesse) || 1,
+ }));
}
/** Woran man ein Bild erkennt -- an seinem Inhalt und nicht an seinem
@@ -583,6 +825,65 @@ spendenRouter.post("/workspace/api/spenden/stufe/:id(\\d+)/bild", nurGeld,
}
});
+/* ======================================================================
+ DAS DOGEN-ZEICHEN
+
+ Filipe: „die dogen auch mit dem symbol, die symbole schick ich dir
+ spaeter. bereite jetzt alles vor."
+
+ EIN BILD IN DEN EINSTELLUNGEN UND NICHT IM QUELLTEXT. Laege es als
+ Datei im Verzeichnis, haenge jedes neue Symbol an einem Deploy --
+ also an mir. So laedt Filipe es selbst hoch, und zwar an derselben
+ Stelle und mit derselben Pruefung wie die Stufenbilder: Die Art
+ wird am INHALT erkannt, nicht am Dateinamen.
+
+ ES LIEGT BEI DEN STUFENBILDERN, und das hat einen Grund: Die
+ Spendentafel in OBS hat keine Anmeldung, und der Weg
+ `/workspace/api/buehne/spendenbild/…` ist der einzige, der Bilder
+ ohne Schranke ausliefert. Ein Zeichen, das nur angemeldet
+ sichtbar waere, fehlte ausgerechnet im Stream.
+ ====================================================================== */
+spendenRouter.post("/workspace/api/spenden/zeichen", nurGeld,
+ express.raw({ type: "*/*", limit: BILD_MAX_BYTES }), zuGross, (req, res) => {
+ try {
+ const endung = bildart(req.body);
+ if (!endung) return res.status(400).json({ fehler: "kein_bild" });
+
+ mkdirSync(BILDER, { recursive: true });
+ const name = `${randomBytes(16).toString("hex")}.${endung}`;
+ writeFileSync(join(BILDER, name), req.body);
+
+ const alt = einstellung(ZEICHEN_NAME, "");
+ einstellungSetzen(ZEICHEN_NAME, name, req.person);
+ /* Das alte wegraeumen -- sonst bleibt bei jedem Wechsel eine
+ Datei liegen, die niemand mehr findet. */
+ if (alt && alt !== name) {
+ try { unlinkSync(join(BILDER, alt)); } catch { /* war schon weg */ }
+ }
+ protokolliere("dogen_zeichen", {
+ personId: req.person.id,
+ detail: `${Math.round(req.body.length / 1024)} KB`,
+ ip: echteIp(req),
+ });
+ res.json({ ok: true, zeichen: zeichenAdresse() });
+ } catch (fehler) {
+ console.error("[spenden] Zeichen:", fehler?.message);
+ res.status(503).json({ fehler: "nicht_verfuegbar" });
+ }
+ });
+
+spendenRouter.delete("/workspace/api/spenden/zeichen", nurGeld, (req, res) => {
+ try {
+ const alt = einstellung(ZEICHEN_NAME, "");
+ einstellungSetzen(ZEICHEN_NAME, "", req.person);
+ if (alt) { try { unlinkSync(join(BILDER, alt)); } catch { /* war weg */ } }
+ res.json({ ok: true, zeichen: "" });
+ } catch (fehler) {
+ console.error("[spenden] Zeichen loeschen:", fehler?.message);
+ res.status(503).json({ fehler: "nicht_verfuegbar" });
+ }
+});
+
spendenRouter.delete("/workspace/api/spenden/stufe/:id(\\d+)/bild", nurGeld, (req, res) => {
try {
const id = Number(req.params.id);
@@ -616,15 +917,13 @@ spendenRouter.delete("/workspace/api/spenden/stufe/:id(\\d+)/bild", nurGeld, (re
spendenRouter.post("/workspace/api/spenden/probe", nurGeld,
express.json({ limit: "2kb" }), (req, res) => {
try {
- const cent = Math.round(Number(req.body?.betrag_cent));
- if (!Number.isFinite(cent) || cent <= 0 || cent > BETRAG_MAX_CENT) {
- return res.status(400).json({ fehler: "betrag_unklar" });
- }
+ const cent = centAusDogen(req.body?.dogen_mille);
+ if (cent === null) return res.status(400).json({ fehler: "betrag_unklar" });
const st = stufeFuer(cent);
const karte = {
id: 0,
- betrag_cent: cent,
- waehrung: "EUR",
+ dogen_mille: centInMille(cent),
+ zeichen: zeichenAdresse(),
/* 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 4598ba7d..a2ad7290 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`. -->
-
-
+
+
-
+