diff --git a/server/mess-alle-einzelsicht.mjs b/server/mess-alle-einzelsicht.mjs
new file mode 100644
index 00000000..51c9cb48
--- /dev/null
+++ b/server/mess-alle-einzelsicht.mjs
@@ -0,0 +1,246 @@
+/* =====================================================================
+ WER HAT SIE, WER NICHT — die Einzelansicht bei „An alle" (24.09.2026)
+
+ ---------------------------------------------------------------------
+ WARUM ES DIESE DATEI GIBT
+
+ Filipe: „wenn ich eine aufgabe an alle verteile will ich dass
+ dogfather und die rechte hand individuel von jedem sehen wer es
+ gemacht hat oder nicht. aufgabe an alle verteilen aber individuel
+ sehen wir das."
+
+ Gebaut ist das in `vorlagenbrett.js`. Ob es AUSSIEHT wie gedacht,
+ beantwortet kein Quelltext: Die Marken stehen in einer Reihe, die
+ umbricht, in einer Karte, die schmaler wird — und auf einem Handy
+ ist die Karte 380 px breit, nicht 400.
+
+ SIE PRUEFT NICHTS und meldet keinen Fehlschlag. Sie macht ein Bild,
+ auf dem man nachsehen kann. Deshalb `mess-` und nicht `pruef-`: Die
+ Portvergabe leitet sich aus den `pruef-`-Namen ab, und eine
+ Messdatei darf die Nummern der Pruefungen nicht verschieben.
+
+ ---------------------------------------------------------------------
+ EIGENE WEGWERF-DATENBANK, NIE DIE ECHTE
+
+ Es entstehen echte Aufgaben — vier Stueck, damit alle vier Zustaende
+ im Bild sind. In einem laufenden System waeren das Testdaten, und die
+ sind verboten. Hier liegt die Datenbank in einem `mktemp`-Ordner und
+ wird am Ende geloescht.
+
+ Aufrufen mit: node server/mess-alle-einzelsicht.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 = 5397, HP = 5398;
+const CREW = "crew.dogfather-universe.com";
+const ordner = mkdtempSync(join(tmpdir(), "ws-alle-sicht-"));
+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-alle-einzelsicht");
+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;
+};
+
+/* Vier Empfaenger und eine rechte Hand — genau die Besetzung, um die es
+ geht: Die Haende sind getrennt (eine verteilt, eine bekommt). */
+const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
+const idHand = anlegen("VanVan", "hand", "CODE-HAND-0001");
+const idLinke = anlegen("Kessi", "linke", "CODE-LINKE-0001");
+const idM1 = anlegen("Diene", "modi", "CODE-MODI-0001");
+const idM2 = anlegen("Marina", "modi", "CODE-MODI-0002");
+const idM3 = anlegen("Ghost", "modi", "CODE-MODI-0003");
+void idDogi; void idHand;
+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 an = await anfrage("/workspace/api/anmelden", "POST",
+ { rolle: "admin", code: "CODE-DOGI-0001" });
+if (an.code !== 200) {
+ console.log(`ABBRUCH: DogFather ließ sich nicht anmelden (HTTP ${an.code}).`);
+ process.exit(2);
+}
+const keks = (an.kopf["set-cookie"] || []).map((x) => x.split(";")[0]).join("; ");
+
+/* ---- Ein Stand, auf dem man ALLE VIER Zustaende sieht ----------------
+ Sonst zeigt das Bild nur „hat sie nicht", und man weiss nachher
+ nicht, ob die Farben stimmen. */
+/* Der Katalog steckt UNTER `katalog` in der Antwort -- die Route
+ liefert daneben noch Bewerbungen und Rechte. */
+const kat = JSON.parse((await anfrage("/workspace/api/vorlagen", "GET", null, keks)).text);
+const liste = kat?.katalog?.aufgaben;
+if (!Array.isArray(liste) || !liste.length) {
+ console.log("ABBRUCH: kein Katalog in der Antwort --",
+ JSON.stringify(kat).slice(0, 300));
+ process.exit(3);
+}
+const v0 = liste.map((v, i) => ({ ...v, nr: i }))[0];
+const verteilt = await anfrage("/workspace/api/vorlagen/uebernehmen", "POST",
+ { art: "katalog", nr: v0.nr, gruppe: v0.gruppe, person_id: "alle" }, keks);
+console.log(`Verteilt an alle: HTTP ${verteilt.code} ${verteilt.text.slice(0, 80)}`);
+
+const d2 = new DatabaseSync(process.env.WORKSPACE_DB);
+/* Einer hat sie erledigt, einer ist ueberfaellig, einer hat sie gar
+ nicht — die Linke Hand behaelt ihr schlichtes „Offen". */
+d2.prepare("UPDATE aufgaben SET status = 'erledigt' WHERE vorlage = ? AND verantwortlich_id = ?")
+ .run(v0.schluessel, idM1);
+d2.prepare("UPDATE aufgaben SET frist = '2026-09-01' WHERE vorlage = ? AND verantwortlich_id = ?")
+ .run(v0.schluessel, idM2);
+d2.prepare("DELETE FROM aufgaben WHERE vorlage = ? AND verantwortlich_id = ?")
+ .run(v0.schluessel, idM3);
+const stand = d2.prepare(
+ "SELECT verantwortlich_id AS w, status, frist FROM aufgaben WHERE vorlage = ?")
+ .all(v0.schluessel);
+d2.close();
+console.log("Stand:", JSON.stringify(stand),
+ `\n (${idLinke}=Kessi/linke, ${idM1}=Diene, ${idM2}=Marina, ${idM3}=Ghost)`);
+
+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"],
+});
+try {
+ /* ZWEI BREITEN. 412 px ist die haeufigste Android-Breite; an ihr ist
+ am 06.09. die Kopfleiste gescheitert. 1280 px ist der kleinste
+ Rechner, an dem noch jemand arbeitet. */
+ for (const [wie, breite, hoehe] of [["handy", 412, 1400], ["computer", 1280, 1600]]) {
+ 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/entwicklung.html`,
+ { waitUntil: "networkidle" });
+ await seite.waitForTimeout(1200);
+
+ /* Das Brett startet zugeklappt — aufklappen, sonst ist das Bild leer. */
+ await seite.evaluate(() => {
+ const kopf = document.querySelector('.vorlagenbrett__kopf[aria-expanded="false"]');
+ if (kopf) kopf.click();
+ });
+ await seite.waitForTimeout(700);
+
+ /* Den Alle-Knopf waehlen -- das ist der Fall, um den es geht. */
+ const traf = await seite.evaluate(() => {
+ const k = document.querySelector('.stufenleiter--wer [data-alle="ja"]');
+ if (!k) return false;
+ k.click(); return true;
+ });
+ await seite.waitForTimeout(1400);
+
+ const gesehen = await seite.evaluate(() => {
+ const karte = document.querySelector('.vorlagenbrett__karte .vorlagenbrett__wer')
+ ?.closest('.vorlagenbrett__karte');
+ const reihe = [...(karte?.querySelectorAll('.vorlagenbrett__wer-p') || [])]
+ .map((m) => ({
+ wer: m.querySelector('.vorlagenbrett__wer-name')?.textContent,
+ stand: m.querySelector('.vorlagenbrett__wer-stand')?.textContent,
+ art: m.dataset.stand,
+ }));
+ return {
+ wen: [...document.querySelectorAll('.stufenleiter--wer .stufenleiter__knopf')]
+ .map((k) => k.textContent.trim().replace(/\s+/g, ' ')),
+ titel: karte?.querySelector('.vorlagenbrett__titel')?.textContent,
+ fuss: karte?.querySelector('.vorlagenbrett__schon')?.textContent,
+ knopf: karte?.querySelector('.knopf')?.textContent,
+ hinweis: karte?.querySelector('.knopf')?.title,
+ reihe,
+ mitReihe: document.querySelectorAll('.vorlagenbrett__wer').length,
+ };
+ });
+
+ console.log(`\n=== ${wie} (${breite} px) ===`);
+ console.log(" Alle-Knopf getroffen:", traf);
+ console.log(" An wen:", JSON.stringify(gesehen.wen));
+ console.log(" Karte :", gesehen.titel);
+ console.log(" Fuss :", gesehen.fuss, "| Knopf:", gesehen.knopf);
+ console.log(" Hinweis:", gesehen.hinweis);
+ console.log(" Karten mit Namensreihe:", gesehen.mitReihe);
+ for (const r of gesehen.reihe) console.log(` ${r.wer} -> ${r.stand} [${r.art}]`);
+ if (konsole.length) console.log(" FEHLER IN DER KONSOLE:", konsole);
+
+ const karte = seite.locator('.vorlagenbrett__karte', { has: seite.locator('.vorlagenbrett__wer') }).first();
+ if (await karte.count()) {
+ await karte.screenshot({ path: `server/mess-alle-${wie}.png` });
+ console.log(` Bild: server/mess-alle-${wie}.png`);
+ }
+ await kontext.close();
+ }
+} finally {
+ await browser.close();
+ vorbau.close();
+ try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
+ process.exit(0);
+}
diff --git a/server/pruef-modi-katalog.mjs b/server/pruef-modi-katalog.mjs
index 0cf47a6b..e1d2a0e1 100644
--- a/server/pruef-modi-katalog.mjs
+++ b/server/pruef-modi-katalog.mjs
@@ -444,7 +444,15 @@ console.log("\n=== 4. DogFather trägt für einen Modi ein ===\n");
method: "POST", headers: jsonKopf,
body: JSON.stringify({ art: "katalog", nr, gruppe: "monat", person_id: idCreat }),
});
- ok(b.status === 404, `an einen Creator dagegen nicht (${b.status})`);
+ /* STRENGER ALS VORHER (24.09.2026). Hier stand nur `status === 404`.
+ Das war zugleich die Antwort auf „die Person gibt es nicht" -- und
+ auf dem Bildschirm stand deshalb „Das gibt es nicht mehr, lade die
+ Seite neu" zu jemandem, den es sehr wohl gibt. Ein Neuladen half
+ nie. Seit heute trennt der Server die beiden Faelle, und geprueft
+ wird das WORT, nicht nur die Nummer: Ein Zahlendreher woanders
+ koennte sonst denselben Haken setzen. */
+ ok(b.status === 400 && b.daten?.fehler === "rolle_ohne_katalog",
+ `an einen Creator dagegen nicht (${b.status} ${b.daten?.fehler || ""})`);
/* Und ohne Person gar nicht -- eine Aufgabe ohne Adressaten ist ein
Zettel. */
@@ -1205,6 +1213,165 @@ console.log("\n=== 7. Gegenprobe: kann das hier ueberhaupt scheitern? ===\n");
ok(e.status === 400, `und eine erfundene Vorlage in der Bewerbung (${e.status})`);
}
+/* ---------- 8. Wer aus dem Katalog etwas bekommen kann --------------- *
+ *
+ * ER STEHT GANZ HINTEN, OBWOHL ER INHALTLICH ZU 4 GEHOERT.
+ *
+ * Grund: Er legt Aufgaben an, und Abschnitt 6 sieht im Browser nach,
+ * wie viele Karten noch einen "Bewerben"-Knopf tragen -- eine Karte,
+ * die man schon hat, traegt keinen. Stand er vorn, fiel diese Zahl von
+ * zehn auf neun, und eine Pruefung wurde rot, an der nichts kaputt war.
+ *
+ * Die Schwelle dort (knoepfe.length >= 10) ist eine feste Zahl und
+ * damit eine Rechnung von gestern -- wer dem Modi kuenftig Aufgaben
+ * gibt, bringt sie wieder zum Kippen. Das hier ist die kleine Loesung
+ * (nicht stoeren); die grosse waere, die Zahl dort abzuleiten statt sie
+ * zu setzen. Wer das anfasst, darf diesen Abschnitt zurueckschieben.
+ */
+/* =====================================================================
+ Filipe, 24.09.2026: „wenn ich eine aufgabe an alle verteile will ich
+ dass dogfather und die rechte hand individuel von jedem sehen wer es
+ gemacht hat oder nicht."
+
+ WARUM DIESER ABSCHNITT NEU IST. „An alle" gab es seit dem Vormittag,
+ und geprueft war davon genau ein Fall: dass eine erfundene Gruppe
+ abgelehnt wird. Der Weg, der tatsaechlich benutzt wird -- an alle
+ verteilen und nachsehen, wer sie hat --, lief ungeprueft.
+
+ UND DABEI LAG ER FALSCH: Die „An wen"-Reihe im Browser zeigte sechs
+ Personen (rechte Hand, linke Hand, vier Modis), der Server nahm aber
+ nur Modis an. „An alle 6" legte vier Aufgaben an, und die beiden
+ Haende gingen leer aus -- ohne ein Wort. Das ist kein Randfall: Es
+ ist der Normalfall dieses Knopfes.
+
+ GEPRUEFT WIRD IN BEIDE RICHTUNGEN. Wer etwas bekommen soll, bekommt
+ es (200); wer nicht, bekommt eine Absage MIT GRUND (400). Eine
+ Pruefung, die nur das Gelingen kennt, kann nicht scheitern -- und
+ bestaetigt damit nichts.
+ ===================================================================== */
+console.log("\n=== 8. Wer aus dem Katalog etwas bekommen kann ===\n");
+{
+ /* VORLAGEN, DIE IN DIESEM LAUF NOCH NIEMAND HAT -- gefragt, nicht
+ getippt. Im ersten Anlauf stand hier `gruppe === "monat"`; diese
+ Gruppe gibt es gar nicht (sie stammte aus einer Absage-Pruefung,
+ in der der Name nie geprueft wurde), und `frei[0]` war undefined.
+ Eine abgeschriebene Kategorie ist dieselbe Falle wie eine feste
+ Zahl. So gefragt bleibt die Pruefung richtig, auch wenn der
+ Katalog morgen andere Gruppen hat. */
+ /* EIN EIGENER, LESENDER GRIFF. Der von ganz oben ist seit Zeile 87
+ zu -- weiter oben machen es die Bewerbungen mit `d2` schon so. Ein
+ offen gelassener Griff waere schlimmer: Die Pruefung wuerde am
+ Ende nicht mehr enden. */
+ const { DatabaseSync: DB4 } = await import("node:sqlite");
+ const d4 = new DB4(process.env.WORKSPACE_DB, { readOnly: true });
+ const belegt = new Set(d4.prepare(
+ "SELECT DISTINCT vorlage AS v FROM aufgaben WHERE vorlage IS NOT NULL")
+ .all().map((x) => x.v));
+ d4.close();
+ const frei = katalog.aufgaben
+ .map((v, i) => ({ ...v, nr: i }))
+ .filter((v) => !belegt.has(v.schluessel));
+ ok(frei.length >= 2,
+ `es gibt genug unbenutzte Vorlagen zum Pruefen (${frei.length})`);
+
+ /* --- Die linke Hand GEHOERT DAZU (neu seit 24.09.2026) ------------
+ Filipes Regel vom 22.09.: „die modis und linke hand sollen ... sich
+ fuer aufgaben bewerben koennen aber die rechte hand oder dogfather
+ muessen annehmen oder ablehnen". Also ist sie Empfaengerin. */
+ const l = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", {
+ method: "POST", headers: jsonKopf,
+ body: JSON.stringify({ art: "katalog", nr: frei[0].nr, gruppe: frei[0].gruppe, person_id: idLinke }),
+ });
+ ok(l.status === 200 && l.daten?.angelegt === 1,
+ `an die linke Hand geht es (${l.status})`);
+
+ /* --- Die rechte Hand NICHT. Sie verteilt; stuende sie in ihrer
+ eigenen Verteilliste, waere „an alle" auch „an mich selbst". */
+ const h = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", {
+ method: "POST", headers: jsonKopf,
+ body: JSON.stringify({ art: "katalog", nr: frei[0].nr, gruppe: frei[0].gruppe, person_id: idHand }),
+ });
+ ok(h.status === 400 && h.daten?.fehler === "rolle_ohne_katalog",
+ `an die rechte Hand dagegen nicht (${h.status} ${h.daten?.fehler || ""})`);
+
+ /* --- Und jetzt „an alle" ------------------------------------------
+ Im Haus dieses Laufs gibt es genau EINEN Modi und EINE linke Hand.
+ „Alle" muss also ZWEI Aufgaben anlegen -- nicht eine (dann waere
+ die linke Hand wieder vergessen) und nicht drei (dann haette die
+ rechte Hand eine bekommen). Die Zahl allein reicht aber nicht:
+ Drei falsche Leute waeren auch drei. Deshalb wird danach
+ nachgesehen, WER sie wirklich hat. */
+ const alle = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", {
+ method: "POST", headers: jsonKopf,
+ body: JSON.stringify({ art: "katalog", nr: frei[1].nr, gruppe: frei[1].gruppe, person_id: "alle" }),
+ });
+ ok(alle.status === 200 && alle.daten?.angelegt === 2,
+ `„an alle" legt bei beiden Empfaengern an (${alle.status}, ${alle.daten?.angelegt})`);
+
+ const d5 = new DB4(process.env.WORKSPACE_DB, { readOnly: true });
+ const hats = new Set(d5.prepare(
+ "SELECT verantwortlich_id AS w FROM aufgaben WHERE vorlage = ?")
+ .all(frei[1].schluessel).map((x) => Number(x.w)));
+ ok(hats.has(idModi) && hats.has(idLinke),
+ `und zwar beim Modi UND bei der linken Hand (${[...hats].join(", ")})`);
+ ok(!hats.has(idHand) && !hats.has(idCreat),
+ "und bei niemandem sonst");
+ d5.close();
+
+ /* --- "AN ALLE" FUELLT LUECKEN, ES DOPPELT NICHT -------------------
+ Filipe, 24.09.2026: „Nur die 2 Fehlenden bekommen sie."
+ Derselbe Druck ein zweites Mal: Beide haben sie schon, also darf
+ KEINE dazukommen -- und der Server muss es SAGEN, sonst sieht ein
+ Knopf, der nichts tut, wie ein Fehler aus. Am 22.09. sind aus zwei
+ Klicks 112 Aufgaben geworden, weil niemand nachgezaehlt hat. */
+ const wieder = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", {
+ method: "POST", headers: jsonKopf,
+ body: JSON.stringify({ art: "katalog", nr: frei[1].nr, gruppe: frei[1].gruppe, person_id: "alle" }),
+ });
+ const zaehle = (schluessel) => {
+ const dz = new DB4(process.env.WORKSPACE_DB, { readOnly: true });
+ const n = dz.prepare(
+ "SELECT COUNT(*) AS n FROM aufgaben WHERE vorlage = ?").get(schluessel)?.n;
+ dz.close();
+ return Number(n);
+ };
+ ok(zaehle(frei[1].schluessel) === 2,
+ `ein zweiter Druck legt nichts dazu (${zaehle(frei[1].schluessel)} statt 2)`);
+ ok(wieder.daten?.uebersprungen === 2,
+ `und er sagt, dass er zwei uebersprungen hat (${wieder.daten?.uebersprungen})`);
+
+ /* --- ABER DIE GEGENPROBE: gemeint ist gemeint ---------------------
+ Eine Sperre, die sich nicht bewusst umgehen laesst, verbietet die
+ Wochenaufgabe. Steht am Knopf "Nochmal" (weil wirklich alle sie
+ haben), schickt der Browser `nochmal: true` -- und DANN muss die
+ Zahl steigen. Ohne diese Probe koennte die Sperre auch schlicht
+ alles blockieren und saehe von aussen genauso richtig aus. */
+ const absicht = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", {
+ method: "POST", headers: jsonKopf,
+ body: JSON.stringify({
+ art: "katalog", nr: frei[1].nr, gruppe: frei[1].gruppe,
+ person_id: "alle", nochmal: true,
+ }),
+ });
+ ok(absicht.status === 200 && zaehle(frei[1].schluessel) === 4,
+ `ein gewolltes "Nochmal" legt sehr wohl an (${zaehle(frei[1].schluessel)} statt 4)`);
+
+ /* --- Und eine LUECKE wird wirklich gefuellt ------------------------
+ Der eigentliche Fall: Einer hat sie, der andere nicht. Dann darf
+ genau EINE entstehen -- nicht null (dann waere die Sperre zu
+ scharf) und nicht zwei (dann doppelt sie). `frei[0]` liegt seit
+ oben bei der linken Hand, beim Modi nicht. */
+ const luecke = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", {
+ method: "POST", headers: jsonKopf,
+ body: JSON.stringify({ art: "katalog", nr: frei[0].nr, gruppe: frei[0].gruppe, person_id: "alle" }),
+ });
+ ok(luecke.daten?.angelegt === 1 && luecke.daten?.uebersprungen === 1,
+ `eine Luecke wird gefuellt, der Rest nicht gedoppelt `
+ + `(${luecke.daten?.angelegt} neu, ${luecke.daten?.uebersprungen} uebersprungen)`);
+ ok(zaehle(frei[0].schluessel) === 2,
+ `und danach hat sie jeder genau einmal (${zaehle(frei[0].schluessel)})`);
+}
+
console.log(`\n${fehler ? "FEHLGESCHLAGEN" : "BESTANDEN"} -- `
+ `${geprueft} geprueft, ${fehler} Fehler, ${offen} nicht nachsehbar`);
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
diff --git a/server/workspace-entwicklung.js b/server/workspace-entwicklung.js
index ba0e9969..2ed87fa5 100644
--- a/server/workspace-entwicklung.js
+++ b/server/workspace-entwicklung.js
@@ -61,7 +61,7 @@
import express from "express";
import {
db, sitzungLesen, protokolliere, echteIp, fuehrtTeamDogi, ROLLEN_NAME,
- ROLLEN_SORTIERUNG, MODI_KATEGORIEN, kategorienFuer,
+ ROLLEN_SORTIERUNG, MODI_KATEGORIEN, kategorienFuer, darfKatalogBekommen,
} from "./workspace.js";
/* Die Aufgaben, die ein Neuer bekommen kann -- dieselbe Liste, aus der
auch die Aufgabenseite ihre Vorschlaege baut. Eine zweite waere die,
@@ -532,6 +532,14 @@ entwicklungRouter.get("/workspace/api/entwicklung/lage", (req, res) => {
personen: personen.map((p) => ({
id: p.id, name: p.name,
rolle_name: ROLLEN_NAME[p.rolle] ?? p.rolle,
+ /* OB SIE EINE KATALOG-VORLAGE BEKOMMEN KANN -- vom Server
+ beantwortet, nicht im Browser nachgerechnet. Der Browser
+ kennt die Rolle nur als ANZEIGENAME ("Rechte Hand"); wer
+ danach entscheidet, entscheidet nach einem Text, den man
+ aendern darf. Und eine zweite Rollenliste im Browser waere
+ genau die Doppelung, die diesen Fehler erzeugt hat -- die
+ "An wen"-Reihe zeigte sechs Leute, beliefert wurden vier. */
+ kann_katalog: darfKatalogBekommen(p.rolle),
dabei_seit_tagen: tageSeit(p.erstellt),
gesetzt: meineZahl.get(p.id) || 0,
veraltet: alteZahl.get(p.id) || 0,
diff --git a/server/workspace-vorlagen.js b/server/workspace-vorlagen.js
index d6509583..42655be7 100644
--- a/server/workspace-vorlagen.js
+++ b/server/workspace-vorlagen.js
@@ -48,6 +48,8 @@ import {
MODI_STUFEN,
entscheidetUeberAufgaben,
darfAufgabenVerteilen,
+ KATALOG_EMPFAENGER,
+ KATALOG_EMPFAENGER_SQL,
} from "./workspace.js";
/* Dieselbe Meldung wie beim anderen Bewerbungsweg -- eine Stelle,
@@ -1928,15 +1930,27 @@ vorlagenRouter.post("/workspace/api/vorlagen/uebernehmen", gleicheHerkunft,
/* Nur an jemanden, fuer den die Kategorien ueberhaupt gelten --
sonst landete eine Modi-Aufgabe samt Kategorie an einem
Creator, und die Trennung waere von innen aufgeweicht. */
+ const platzE = KATALOG_EMPFAENGER_SQL.map(() => "?").join(", ");
const zielListe = anAlle
? db().prepare(
- "SELECT id, rolle FROM personen WHERE aktiv = 1 AND rolle = 'modi' ORDER BY id").all()
+ `SELECT id, rolle FROM personen
+ WHERE aktiv = 1 AND rolle IN (${platzE}) ORDER BY id`)
+ .all(...KATALOG_EMPFAENGER_SQL)
: [db().prepare(
"SELECT id, rolle FROM personen WHERE id = ? AND aktiv = 1").get(zielId)]
.filter(Boolean);
- if (!zielListe.length || zielListe.some((z) => z.rolle !== "modi")) {
+ if (!zielListe.length) {
return res.status(404).json({ fehler: "nicht_gefunden" });
}
+ /* EINE EIGENE ANTWORT FUER „DIE ROLLE BEKOMMT HIER NICHTS".
+ Vorher lief das ueber dieselbe 404 wie „die Person gibt es
+ nicht" -- und auf dem Bildschirm stand „lade die Seite neu",
+ was an einer Person, die sehr wohl existiert, nur ratlos
+ macht. Der Unterschied ist keine Feinheit: Das eine repariert
+ ein Neuladen, das andere nie. */
+ if (zielListe.some((z) => !KATALOG_EMPFAENGER.has(z.rolle))) {
+ return res.status(400).json({ fehler: "rolle_ohne_katalog" });
+ }
/* Nur noch als Beleg, dass die Liste nicht leer ist -- gelesen
wird sie in der Schleife weiter unten. Ein ungenutzter Wert
waere Rauschen; dieser sagt etwas. */
@@ -1998,9 +2012,32 @@ vorlagenRouter.post("/workspace/api/vorlagen/uebernehmen", gleicheHerkunft,
Bei einer einzelnen Person aendert sich nichts: Die
Schleife laeuft dann genau einmal. */
+ /* ==== "AN ALLE" FUELLT LUECKEN, ES DOPPELT NICHT (24.09.2026)
+
+ Filipe, zur Frage "3 von 5 haben sie schon": „Nur die 2
+ Fehlenden bekommen sie."
+
+ GEMESSEN, NICHT GEGLAUBT. Der Kommentar zwei Absaetze
+ hoeher behauptete, die Sperre werde "bei alle pro Person neu
+ gestellt". Er beschreibt nur, WO sie steht -- aktiv war sie
+ allein bei `katalog_alle`. Zweimal "An alle" auf dieselbe
+ Karte ergab deshalb VIER Aufgaben statt zwei. Gefunden hat
+ das keine Ueberlegung, sondern eine Pruefung, die nachzaehlt.
+
+ UND DIE BEWUSSTE WIEDERHOLUNG BLEIBT MOEGLICH. Manches steht
+ jede Woche an; wer es erneut verteilen will, drueckt einen
+ Knopf, der dann auch "Nochmal" heisst -- und der Browser sagt
+ das hier ausdruecklich (`nochmal`). Nicht die Sperre
+ verschwindet, sondern es wird gesagt, dass man sie meint.
+ Ein stillschweigendes Doppeln waere das, was am 22.09. aus
+ zwei Klicks 112 Aufgaben gemacht hat.
+
+ EINE EINZELNE PERSON BLEIBT UNBERUEHRT: Dort ist ein zweiter
+ Druck ohnehin eine Entscheidung, und der Knopf sagt es. */
+ const luecken = anAlle && req.body.nochmal !== true;
const lagSchonDa = [];
for (const ziel of zielListe) {
- const liegtSchon = art === "katalog_alle"
+ const liegtSchon = (art === "katalog_alle" || luecken)
? new Set(db().prepare(
`SELECT vorlage FROM aufgaben
WHERE verantwortlich_id = ?
diff --git a/server/workspace.js b/server/workspace.js
index c9967c67..b878140e 100644
--- a/server/workspace.js
+++ b/server/workspace.js
@@ -139,6 +139,48 @@ export const WIE_RECHTE_HAND = new Set(["hand", "linke"]);
/** Hat diese Person die Team-Rechte einer Hand (rechts oder links)? */
export const istHand = (person) => !!person && WIE_RECHTE_HAND.has(person.rolle);
+/* =====================================================================
+ WER EINE KATALOG-VORLAGE BEKOMMEN KANN (24.09.2026)
+ =====================================================================
+
+ Filipe: „wenn ich eine aufgabe an alle verteile will ich dass
+ dogfather und die rechte hand individuel von jedem sehen wer es
+ gemacht hat oder nicht."
+
+ WARUM DIESE MENGE HIER STEHT UND NICHT DREIMAL VERSTREUT. Bis heute
+ gab es zwei Listen, die dasselbe meinten und es nicht taten:
+
+ * Die „An wen"-Reihe im Browser bekam ihre Leute aus
+ `KARTEN_ROLLEN` (hand, linke, modi) -- gemessen am echten Stand
+ SECHS Personen.
+ * Der Katalog-Weg im Server nahm `rolle = 'modi'` -- VIER.
+
+ Folge: „Alle" versprach sechs und belieferte vier; die beiden
+ Haende gingen leer aus, ohne ein Wort. Und wer einzeln auf „An
+ Rechte Hand" drueckte, bekam vom Server 404 und auf dem Bildschirm
+ „Das gibt es nicht mehr -- wahrscheinlich hat es jemand geloescht".
+ Eine Meldung, die in die Irre fuehrt: Die Person gibt es sehr wohl.
+
+ ES IST EINE MENGE UND KEINE ABSCHRIFT. Wer sie erweitert, erweitert
+ damit die SQL-Abfrage, die Annahme-Pruefung und die Liste im
+ Browser zugleich -- die drei koennen gar nicht mehr auseinander-
+ laufen. Genau das war der Fehler, nicht die Zahl.
+
+ WARUM MODI UND LINKE HAND. Filipes Regel vom 22.09.2026: „die modis
+ und linke hand sollen ... sich fuer aufgaben bewerben koennen aber
+ die rechte hand oder dogfather muessen annehmen oder ablehnen".
+ Also sind das die beiden Rollen, die Aufgaben BEKOMMEN -- und
+ DogFather und die rechte Hand die, die verteilen und zusehen. */
+export const KATALOG_EMPFAENGER = new Set(["modi", "linke"]);
+
+/** Kann diese Rolle eine Vorlage aus dem Katalog bekommen? */
+export const darfKatalogBekommen = (rolle) => KATALOG_EMPFAENGER.has(rolle);
+
+/** Die Rollen als SQL-Platzhalterliste -- abgeleitet, nicht getippt.
+ * Eine Abfrage, die die Namen selbst noch einmal enthielte, waere die
+ * vierte Liste und damit genau das Problem von vorn. */
+export const KATALOG_EMPFAENGER_SQL = [...KATALOG_EMPFAENGER];
+
/* DIE EINE REIHENFOLGE, IN DER ROLLEN UEBERALL ERSCHEINEN.
Sie steht hier einmal, damit keine Liste eine eigene erfindet.
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index ab44479d..65e74f4d 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`. -->
-
-
+
+
-
+