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