diff --git a/server/pruef-an-alle.mjs b/server/pruef-an-alle.mjs
new file mode 100644
index 00000000..58680c7e
--- /dev/null
+++ b/server/pruef-an-alle.mjs
@@ -0,0 +1,231 @@
+/* =====================================================================
+ „AN ALLE" BEI „AN WEN" (24.09.2026)
+
+ Filipe: „bei an ween fehlt noch die option alle neben den namen
+ allen."
+
+ DREI FRAGEN, und die dritte ist die, an der es schiefgehen kann:
+
+ 1. Trifft „alle" wirklich JEDEN -- oder nur den ersten?
+ 2. Bleibt der Duplikat-Schutz erhalten? Er fragt, was EINE Person
+ schon hat; bei „alle" muss er je Person neu gestellt werden.
+ Sonst bekaeme nur die erste ihre Pruefung und alle anderen
+ die Vorlage doppelt -- genau so entstanden am 22.09. aus zwei
+ Klicks 112 Aufgaben.
+ 3. Kommt jemand darueber an Leute heran, die ihn nichts angehen?
+ „alle" darf nicht mehr treffen als die Namen daneben.
+ ===================================================================== */
+import { mkdtempSync, rmSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+
+const ordner = mkdtempSync(join(tmpdir(), "ws-aa-"));
+process.env.WORKSPACE_DB = join(ordner, "workspace.db");
+
+import { notbremse } from "./helfer-notbremse.mjs";
+const { eigenerPort } = await import("./helfer-port.mjs");
+const PORT = await eigenerPort(import.meta, "die An-alle-Pruefung");
+
+process.env.PORT = `${PORT}`;
+process.env.SITE_ACCESS_SECRET = "lokaler-test";
+process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
+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 }); };
+await import("./index.js");
+notbremse(240_000, "pruef-an-alle");
+await new Promise((r) => setTimeout(r, 900));
+
+let fehler = 0, geprueft = 0;
+const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
+const melde = (t) => console.log(t);
+
+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 ks = randomBytes(32).toString("hex");
+d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
+ + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
+ .run("code_kennung_schluessel", ks, jetzt);
+function anlegen(name, rolle, code, aktiv = 1) {
+ const salt = randomBytes(16).toString("hex");
+ const hash = scryptSync(code, salt, 64,
+ { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
+ d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n,"
+ + " code_kennung, aktiv, erstellt) VALUES (?,?,?,?,?,?,?,?)")
+ .run(name, rolle, hash, salt, 32768,
+ createHmac("sha256", ks).update(code).digest("hex"), aktiv, jetzt);
+ return d.prepare("SELECT last_insert_rowid() AS id").get().id;
+}
+anlegen("Filipe", "admin", "CODE-DOGI-0001");
+const modis = [
+ anlegen("Miss", "modi", "CODE-MODI-0001"),
+ anlegen("Ghost", "modi", "CODE-MODI-0002"),
+ anlegen("Marina", "modi", "CODE-MODI-0003"),
+];
+/* EIN GESPERRTER MODI. Er darf NICHT mitbekommen -- ein Zugang, der
+ nicht hereinkommt, braucht keine Aufgabenliste. */
+const gesperrt = anlegen("Diene", "modi", "CODE-MODI-0004", 0);
+/* Und ein Creator: Er steht gar nicht in dieser Auswahl und darf
+ auch ueber „alle" nichts bekommen. */
+const creator = anlegen("Luna", "creator", "CODE-CREA-0001");
+d.close();
+
+const CREW = "crew.dogfather-universe.com";
+const { request } = await import("node:http");
+function roh(pfad, keks, koerper, methode) {
+ return new Promise((fertig, schief) => {
+ const daten = koerper ? JSON.stringify(koerper) : null;
+ const a = request({
+ host: "127.0.0.1", port: PORT, path: pfad,
+ method: methode || (daten ? "POST" : "GET"),
+ headers: {
+ Host: CREW, Origin: `https://${CREW}`,
+ ...(keks ? { Cookie: keks } : {}),
+ ...(daten ? { "Content-Type": "application/json",
+ "Content-Length": Buffer.byteLength(daten) } : {}),
+ },
+ }, (antwort) => {
+ let text = "";
+ antwort.on("data", (t) => { text += t; });
+ antwort.on("end", () => fertig({ code: antwort.statusCode, text }));
+ });
+ a.on("error", schief);
+ if (daten) a.write(daten);
+ a.end();
+ });
+}
+const json = (r) => { try { return JSON.parse(r.text); } catch { return {}; } };
+
+async function anmelden(rolle, code) {
+ const r = await new Promise((fertig, schief) => {
+ const daten = JSON.stringify({ rolle, code, alter_ok: true });
+ const a = request({
+ host: "127.0.0.1", port: PORT, path: "/workspace/api/anmelden",
+ method: "POST",
+ headers: { Host: CREW, Origin: `https://${CREW}`,
+ "Content-Type": "application/json", "Content-Length": Buffer.byteLength(daten) },
+ }, (antwort) => {
+ let text = "";
+ antwort.on("data", (t) => { text += t; });
+ antwort.on("end", () => fertig({ code: antwort.statusCode, kopf: antwort.headers }));
+ });
+ a.on("error", schief);
+ a.write(daten); a.end();
+ });
+ if (r.code !== 200) return null;
+ return [].concat(r.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; ");
+}
+
+const kDogi = await anmelden("admin", "CODE-DOGI-0001");
+const kModi = await anmelden("modi", "CODE-MODI-0001");
+ok(!!kDogi && !!kModi, "DogFather und ein Modi sind angemeldet");
+
+/* Wie viele Aufgaben hat wer? Direkt aus der Datenbank -- die Antwort
+ der Route zu glauben hiesse, sie mit sich selbst zu pruefen. */
+function zaehle(personId) {
+ const db2 = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true });
+ const { n } = db2.prepare(
+ "SELECT COUNT(*) AS n FROM aufgaben WHERE verantwortlich_id = ? AND vorlage IS NOT NULL")
+ .get(personId);
+ db2.close();
+ return n;
+}
+
+/* Welche Kategorie gibt es ueberhaupt? Aus dem Katalog, den die Seite
+ auch bekommt -- eine erfundene Gruppe waere eine Pruefung, die
+ nichts trifft. */
+const katalog = json(await roh("/workspace/api/vorlagen", kDogi));
+const ersteGruppe = (katalog.katalog?.gruppen || [])[0]?.schluessel
+ || (katalog.katalog?.aufgaben || [])[0]?.gruppe || "";
+ok(!!ersteGruppe, `es gibt eine Kategorie zum Verteilen (${ersteGruppe})`);
+
+melde("\n=== 1. alle trifft jeden ===");
+{
+ const vorher = modis.map(zaehle);
+ ok(vorher.every((x) => x === 0), `vorher hat niemand etwas (${vorher.join(", ")})`);
+
+ const a = await roh("/workspace/api/vorlagen/uebernehmen", kDogi,
+ { art: "katalog_alle", gruppe: ersteGruppe, stufe: "", person_id: "alle" });
+ ok(a.code === 200, `die Anfrage geht durch (${a.code})`);
+ const r = json(a);
+ ok((r.angelegt || 0) > 0, `es wurde etwas angelegt (${r.angelegt})`);
+
+ const nachher = modis.map(zaehle);
+ ok(nachher.every((x) => x > 0),
+ `JEDER der drei Modis hat jetzt Aufgaben (${nachher.join(", ")})`);
+ ok(new Set(nachher).size === 1,
+ "und alle gleich viele -- sonst hat die Schleife jemanden ausgelassen");
+ ok((r.angelegt || 0) === nachher[0] * modis.length,
+ `die Zahl in der Antwort passt zur Datenbank (${r.angelegt} = ${nachher[0]} x ${modis.length})`);
+}
+
+melde("\n=== 2. Der Duplikat-Schutz gilt je Person ===");
+{
+ const vorher = modis.map(zaehle);
+ const a = await roh("/workspace/api/vorlagen/uebernehmen", kDogi,
+ { art: "katalog_alle", gruppe: ersteGruppe, stufe: "", person_id: "alle" });
+ ok(a.code === 200, `ein zweiter Klick geht durch (${a.code})`);
+ const r = json(a);
+ ok((r.angelegt || 0) === 0,
+ `und legt NICHTS mehr an (${r.angelegt}) -- genau das war der Fehler vom 22.09.`);
+ /* `uebersprungen` ist eine ZAHL, kein Feld -- nachgesehen in
+ workspace-vorlagen.js (`uebersprungen: uebersprungen.length`).
+ Mein erster Versuch rief `.length` darauf auf und bekam
+ `undefined`; ein Messfehler, der wie eine fehlende Antwort
+ aussah. */
+ ok(Number(r.uebersprungen) > 0,
+ `stattdessen sagt die Antwort, wie viele schon lagen (${r.uebersprungen})`);
+ ok(Number(r.uebersprungen) === 36,
+ `und zwar alle 36 -- 12 Vorlagen mal 3 Modis (${r.uebersprungen})`);
+ const nachher = modis.map(zaehle);
+ ok(nachher.join() === vorher.join(),
+ `niemand hat etwas doppelt (${nachher.join(", ")})`);
+}
+
+melde("\n=== 3. alle trifft nicht mehr als die Namen ===");
+{
+ ok(zaehle(gesperrt) === 0,
+ `ein GESPERRTER Modi bekommt nichts (${zaehle(gesperrt)})`);
+ ok(zaehle(creator) === 0,
+ `ein Creator erst recht nicht (${zaehle(creator)})`);
+
+ /* Und wer gar nicht verteilen darf, kommt auch ueber „alle" nicht
+ weiter -- sonst waere der neue Wert ein Weg an der Rechtefrage
+ vorbei. */
+ const alsModi = await roh("/workspace/api/vorlagen/uebernehmen", kModi,
+ { art: "katalog_alle", gruppe: ersteGruppe, stufe: "", person_id: "alle" });
+ ok(alsModi.code >= 400,
+ `ein Modi kann nicht an alle verteilen (${alsModi.code})`);
+}
+
+melde("\n=== 4. Gegenproben ===");
+{
+ /* EINE EINZELNE PERSON GEHT WEITERHIN -- sonst hiesse „alle
+ funktioniert" nur, dass der alte Weg kaputt ist. */
+ const einzeln = await roh("/workspace/api/vorlagen/uebernehmen", kDogi,
+ { art: "katalog", nr: 0, gruppe: ersteGruppe, person_id: modis[0] });
+ ok(einzeln.code === 200 || einzeln.code === 400,
+ `der Weg an EINE Person antwortet noch (${einzeln.code})`);
+
+ /* Ein erfundener Wert ist kein "alle". */
+ const quatsch = await roh("/workspace/api/vorlagen/uebernehmen", kDogi,
+ { art: "katalog_alle", gruppe: ersteGruppe, stufe: "", person_id: "alle-zusammen" });
+ ok(quatsch.code >= 400,
+ `alle-zusammen ist nicht alle (${quatsch.code})`);
+
+ /* Und ohne Person weiterhin die alte Absage. */
+ const ohne = await roh("/workspace/api/vorlagen/uebernehmen", kDogi,
+ { art: "katalog_alle", gruppe: ersteGruppe, stufe: "" });
+ ok(ohne.code === 400, `ohne Person immer noch 400 (${ohne.code})`);
+}
+
+try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
+
+console.log("");
+console.log(`${geprueft} Pruefungen, ${fehler} Fehler`);
+console.log(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
+process.exit(fehler ? 1 : 0);
diff --git a/server/workspace-vorlagen.js b/server/workspace-vorlagen.js
index 320e028a..d6509583 100644
--- a/server/workspace-vorlagen.js
+++ b/server/workspace-vorlagen.js
@@ -1903,16 +1903,44 @@ vorlagenRouter.post("/workspace/api/vorlagen/uebernehmen", gleicheHerkunft,
if (!darfAufgabenVerteilen(req.person)) {
return res.status(403).json({ fehler: "erst_bewerben" });
}
- const zielId = Number(req.body.person_id) || null;
- if (!zielId) return res.status(400).json({ fehler: "Bitte zuerst eine Person wählen." });
+ /* ==== „AN ALLE" (24.09.2026) ================================
+
+ Filipe: „bei an ween fehlt noch die option alle neben den
+ namen allen."
+
+ EINE ANFRAGE, NICHT SECHS. Der Browser koennte fuer jede
+ Person einzeln anfragen -- und beim dritten von sechs
+ Aufrufen die Verbindung verlieren. Dann haette die Haelfte
+ des Teams die Aufgabe und die andere nicht, und niemand
+ saehe, wo es abgebrochen ist. Hier laeuft alles in einem
+ Durchgang.
+
+ DER DUPLIKAT-SCHUTZ GILT JE PERSON. Er steht weiter unten
+ und fragt, was diese eine Person schon hat -- bei „alle"
+ wird er deshalb pro Person neu gestellt. Wer die Vorlage
+ schon liegen hat, bekommt sie nicht doppelt; genau so
+ entstanden am 22.09. die 112 Aufgaben aus zwei Klicks. */
+ const anAlle = String(req.body.person_id) === "alle";
+ const zielId = anAlle ? null : (Number(req.body.person_id) || null);
+ if (!anAlle && !zielId) {
+ return res.status(400).json({ fehler: "Bitte zuerst eine Person wählen." });
+ }
/* 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 zielPerson = db().prepare(
- "SELECT id, rolle FROM personen WHERE id = ? AND aktiv = 1").get(zielId);
- if (!zielPerson || zielPerson.rolle !== "modi") {
+ const zielListe = anAlle
+ ? db().prepare(
+ "SELECT id, rolle FROM personen WHERE aktiv = 1 AND rolle = 'modi' ORDER BY id").all()
+ : [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")) {
return res.status(404).json({ fehler: "nicht_gefunden" });
}
+ /* 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. */
+ if (!zielListe[0]) return res.status(404).json({ fehler: "nicht_gefunden" });
/* AUS DEM KATALOG, DEN DIE SEITE AUCH BEKOMMEN HAT (16.09.2026).
@@ -1961,19 +1989,30 @@ vorlagenRouter.post("/workspace/api/vorlagen/uebernehmen", gleicheHerkunft,
Dieselbe Sperre steht seit dem 10.09. im Termin-Zweig weiter
unten. Dass sie hier fehlte, ist nicht aufgefallen, weil
niemand zweimal drueckt -- bis es jemand tat. */
- const liegtSchon = art === "katalog_alle"
- ? new Set(db().prepare(
- `SELECT vorlage FROM aufgaben
- WHERE verantwortlich_id = ?
- AND vorlage IS NOT NULL
- AND status NOT IN ('erledigt', 'abgebrochen')`)
- .all(zielPerson.id).map((z) => z.vorlage))
- : new Set();
+ /* JE PERSON EINMAL (24.09.2026, fuer „An alle").
+
+ Die Duplikatsperre fragt, was DIESE Person schon liegen
+ hat -- deshalb steht sie INNERHALB der Schleife. Waere sie
+ davor, bekaeme bei „alle" nur die erste Person ihre
+ Pruefung, und alle anderen haetten die Vorlage doppelt.
+
+ Bei einer einzelnen Person aendert sich nichts: Die
+ Schleife laeuft dann genau einmal. */
const lagSchonDa = [];
- for (const v of auswahl) {
- if (liegtSchon.has(v.schluessel)) { lagSchonDa.push(v.titel); continue; }
- katalogAufgabeAnlegen(v, zielPerson.id, req.person.id);
- angelegt.push(v.titel);
+ for (const ziel of zielListe) {
+ const liegtSchon = art === "katalog_alle"
+ ? new Set(db().prepare(
+ `SELECT vorlage FROM aufgaben
+ WHERE verantwortlich_id = ?
+ AND vorlage IS NOT NULL
+ AND status NOT IN ('erledigt', 'abgebrochen')`)
+ .all(ziel.id).map((z) => z.vorlage))
+ : new Set();
+ for (const v of auswahl) {
+ if (liegtSchon.has(v.schluessel)) { lagSchonDa.push(v.titel); continue; }
+ katalogAufgabeAnlegen(v, ziel.id, req.person.id);
+ angelegt.push(v.titel);
+ }
}
/* DIE ZAHL GEHOERT IN DIE ANTWORT, nicht nur in die Datenbank.
Ein Knopf, der "12 uebernehmen" heisst und dann schweigend
diff --git a/server/workspace.js b/server/workspace.js
index 28f7108a..16f4c144 100644
--- a/server/workspace.js
+++ b/server/workspace.js
@@ -6268,6 +6268,23 @@ workspaceRouter.get("/workspace/api/ich", (req, res) => {
weiterarbeiten. Fuer DogFather und die rechte Hand ist sie
nicht leer, fuer alle anderen schon. */
darf_zugaenge_verwalten: darfAnlegen(person).length > 0,
+ /* IN WELCHEM HAUS BIN ICH GERADE? (24.09.2026)
+
+ Filipe: „in dieser app gibt es keinen und wird es niemals einen
+ anderen creator geben wie mich."
+
+ Die Aufgabenseite beschriftete eine Filterreihe mit „Alle
+ Creator" -- und auf der Team-Dogi-Adresse standen darunter
+ Modis. Es gibt dort keine Creator, und es wird nie welche
+ geben. Die Oberflaeche konnte das bisher nicht wissen: `haus`
+ steht an der Sitzung, wurde aber nie mitgeschickt.
+
+ ABGELEITET, NICHT GERATEN. Eine Rollenliste im Browser
+ („hand, linke, modi heisst crew") waere die naechste zweite
+ Wahrheit -- und sie waere falsch fuer DogFather, der in beiden
+ Haeusern arbeitet. Hier entscheidet dieselbe Angabe, die auch
+ `hausBedingung` und `bereicheFuer` benutzen. */
+ haus: person?.haus === "crew" ? "crew" : "agentur",
/* UND LOESCHEN -- getrennt, weil es das Einzige ist, was sich nicht
zuruecknehmen laesst. Heute dieselbe Antwort; sollte Filipe es
spaeter wieder einschraenken, ist hier die Stelle. */
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index ff5ee714..c44395e4 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`. -->
-
-
+
+
-
+