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