Bei "An alle" sieht man jetzt, wer sie hat und wer nicht

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."

Vier Fehler, die zusammenhingen -- alle gemessen, keiner geraten:

1. "Alle" waehlen, "An alle" druecken, nichts passiert. Die Zeile
   verglich verantwortlich_id !== "alle"; niemand heisst so, also wurde
   jede Aufgabe uebersprungen und die Liste blieb leer. Die Aufgaben
   entstanden, man sah es nur nicht.

2. Der Vermerk an der Karte haette bei "alle" den Stand EINER fremden
   Person gezeigt -- welcher, haengt von der Reihenfolge der Daten ab.
   Jetzt steht dort, wie weit es ist, und darunter namentlich, wer sie
   hat: Offen / Erledigt / ueberfaellig / hat sie nicht. Das Wort steht
   immer dabei, die Farbe ist nur die Abkuerzung.

3. Die "An wen"-Reihe zeigte SECHS Personen, der Server belieferte VIER.
   Rechte und linke Hand gingen leer aus, ohne ein Wort; einzeln
   angeschrieben kam "Das gibt es nicht mehr, lade die Seite neu" zu
   jemandem, den es sehr wohl gibt. Empfaenger sind jetzt Modis UND
   linke Hand (Filipes Regel vom 22.09.), und die Menge steht EINMAL in
   workspace.js -- SQL-Abfrage, Annahme und Browserliste leiten sich
   daraus ab und koennen nicht mehr auseinanderlaufen.

4. Zweimal "An alle" legte alles doppelt an. Der Kommentar im Server
   behauptete das Gegenteil; aktiv war die Sperre nur beim Massenknopf.
   "An alle" fuellt jetzt Luecken. Die bewusste Wiederholung bleibt:
   Steht am Knopf "Nochmal" (weil wirklich alle sie haben), sagt der
   Browser das ausdruecklich, und dann legt der Server neu an.

Geprueft: pruef-modi-katalog 144 statt 133, 0 Fehler -- elf neue
Pruefungen fuer Empfaenger, Luecken und die Gegenprobe, dass ein
gewolltes "Nochmal" sehr wohl anlegt. Dazu mess-alle-einzelsicht.mjs
(eigene Wegwerf-Datenbank, nie die echte) mit Bildern bei 412 und
1280 px.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-24 17:26:35 +02:00
co-authored by Claude Opus 5
parent 67b5e00150
commit 40b48e89f1
50 changed files with 1381 additions and 650 deletions
+246
View File
@@ -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);
}
+168 -1
View File
@@ -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 */ }
+9 -1
View File
@@ -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,
+40 -3
View File
@@ -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 = ?
+42
View File
@@ -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.