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`. --> - - + + - +