diff --git a/server/mess-support-runde.mjs b/server/mess-support-runde.mjs new file mode 100644 index 00000000..79d80477 --- /dev/null +++ b/server/mess-support-runde.mjs @@ -0,0 +1,215 @@ +/* ===================================================================== + WIE DER SUPPORT JETZT AUSSIEHT UND SICH ANFUEHLT (25.09.2026) + + Filipe, zwei Wuensche an einem Abend: + „ich will dass die leute die mir was geschickt haben im support, + auch meine notiz bekommen wenn ich fertig bin ... und erst wenn + es funktioniert gedrueckt wird, will ich dass alles richtig + fertig ist." + „ich will dass du bitte diese kachel viel geiler viel + profissioneller machst, die soll koomplett anders aussehen und + strukturiert sein ... die hauptfarbe der kachel soll auch + babyblau sein mit bissl lila." + + SIE PRUEFT NICHTS und meldet keinen Fehlschlag -- sie macht Bilder, + auf denen man nachsehen kann. Deshalb `mess-` und nicht `pruef-`. + Gemessen wird die Wahrheit in `pruef-support.mjs` (58 Pruefungen); + hier geht es um das, was man nur SIEHT. + + VIER BILDER, weil die Sache vier Seiten hat: + 1. Die Leitung sieht die Meldung und ihren Knopf. + 2. Der Melder sieht die Notiz und seine zwei Knoepfe -- der Kern + des ganzen Umbaus. + 3. Derselbe Blick auf dem Handy, 412 px. Das ist die Breite, auf + der die Modis tatsaechlich arbeiten. + 4. Nach zwei Runden: der Verlauf mit beiden Urteilen. Genau der + Fall, den Filipe gemeint hat („manchmal ... nicht sofort + perfekt"). + + EIGENE WEGWERF-DATENBANK in einem `mktemp`-Ordner: Es entstehen + echte Meldungen. In einem laufenden System waeren das Testdaten, + und die sind verboten. + + Aufrufen mit: node server/mess-support-runde.mjs + ===================================================================== */ +import { mkdtempSync, rmSync, readFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { execFileSync } from "node:child_process"; +import { request as httpAnfrage } from "node:http"; +import { createServer as httpsServer } from "node:https"; + +const PORT = 5411, HP = 5412; +const CREW = "crew.dogfather-universe.com"; +const ordner = mkdtempSync(join(tmpdir(), "ws-supportrunde-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = String(PORT); +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; + +const express = (await import("express")).default; +const ec = express.response.cookie; +express.response.cookie = function (n, w, o) { + return ec.call(this, n, w, { ...(o || {}), secure: false }); +}; +import { notbremse } from "./helfer-notbremse.mjs"; +await import("./index.js"); +notbremse(240_000, "mess-support-runde"); +await new Promise((r) => setTimeout(r, 900)); +process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); }); +process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); }); + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); +const sch = randomBytes(32).toString("hex"); +d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") + .run("code_kennung_schluessel", sch, jetzt); +const anlegen = (name, rolle, code) => { + const s = randomBytes(16).toString("hex"); + const h = scryptSync(code, s, 64, + { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); + return d.prepare("INSERT INTO personen" + + " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)" + + " VALUES (?,?,?,?,?,?,1,?) RETURNING id") + .get(name, rolle, h, s, 32768, + createHmac("sha256", sch).update(code).digest("hex"), jetzt).id; +}; +anlegen("Filipe", "admin", "CODE-DOGI-0001"); +const idHand = anlegen("Rechte Hand", "hand", "CODE-HAND-0001"); +const idModi = anlegen("Miss", "modi", "CODE-MODI-0001"); +const idM2 = anlegen("Marina", "modi", "CODE-MODI-0002"); + +/* `von_name` GEHOERT DAZU -- die echte Melderoute schreibt ihn mit. + Ohne ihn saehe die Leitung „Liegt beim Melder" statt „Liegt bei + Miss", und das Bild zeigte einen Fall, den es im Betrieb nicht + gibt. Eine Messung mit zu wenig Daten misst die falsche Seite. */ +const meldung = (person, name, text, seite, stand) => d.prepare(`INSERT INTO support_meldungen + (person_id, von_name, von_rolle, text, seite, stand, erstellt, geaendert) + VALUES (?,?,?,?,?,?,?,?) RETURNING id`) + .get(person, name, "modi", text, seite, stand, jetzt, jetzt).id; + +const offen = meldung(idM2, "Marina", + "Der Kalender springt beim Wischen manchmal zwei Monate weiter.", + "/workspace/kalender.html", "neu"); +const dran = meldung(idModi, "Miss", + "Ich konnte da wieder nicht zurück gehen, musste die App schließen " + + "damit ich wieder auf die Hauptseite kam.", + "/workspace/support.html", "in_arbeit"); +d.close(); + +const anfrage = (pfad, art, koerper, keks) => new Promise((fertig, schief) => { + const a = httpAnfrage({ + host: "127.0.0.1", port: PORT, path: pfad, method: art, + headers: { + Host: CREW, "Content-Type": "application/json", + ...(keks ? { Cookie: keks } : {}), + }, + }, (r) => { + let t = ""; r.on("data", (x) => { t += x; }); + r.on("end", () => fertig({ code: r.statusCode, kopf: r.headers, text: t })); + }); + a.on("error", schief); + if (koerper) a.write(JSON.stringify(koerper)); + a.end(); +}); +const anmelden = async (rolle, code) => { + const an = await anfrage("/workspace/api/anmelden", "POST", { rolle, code }); + if (an.code !== 200) { console.log(`ABBRUCH: Anmeldung ${rolle} ${an.code}`); process.exit(2); } + return (an.kopf["set-cookie"] || []).map((x) => x.split(";")[0]).join("; "); +}; +const kHand = await anmelden("hand", "CODE-HAND-0001"); +const kModi = await anmelden("modi", "CODE-MODI-0001"); + +/* --- Runde 1: die Leitung sagt „behoben" ---------------------------- */ +await anfrage(`/workspace/api/support/${dran}`, "PATCH", + { stand: "erledigt", antwort: "Der Zurück-Knopf ist wieder da – " + + "er war hinter der Kopfleiste verschwunden." }, kHand); + +const schl = join(ordner, "b.key"), zert = join(ordner, "b.crt"); +execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes", "-keyout", schl, + "-out", zert, "-days", "2", "-subj", `/CN=${CREW}`, "-addext", `subjectAltName=DNS:${CREW}`], +{ stdio: "ignore" }); +const vorbau = httpsServer({ key: readFileSync(schl), cert: readFileSync(zert) }, + (von, nach) => { + const w = httpAnfrage({ + host: "127.0.0.1", port: PORT, path: von.url, method: von.method, + headers: { ...von.headers, host: `${CREW}:${HP}` }, + }, (a) => { nach.writeHead(a.statusCode || 502, a.headers); a.pipe(nach); }); + von.pipe(w); + }); +await new Promise((r) => vorbau.listen(HP, "127.0.0.1", r)); + +const pw = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); +const browser = await pw.chromium.launch({ + args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1`, "--ignore-certificate-errors"], +}); + +async function blicken(name, keks, breite, hoehe) { + const kontext = await browser.newContext({ viewport: { width: breite, height: hoehe } }); + await kontext.addCookies(keks.split("; ").map((k) => { + const [n, ...r] = k.split("="); + return { name: n, value: r.join("="), domain: CREW, path: "/" }; + })); + const seite = await kontext.newPage(); + const konsole = []; + seite.on("console", (e) => { + if (e.type() === "error" && !/favicon|ERR_CERT|net::ERR/i.test(e.text())) { + konsole.push(e.text().slice(0, 140)); + } + }); + await seite.goto(`https://${CREW}:${HP}/workspace/support.html`, { waitUntil: "networkidle" }); + await seite.waitForTimeout(1400); + const blick = await seite.evaluate(() => { + const karten = [...document.querySelectorAll(".s-karte")]; + const knopf = (k) => [...k.querySelectorAll(".s-karte__tun button")] + .map((b) => b.textContent.trim()).join(" | "); + return { + karten: karten.length, + staende: karten.map((k) => k.querySelector(".s-karte__stand")?.textContent.trim()) + .join(" / "), + knoepfe: karten.map(knopf).filter(Boolean).join(" // ") || "-", + /* Der Hinweis „wer ist dran" wird MITGEMESSEN -- er ist einmal + stumm ausgefallen (before() auf einem heimatlosen Element), + und im Bild sah das aus wie Absicht. */ + wartet: [...document.querySelectorAll(".s-karte__wartet")] + .map((w) => w.textContent.trim()).join(" | ") || "-", + runden: document.querySelectorAll(".s-runde").length, + urteile: [...document.querySelectorAll(".s-runde__urteil")] + .map((u) => u.dataset.urteil).join(", ") || "-", + akzent: getComputedStyle(document.querySelector(".inhalt")).getPropertyValue("--akzent").trim(), + felder: document.querySelectorAll(".s-bilanz__feld").length, + }; + }); + console.log(`\n=== ${name} (${breite} x ${hoehe}) ===`); + for (const [k, v] of Object.entries(blick)) console.log(` ${k.padEnd(10)} ${v}`); + if (konsole.length) console.log(" FEHLER IN DER KONSOLE:", konsole); + const datei = `server/mess-support-${name}.png`; + await seite.screenshot({ path: datei, fullPage: false }).catch(() => {}); + console.log(` Bild: ${datei}`); + await kontext.close(); +} + +try { + await blicken("leitung-pc", kHand, 1280, 1000); + await blicken("melder-pc", kModi, 1280, 1000); + await blicken("melder-handy", kModi, 412, 915); + + /* --- Runde 2: „noch nicht", dann behoben, dann bestaetigt --------- */ + await anfrage(`/workspace/api/support/${dran}/rueckmeldung`, "POST", + { geht: false, text: "Der Knopf ja – aber jetzt springt die Seite nach oben." }, kModi); + await anfrage(`/workspace/api/support/${dran}`, "PATCH", + { stand: "erledigt", antwort: "Das Springen kam vom Nachladen. Ist raus." }, kHand); + await blicken("melder-runde2", kModi, 1280, 1000); + + console.log(`\n(Die offene Meldung #${offen} bleibt unangetastet stehen –` + + " so sieht man beide Sorten nebeneinander.)"); +} finally { + await browser.close(); + vorbau.close(); + try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } + process.exit(0); +} diff --git a/server/pruef-kachelfarben.mjs b/server/pruef-kachelfarben.mjs index bcc19643..7d6dc9c9 100644 --- a/server/pruef-kachelfarben.mjs +++ b/server/pruef-kachelfarben.mjs @@ -63,6 +63,33 @@ const FESTGELEGT = [ warum: "knalliges Rot -- wer den vertraulichen Weg sucht, soll ihn finden, ohne zu lesen" }, { ton: 39, kachel: "Draußen", warum: "die Farbe, die Regeln & Hilfe hatte -- ausdruecklicher Wunsch (screen5)" }, + /* BABYBLAU IST PER DEFINITION BLASS (25.09.2026). + + Filipe: „die hauptfarbe der kachel soll auch babyblau sein mit + bissl lila." + + Diese Farbe haelt die Buntheitsgrenze von 0,12 NICHT (sie liegt + bei 0,074) -- und kann sie nicht halten, das liegt am Wort. + Nachgemessen mit `node tools/_babyblau-suchen.mjs`: Unter allen + Blautoenen im Umkreis des Wunsches gibt es KEINEN, der gleichzeitig + Buntheit >= 0,12 und Abstand >= 0,09 schafft -- der beste kommt + auf 0,075 Abstand. Der Blaubereich ist von den Toenen 15, 23, 26, + 31, 41 und 43 besetzt. + + DIE GRENZE WIRD DESHALB NICHT GESENKT, sondern hier BENANNT + ausgenommen. Eine gesenkte Grenze gaelte fuer alle 45 Toene und + liesse ab morgen jede blasse Farbe durch; eine benannte Ausnahme + gilt fuer genau diese eine und steht mit ihrem Grund da. + + UND SIE IST NICHT GRATIS: Wer blass sein darf, muss dafuer beim + ABSTAND ueberdurchschnittlich sein -- das ist der Zweck, den die + Buntheitsregel nur stellvertretend misst. Gemessen 0,1125 zur + naechsten Farbe (Ton 15), das 7,3-Fache des engsten Paares, das im + Haus ohnehin steht. Die Bedingung dazu steht unten bei der + Pruefung und ist echt: Faellt der Abstand unter 0,10, wird diese + Zeile rot. */ + { ton: 44, kachel: "Support", hex: "#a8d8ff", blassErlaubt: 0.10, + warum: "Babyblau auf Wunsch -- blass, dafuer mit gemessenem Sicherheitsabstand" }, ]; let ok = 0, fehl = 0; @@ -265,16 +292,43 @@ for (const f of FESTGELEGT) { console.log("\n=== Lesbar und kraeftig ==="); { const dunkel = [], blass = []; + /* Die benannten Ausnahmen -- abgeleitet aus FESTGELEGT und nicht + hier noch einmal aufgezaehlt. Eine zweite Liste waere die, die + beim naechsten Mal vergessen wird. */ + const darfBlassSein = new Map( + FESTGELEGT.filter((f) => f.blassErlaubt).map((f) => [f.ton, f.blassErlaubt])); for (const n of benutzt.keys()) { const hex = farben.get(n); if (!hex) continue; if (kontrast(hex) < 4.5) dunkel.push(`Ton ${n} ${hex} (${kontrast(hex).toFixed(2)}:1)`); - if (buntheit(hex) < MINDEST_BUNTHEIT) blass.push(`Ton ${n} ${hex} (${buntheit(hex).toFixed(3)})`); + if (buntheit(hex) < MINDEST_BUNTHEIT && !darfBlassSein.has(n)) { + blass.push(`Ton ${n} ${hex} (${buntheit(hex).toFixed(3)})`); + } } pruef(`alle ${benutzt.size} benutzten Toene erreichen 4,5:1 gegen den Grund`, dunkel.length === 0, dunkel.join(", ")); - pruef(`keiner ist blasser als ${MINDEST_BUNTHEIT}`, + pruef(`keiner ist blasser als ${MINDEST_BUNTHEIT}` + + (darfBlassSein.size ? ` (ausser ${darfBlassSein.size} benannten)` : ""), blass.length === 0, blass.join(", ")); + + /* WAS DIE AUSNAHME KOSTET, WIRD EINGEFORDERT. Ohne diese Zeile waere + `blassErlaubt` ein Freibrief: Man setzt eine Farbe fest, und die + Pruefung sieht ab da weg. So ist es umgekehrt -- wer blass sein + darf, muss beim Abstand BESSER sein als die Hausgrenze, und das + wird gemessen, nicht versprochen. */ + for (const [ton, noetig] of darfBlassSein) { + const hex = farben.get(ton); + if (!hex) { pruef(`Ton ${ton} ist festgelegt, steht aber nicht in start.css`, false); continue; } + let naechste = Infinity, wer = "-"; + for (const [m, h2] of farben) { + if (m === ton || !benutzt.has(m)) continue; + const d = abstand(oklab(hex), oklab(h2)); + if (d < naechste) { naechste = d; wer = `Ton ${m}`; } + } + pruef(`Ton ${ton} darf blass sein und haelt dafuer ${noetig} Abstand ` + + `(${naechste.toFixed(4)} zu ${wer})`, naechste >= noetig, + `nur ${naechste.toFixed(4)} -- die Ausnahme ist damit nicht mehr gedeckt`); + } } /* ---- Gegenprobe: kann diese Pruefung ueberhaupt rot werden? --------- */ @@ -291,6 +345,14 @@ console.log("\n=== Gegenprobe ==="); abstand(weit, weit2) >= MINDEST_ABSTAND); pruef("ein zu dunkler Ton wuerde auffallen (#101820)", kontrast("#101820") < 4.5); pruef("ein blasser Ton wuerde auffallen (#ffe5ae)", buntheit("#ffe5ae") < MINDEST_BUNTHEIT); + /* Und die Ausnahme selbst muss zu eng werden koennen. Sonst waere + die Bedingung darueber nur Text. */ + { + const nah = FESTGELEGT.find((f) => f.blassErlaubt); + pruef("die Ausnahme-Bedingung schlaegt bei zu wenig Abstand an " + + `(0.05 < ${nah?.blassErlaubt ?? "-"})`, + !!nah && 0.05 < nah.blassErlaubt); + } } /* ===================================================================== diff --git a/server/pruef-support.mjs b/server/pruef-support.mjs index 5d796e1d..2a13a3e3 100644 --- a/server/pruef-support.mjs +++ b/server/pruef-support.mjs @@ -448,6 +448,16 @@ melde("\n=== Sich kuemmern ==="); `„erledigt" schickt die Meldung zuerst an den Melder (${nach1?.stand})`); ok(/Behoben/.test(nach1?.antwort || "") && nach1?.darf_bestaetigen === true, `er sieht die Notiz und darf bestaetigen (${nach1?.darf_bestaetigen})`); + + /* UND DIE LEITUNG SIEHT DIESE KNOEPFE NICHT. Im ersten Anlauf tat + sie es: Der Server lehnte sie richtig mit 404 ab, die Karte bot + ihr die Knöpfe trotzdem an -- `darf_bestaetigen` war eine + Aussage ueber die Meldung statt ueber den Betrachter. Gefunden + hat das kein Test, sondern ein Bildschirmfoto. Ab jetzt findet + es der Test. */ + const beiDerLeitung = await standVon("hand", meldungMitBild); + ok(beiDerLeitung?.darf_bestaetigen === false, + `die Leitung bekommt diese Knoepfe NICHT (${beiDerLeitung?.darf_bestaetigen})`); ok((nach1?.verlauf || []).length === 1 && nach1.verlauf[0].nr === 1, `und der Verlauf hat Runde 1 (${(nach1?.verlauf || []).length})`); @@ -461,6 +471,27 @@ melde("\n=== Sich kuemmern ==="); ok(fremd.status === 404, `Gegenprobe: die Leitung bestaetigt nicht fuer den Melder (${fremd.status})`); + /* ---- NACHLEGEN ERSETZT, ES VERDOPPELT NICHT ------------------ + + Der Leitung faellt hinterher noch etwas ein, waehrend die + Meldung schon beim Melder liegt. Ohne ON CONFLICT entstuende + eine ZWEITE Zeile „Runde 1", und sein spaeteres Urteil traefe + beide gleichzeitig -- der Verlauf zeigte dasselbe Urteil unter + zwei verschiedenen Antworten. */ + const nachgelegt = await hol("hand", `/workspace/api/support/${meldungMitBild}`, { + method: "PATCH", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ stand: "erledigt", antwort: "Behoben – und der Rand stimmt auch wieder." }), + }); + ok(nachgelegt.status === 200, `die Leitung legt noch etwas nach (${nachgelegt.status})`); + + const nachNachtrag = await standVon("modi", meldungMitBild); + ok((nachNachtrag?.verlauf || []).length === 1, + `der Verlauf hat immer noch EINE Runde (${(nachNachtrag?.verlauf || []).length})`); + ok(/Rand stimmt/.test((nachNachtrag?.verlauf || [])[0]?.antwort || ""), + "und darin steht der neue Satz, nicht der alte"); + ok(nachNachtrag?.stand === "wartet", + `die Meldung liegt weiter beim Melder (${nachNachtrag?.stand})`); + /* ---- „Noch nicht" -- und ohne Wort geht das nicht ------------- */ const ohneWort = await hol("modi", `/workspace/api/support/${meldungMitBild}/rueckmeldung`, { method: "POST", headers: { "Content-Type": "application/json" }, diff --git a/server/support-tabellen.js b/server/support-tabellen.js index bcea965c..4aec5567 100644 --- a/server/support-tabellen.js +++ b/server/support-tabellen.js @@ -182,7 +182,43 @@ export function supportTabellen(d) { urteil_text TEXT, urteil_am TEXT ); - CREATE INDEX IF NOT EXISTS idx_support_runden_meldung + /* EINDEUTIG, NICHT NUR SCHNELL (25.09.2026). + + Hier stand ein gewoehnlicher Index. Dann faellt auf: Antwortet + die Leitung ein zweites Mal, waehrend die Meldung schon beim + Melder liegt, entsteht eine ZWEITE Zeile mit derselben Nummer -- + und sein spaeteres „geht wieder" beschriftet dann beide auf + einmal (das UPDATE trifft ueber meldung_id und nr -- KEINE + Schraegstriche drum herum: Dieser Text steht in einem + Template-Literal, und ein Gegen-Apostroph darin beendet es. Das + hat heute Nacht schon einmal den Start gekostet). Der + Verlauf zeigte zweimal „Runde 1" mit demselben Urteil. + + Der Index macht das unmoeglich, statt es zu verbieten. Die + Route traegt seither mit ON CONFLICT nach: Wer nachlegt, + ERSETZT seine Antwort in derselben Runde. Beides zusammen -- + ohne den Index waere ON CONFLICT wirkungslos, ohne ON CONFLICT + wuerde der Index das Nachlegen mit einem Fehler beantworten. + + UNBEDENKLICH BEIM UMSTELLEN: Die Tabelle ist in derselben Nacht + entstanden; es gibt nur einen Weg, der sie fuellt. Gaebe es doch + eine Dublette, schluege der Aufbau LAUT fehl statt still. + + UND WARUM DER NAME EIN ANDERER IST -- der eigentliche Fund: + Hier stand zuerst CREATE UNIQUE INDEX IF NOT EXISTS unter dem + ALTEN Namen. Lokal war alles gruen, weil jede Pruefung ihre + Datenbank frisch anlegt. Auf dem Server gab es den Namen aber + schon, als gewoehnlichen Index -- und IF NOT EXISTS sieht nur + den NAMEN, nicht die Bauart. Der Index waere also nie eindeutig + geworden, ON CONFLICT haette kein Ziel gefunden, und das + Nachlegen waere mit einem Fehler abgebrochen. GENAU DORT, wo + lokal alles gruen ist. Aufgefallen beim Durchspielen auf einer + Kopie der echten Datenbank, nicht in der Pruefung. + + Deshalb: den alten ausdruecklich wegnehmen, den neuen unter + eigenem Namen anlegen. Beides vertraegt jede Wiederholung. */ + DROP INDEX IF EXISTS idx_support_runden_meldung; + CREATE UNIQUE INDEX IF NOT EXISTS idx_support_runden_einmal ON support_runden (meldung_id, nr); /* Die Leitung fragt „was ist offen?", der Melder „was ist meins?". diff --git a/server/workspace-support.js b/server/workspace-support.js index 96f0392a..8127c2f3 100644 --- a/server/workspace-support.js +++ b/server/workspace-support.js @@ -111,6 +111,23 @@ function meldungFuer(person, id) { return m.person_id === person?.id ? m : null; } +/** Darf DIESE Person bei DIESER Meldung jetzt bestätigen? + * + * AN EINER STELLE, weil es zwei Stellen gibt, die es wissen müssen: + * die Route, die die Antwort annimmt, und die Anzeige, die die + * Knöpfe zeigt. Standen sie getrennt da, driften sie — und genau das + * ist am 25.09.2026 im ersten Anlauf passiert: Der Server lehnte die + * Leitung richtig mit 404 ab, die Karte zeigte ihr die Knöpfe + * trotzdem. Zwei Knöpfe, die nur eine Fehlermeldung können, sind + * schlimmer als gar keine. + * + * ES IST DER MELDER, NICHT „WER NICHT LEITUNG IST": Die Leitung darf + * ihre eigene Meldung sehr wohl selbst bestätigen — sie hat sie ja + * geschrieben. Verboten ist nur, FREMDE Arbeit abzunicken. */ +function darfBestaetigen(m, personId) { + return !!m && m.person_id === personId && m.stand === SUPPORT_STAND.wartet; +} + /** Wie eine Meldung nach draußen aussieht. * * AN EINER STELLE, weil vier Routen sie ausgeben. Vier Stellen wären @@ -119,8 +136,13 @@ function meldungFuer(person, id) { * * `fuerLeitung` entscheidet nur über die Zusatzangaben, nicht über * den Inhalt: Wer meldet, sieht seine eigene Meldung vollständig, - * auch den Namen, den er selbst trägt. */ -function nachDraussen(m, fuerLeitung) { + * auch den Namen, den er selbst trägt. + * + * `werFragtId` ist Pflicht, seit es die Knöpfe des Melders gibt — + * ohne sie ist `darf_bestaetigen` eine Aussage über die Meldung + * statt über den Betrachter, und die Leitung bekommt Knöpfe, die sie + * nicht drücken darf. */ +function nachDraussen(m, fuerLeitung, werFragtId) { return { id: m.id, text: m.text, @@ -162,7 +184,7 @@ function nachDraussen(m, fuerLeitung) { /* DARF ICH JETZT BESTAETIGEN? Vom Server beantwortet, nicht im Browser nachgerechnet: Dort stuende die Regel ein zweites Mal, und die zweite Fassung waere ab der naechsten Aenderung falsch. */ - darf_bestaetigen: m.stand === SUPPORT_STAND.wartet, + darf_bestaetigen: darfBestaetigen(m, werFragtId), }; } @@ -202,7 +224,7 @@ supportRouter.get("/workspace/api/support", (req, res) => { res.json({ leitung, - meldungen: zeilen.map((m) => nachDraussen(m, leitung)), + meldungen: zeilen.map((m) => nachDraussen(m, leitung, req.person.id)), /* Die Zahlen stehen in der Antwort und werden nicht im Browser nachgezählt: Die Liste ist bei 300 abgeschnitten, eine Zählung darauf wäre ab der 301. Meldung still falsch. */ @@ -347,7 +369,9 @@ supportRouter.post("/workspace/api/support", } const frisch = db().prepare("SELECT * FROM support_meldungen WHERE id = ?").get(id); - res.status(201).json({ meldung: nachDraussen(frisch, istLeitung(req.person)) }); + res.status(201).json({ + meldung: nachDraussen(frisch, istLeitung(req.person), req.person.id), + }); } catch (fehler) { /* DIE DATEI GEHT MIT, WENN DIE ZEILE NICHT ZUSTANDE KAM. Sonst sammelt der Ordner Bilder an, zu denen es keine Meldung gibt — @@ -409,7 +433,12 @@ supportRouter.post("/workspace/api/support/:id/rueckmeldung", if (!m || m.person_id !== req.person.id) { return res.status(404).json({ fehler: "nicht_gefunden" }); } - if (m.stand !== SUPPORT_STAND.wartet) { + /* 404 oben war die SICHTBARKEITSFRAGE, das hier ist die + STANDFRAGE -- zwei verschiedene Auskünfte, deshalb zwei Codes. + Gefragt wird mit derselben Funktion, die auch die Knöpfe + zeigt: Was der Bildschirm anbietet und was der Server annimmt, + ist damit ein Satz und nicht zwei. */ + if (!darfBestaetigen(m, req.person.id)) { return res.status(409).json({ fehler: "Hier wartet gerade nichts auf deine Antwort.", }); @@ -468,7 +497,7 @@ supportRouter.post("/workspace/api/support/:id/rueckmeldung", } const frisch = db().prepare("SELECT * FROM support_meldungen WHERE id = ?").get(m.id); - res.json({ meldung: nachDraussen(frisch, false) }); + res.json({ meldung: nachDraussen(frisch, false, req.person.id) }); } catch (fehler) { console.error("[support] Rueckmeldung:", fehler?.message); res.status(503).json({ fehler: "nicht_verfuegbar" }); @@ -559,9 +588,17 @@ supportRouter.patch("/workspace/api/support/:id(\\d+)", express.json(), (req, re entsteht hier und nicht beim Melden: Runde 1 beginnt mit dem ersten „so, probier mal", nicht mit dem Problem. */ if (stand === SUPPORT_STAND.wartet && antwort) { + /* NACHLEGEN ERSETZT, ES VERDOPPELT NICHT. Antwortet die Leitung + ein zweites Mal, waehrend die Meldung schon beim Melder liegt + („ach, und noch etwas ..."), gehoert das in DIESELBE Runde. + Ohne diese Zeile stuenden zwei „Runde 1" untereinander, und + sein spaeteres Urteil traefe beide gleichzeitig. */ db().prepare(`INSERT INTO support_runden (meldung_id, nr, antwort, von_id, von_name, am) - VALUES (?,?,?,?,?,?)`) + VALUES (?,?,?,?,?,?) + ON CONFLICT (meldung_id, nr) DO UPDATE + SET antwort = excluded.antwort, von_id = excluded.von_id, + von_name = excluded.von_name, am = excluded.am`) .run(m.id, m.runde || 1, antwort, req.person.id, req.person.name, n); } @@ -588,7 +625,7 @@ supportRouter.patch("/workspace/api/support/:id(\\d+)", express.json(), (req, re } const frisch = db().prepare("SELECT * FROM support_meldungen WHERE id = ?").get(m.id); - res.json({ meldung: nachDraussen(frisch, true) }); + res.json({ meldung: nachDraussen(frisch, true, req.person.id) }); } catch (fehler) { console.error("[support] Bearbeiten:", fehler?.message); res.status(503).json({ fehler: "nicht_verfuegbar" }); diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index 565325e5..839adc78 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,15 +16,15 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +