Beim Nachsehen zu VanVans Meldung #6 aufgefallen, nicht beim Bauen: Der Knopf haengt an ZWEI Kartenarten -- an den Creator-Vorlagen (vorlagenbrett.js:459) und am Team-Katalog (:1220). Im Browser gemessen war nur die erste. UND DIE ZWEITE IST DIE, DIE VANVAN BENUTZT. Die Creator-Vorlagen stehen auf „Aufgaben" und richten sich an Betreuer; der Katalog steht auf „Eure Aufgaben", und dort verteilt die rechte Hand an die Modis. Genau das war ihre Meldung. Dass man diese beiden Bretter verwechselt, ist in diesem Haus schon passiert: In pruef-bewerbung-aufgaben steht es seit dem 30.09. als Messfehler notiert, „der wie ein Befund aussieht". Diesmal waere es andersherum gewesen -- ein gruener Haken ueber einem Weg, den niemand geprueft hat. GEMESSEN HAT ES NICHTS KAPUTTES GEFUNDEN: Der Katalogzweig funktioniert. Aber er war unbewiesen, und das ist derselbe Zustand wie „ungeprueft" -- nur mit besserem Gefuehl. DIE GEGENPROBE ZUM ANDEREN BRETT STECKT JETZT DRIN. Ein Creator bekommt den „dauerhaft"-Haken NICHT (geprueft in pruef-aufgaben-vorlagen), die Leitung MUSS ihn bekommen (geprueft hier). Zwei Pruefungen, die denselben Unterschied von beiden Seiten messen -- erst dann ist es eine Regel und nicht ein Zufall. GEPRUEFT -- pruef-modi-katalog 150 -> 159 ok jede Katalogkarte hat einen „Anpassen …"-Knopf (12 von 12) das Fenster hat Frist und Anmerkung und die Leitung bekommt den „dauerhaft"-Haken die Frist der Vorlage steht als Vorgabe drin (2026-10-05, fruehestens 2026-10-03) mit Haken wird die Frist gesperrt die angepasste Aufgabe liegt in ihrer Liste (3 -> 4) sie ist wirklich dauerhaft · und hat keine Frist die Anmerkung steht als Notiz dabei Die letzten drei fragen die DATENBANK, nicht den Bildschirm. Was auf dem Brett steht, ist eine Darstellung; was in der Aufgabe steht, gilt. Co-Authored-By: Claude Opus 5 <[email protected]>
1562 lines
79 KiB
JavaScript
1562 lines
79 KiB
JavaScript
/* =====================================================================
|
|
DER AUFGABENKATALOG AUS TEIL 2 DES ANFORDERUNGSDOKUMENTS
|
|
|
|
Kapitel 14: "Alle Aufgaben dieses Katalogs koennen 1:1 als Vorlagen in
|
|
die App importiert werden -- inklusive Kategorie, empfohlener Rolle
|
|
und Frequenz. So ist das Aufgaben-Board ab dem ersten Tag vollstaendig
|
|
befuellt, statt leer zu starten."
|
|
|
|
GEPRUEFT WIRD NICHT NUR, DASS ER DA IST. Ein Katalog, den man ansehen,
|
|
aber nicht uebernehmen kann, ist eine Speisekarte ohne Kueche. Und ein
|
|
uebernommener Eintrag, bei dem die Kategorie unterwegs verlorengeht,
|
|
ist die Haelfte wert -- dann sortiert man 60-mal von Hand ein, was im
|
|
Dokument bereits danebensteht.
|
|
|
|
DAZU DIE VERBORGENHEIT: Wer den Katalog nicht bekommt, darf auch nicht
|
|
erfahren, dass es ihn gibt. Eine Absage muss deshalb wortgleich mit
|
|
der fuer eine erfundene Art sein.
|
|
===================================================================== */
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-kat2-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
/* DER PORT MUSS MEINER SEIN (nachgeruestet 19.09.2026).
|
|
Ohne diese Zeile startet der eigene Server bei belegtem Port
|
|
STILL nicht -- und alles Folgende misst gegen einen fremden
|
|
Stand. Das ist der dritte Ausgang: belegt ist etwas anderes
|
|
als kaputt. */
|
|
const PORT = await eigenerPort(import.meta, "pruef-modi-katalog");
|
|
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 }); };
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
import { ersatzschreibungen } from "./helfer-deutsch.mjs";
|
|
import { fileURLToPath } from "node:url";
|
|
import { eigenerPort } from "./helfer-port.mjs";
|
|
await import("./index.js");
|
|
notbremse(180_000, "pruef-modi-katalog");
|
|
await new Promise((r) => setTimeout(r, 700));
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
|
|
let fehler = 0;
|
|
let geprueft = 0;
|
|
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
let offen = 0;
|
|
const unklar = (t) => { offen++; console.log(" -- " + t); };
|
|
|
|
/* ---------- Personen ------------------------------------------------- */
|
|
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 schluessel = randomBytes(32).toString("hex");
|
|
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
|
|
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
|
.run("code_kennung_schluessel", schluessel, jetzt);
|
|
|
|
function anlegen(name, rolle, code) {
|
|
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 (?,?,?,?,?,?,1,?)")
|
|
.run(name, rolle, hash, salt, 32768,
|
|
createHmac("sha256", schluessel).update(code).digest("hex"), jetzt);
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
}
|
|
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
|
const idMan = anlegen("Schulle", "manager", "CODE-MAN-0001");
|
|
const idCreat = anlegen("Tili", "creator", "CODE-ANNA-0001");
|
|
const idModi = anlegen("Marina", "modi", "CODE-MODI-0001");
|
|
/* DIE BEIDEN HAENDE (23.09.2026). Ohne sie laesst sich die Grenze, die
|
|
Filipe gezogen hat, gar nicht messen: "nur dogfather und die rechte
|
|
hand sollen annehmen oder ablehnen koennen". Die linke Hand darf
|
|
verteilen -- und genau deshalb ist sie die schaerfste Gegenprobe:
|
|
Wer nur "darf verteilen" prueft, laesst sie mitentscheiden. */
|
|
const idHand = anlegen("Rieke", "hand", "CODE-HAND-0001");
|
|
const idLinke = anlegen("Funny", "linke", "CODE-LINK-0001");
|
|
void idHand; void idLinke;
|
|
d.close();
|
|
void idDogi;
|
|
void idMan;
|
|
|
|
async function anmelden(rolle, code) {
|
|
const a = await fetch(BASIS + "/workspace/api/anmelden", {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ rolle, code }),
|
|
});
|
|
const keks = (a.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; ");
|
|
return { status: a.status, keks };
|
|
}
|
|
async function hole(keks, pfad, aufruf = {}) {
|
|
const a = await fetch(BASIS + pfad, {
|
|
...aufruf, headers: { ...(aufruf.headers || {}), cookie: keks },
|
|
});
|
|
return { status: a.status, daten: await a.json().catch(() => null) };
|
|
}
|
|
const jsonKopf = { "Content-Type": "application/json" };
|
|
|
|
const modiAn = await anmelden("modi", "CODE-MODI-0001");
|
|
const dogiAn = await anmelden("admin", "CODE-DOGI-0001");
|
|
const manAn = await anmelden("manager", "CODE-MAN-0001");
|
|
const creatAn = await anmelden("creator", "CODE-ANNA-0001");
|
|
/* Ueber den stillen Zugang -- welche Kachel man anfasst, ist
|
|
gleichgueltig, der Code allein entscheidet (siehe die Anmelderoute). */
|
|
const handAn = await anmelden("hand", "CODE-HAND-0001");
|
|
const linkeAn = await anmelden("linke", "CODE-LINK-0001");
|
|
|
|
/* ---------- 1. Ist er vollstaendig da? ------------------------------- */
|
|
console.log("\n=== 1. Der Katalog, wie ihn das Dokument beschreibt ===\n");
|
|
|
|
let katalog = null;
|
|
{
|
|
const a = await hole(modiAn.keks, "/workspace/api/vorlagen");
|
|
katalog = a.daten?.katalog || null;
|
|
/* SEIT DEM 16.09.2026 NACH KATEGORIE UND STUFE.
|
|
|
|
Vorher standen hier neun PHASEN mit 60 Eintraegen -- und
|
|
nachgezaehlt gehoerten 39 davon DogFather und der rechten Hand.
|
|
Es war ein Aufbauplan, kein Arbeitskatalog. Der Aufbauplan gibt es
|
|
weiterhin, unter `aufbauplan`; hier steht jetzt, was ein Mensch im
|
|
Team TUT.
|
|
|
|
ZAHLEN FEST HINGESCHRIEBEN, wie vorher: Eine Zahl, die sich selbst
|
|
nachzaehlt, merkt nicht, wenn etwas herausfaellt. */
|
|
ok(katalog && katalog.gruppen.length === 14,
|
|
`${katalog?.gruppen?.length ?? 0} Kategorien (erwartet 14)`);
|
|
ok(katalog && katalog.stufen.length === 3,
|
|
`${katalog?.stufen?.length ?? 0} Stufen (erwartet 3)`);
|
|
ok(katalog && katalog.aufgaben.length === 101,
|
|
`${katalog?.aufgaben?.length ?? 0} Aufgaben (erwartet 101)`);
|
|
|
|
/* JEDE KATEGORIE MUSS BELEGT SEIN. Vorher hatten vier von vierzehn
|
|
eine Aufgabe oder keine (Events 1, Wachstum 1, Branding 1,
|
|
Sonstiges 0) -- eine Kategorie ohne Inhalt ist ein leeres
|
|
Versprechen im Auswahlfeld. */
|
|
const je = {};
|
|
for (const v of katalog?.aufgaben || []) je[v.gruppe] = (je[v.gruppe] || 0) + 1;
|
|
const leer = (katalog?.gruppen || []).filter((g) => !je[g.schluessel]);
|
|
ok(leer.length === 0,
|
|
`jede Kategorie hat Aufgaben${leer.length ? ": leer sind " + leer.map((g) => g.name).join(", ") : ""}`);
|
|
ok(Math.min(...Object.values(je)) >= 3,
|
|
`auch die kleinste hat mindestens drei (${Math.min(...Object.values(je))})`);
|
|
|
|
/* UND JEDE STUFE. Eine Stufe ohne Aufgaben waere ein Knopf, der eine
|
|
leere Liste oeffnet. */
|
|
const jeStufe = {};
|
|
for (const v of katalog?.aufgaben || []) jeStufe[v.stufe] = (jeStufe[v.stufe] || 0) + 1;
|
|
ok((katalog?.stufen || []).every((s) => jeStufe[s.schluessel] > 0),
|
|
`jede Stufe ist belegt (${Object.entries(jeStufe).map(([k, n]) => `${k}:${n}`).join(" ")})`);
|
|
|
|
ok((katalog?.aufgaben || []).every((v) => v.titel && v.text && v.gruppe && v.stufe && v.tage),
|
|
"jede Aufgabe hat Titel, Text, Kategorie, Stufe und Frist");
|
|
/* DER TEXT SAGT WARUM, nicht was. Ein Text, der kuerzer ist als der
|
|
Titel, ist eine Wiederholung -- und die haette man sich sparen
|
|
koennen. */
|
|
const duenn = (katalog?.aufgaben || []).filter((v) => v.text.length < v.titel.length + 20);
|
|
ok(duenn.length === 0,
|
|
`jeder Text begruendet${duenn.length ? ": zu duenn bei " + duenn[0].schluessel : ""}`);
|
|
ok(new Set((katalog?.aufgaben || []).map((v) => v.schluessel)).size
|
|
=== (katalog?.aufgaben || []).length, "keine Kennung doppelt");
|
|
|
|
/* DER AUFBAUPLAN IST NICHT VERLORENGEGANGEN. */
|
|
const plan = a.daten?.aufbauplan;
|
|
ok(plan && plan.aufgaben.length === 60 && plan.gruppen.length === 9,
|
|
`der Aufbauplan steht weiter zur Verfuegung (${plan?.aufgaben?.length ?? 0} in `
|
|
+ `${plan?.gruppen?.length ?? 0} Etappen)`);
|
|
}
|
|
|
|
/* Jede Kategorie im Katalog muss es auch wirklich geben -- sonst nimmt
|
|
der Server sie beim Uebernehmen an und lehnt sie beim Aendern ab. */
|
|
{
|
|
const a = await hole(modiAn.keks, "/workspace/api/aufgaben");
|
|
const gueltig = new Set((a.daten?.kategorien || []).map((k) => k.wert));
|
|
/* Die Kategorie heisst im ausgelieferten Katalog `gruppe` -- sie ist
|
|
zugleich die Gruppe, nach der die Oberflaeche sortiert. Ein
|
|
zweiter Name fuer dasselbe waere die Gelegenheit, dass beide
|
|
auseinanderlaufen. */
|
|
const unbekannt = [...new Set((katalog?.aufgaben || []).map((v) => v.gruppe))]
|
|
.filter((k) => !gueltig.has(k));
|
|
ok(gueltig.size > 0 && unbekannt.length === 0,
|
|
unbekannt.length
|
|
? `unbekannte Kategorie(n) im Katalog: ${unbekannt.join(", ")}`
|
|
: `alle ${gueltig.size} Kategorien passen zum Katalog`);
|
|
}
|
|
|
|
/* ---------- 2. Wer bekommt ihn nicht? -------------------------------- */
|
|
console.log("\n=== 2. Und wer ihn nicht bekommt ===\n");
|
|
for (const [name, an] of [["Manager", manAn], ["Creator", creatAn]]) {
|
|
const a = await hole(an.keks, "/workspace/api/vorlagen");
|
|
ok(a.status === 200 && a.daten?.katalog === null,
|
|
`${name}: kein Katalog (${JSON.stringify(a.daten?.katalog)})`);
|
|
/* Und der ANDERE Katalog ist bei ihnen sehr wohl da -- sonst waere
|
|
oben auch gruen, wenn die ganze Antwort leer kaeme. */
|
|
ok(a.daten?.aufgaben && Object.keys(a.daten.aufgaben).length > 0,
|
|
`${name}: der Creator-Katalog dagegen schon`);
|
|
}
|
|
{
|
|
const a = await hole(modiAn.keks, "/workspace/api/vorlagen");
|
|
ok(a.daten?.aufgaben === null,
|
|
"und der Modi bekommt umgekehrt den Creator-Katalog nicht");
|
|
}
|
|
|
|
/* ---------- 3. Ein Modi kann sich nur BEWERBEN ------------------------
|
|
|
|
Filipe, 23.09.2026: "die modis sollen bei all diesen voschlaegen
|
|
auch nur bewerben koennen. die aufgaben aus der vorlage, da sollen
|
|
die modis sich nur bewerben koennen und nur dogfather und die rechte
|
|
hand sollen annehmen oder ablehnen koennen, mit einem text als
|
|
notiz."
|
|
|
|
BIS HEUTE STAND HIER DAS GEGENTEIL: "uebernommen (200)", "und er
|
|
selbst ist verantwortlich". Diese Pruefung ist beim Umbau ROT
|
|
geworden -- richtig so, sie hat die Aenderung bemerkt.
|
|
|
|
DREI DINGE WERDEN GEMESSEN, und das dritte ist das, was Filipe
|
|
ausdruecklich wollte:
|
|
1. Der Modi kommt an die alte Tuer nicht mehr heran.
|
|
2. Die Bewerbung kommt an -- und legt NOCH KEINE Aufgabe an.
|
|
3. Erst die Zusage macht daraus Arbeit, mit Notiz daran.
|
|
--------------------------------------------------------------------- */
|
|
console.log("\n=== 3. Ein Modi kann sich nur bewerben ===\n");
|
|
|
|
let bewerbungsNr = null, bewerbungsVorlage = null;
|
|
{
|
|
const nr = katalog.aufgaben.findIndex((v) => v.gruppe === "chat");
|
|
const vorlage = katalog.aufgaben[nr];
|
|
bewerbungsVorlage = vorlage;
|
|
|
|
/* ---- 1. Die alte Tuer ist zu -------------------------------------- */
|
|
const alt = await hole(modiAn.keks, "/workspace/api/vorlagen/uebernehmen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ art: "katalog", nr, gruppe: "chat" }),
|
|
});
|
|
ok(alt.status === 403, `selbst uebernehmen geht nicht mehr (${alt.status})`);
|
|
/* DER SATZ IST EIN WEGWEISER, KEINE ABSAGE. Wer "Das darfst du
|
|
nicht" liest, haelt sich fuer ausgeschlossen -- es gibt aber einen
|
|
Weg, er fuehrt nur ueber jemanden. */
|
|
ok(alt.daten?.fehler === "erst_bewerben",
|
|
` und sagt, was stattdessen geht (${alt.daten?.fehler})`);
|
|
|
|
const keine = await hole(modiAn.keks, "/workspace/api/aufgaben");
|
|
const gibtsNicht = (keine.daten?.aufgaben || [])
|
|
.some((x) => x.vorlage === vorlage.schluessel);
|
|
ok(!gibtsNicht, " und es liegt nichts auf seinem Brett");
|
|
|
|
/* ---- 2. Die Bewerbung ---------------------------------------------- */
|
|
const b = await hole(modiAn.keks, "/workspace/api/vorlagen/bewerben", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ vorlage: vorlage.schluessel, text: "ich habe Freitag Zeit" }),
|
|
});
|
|
ok(b.status === 201, `die Bewerbung kommt an (${b.status})`);
|
|
|
|
/* DAS IST DER KERN DER BAUWEISE: Bis jemand ja sagt, entsteht keine
|
|
Arbeit. Waere die Aufgabe schon da, stuenden nach zwoelf Absagen
|
|
zwoelf herrenlose Aufgaben auf dem Brett -- und das Ablehnen
|
|
muesste Aufgaben LOESCHEN, um das wieder einzufangen. */
|
|
const nochNicht = await hole(modiAn.keks, "/workspace/api/aufgaben");
|
|
ok(!(nochNicht.daten?.aufgaben || []).some((x) => x.vorlage === vorlage.schluessel),
|
|
" und legt noch KEINE Aufgabe an — das tut erst die Zusage");
|
|
|
|
const meine = await hole(modiAn.keks, "/workspace/api/vorlagen");
|
|
const meineB = (meine.daten?.bewerbungen || [])
|
|
.find((x) => x.vorlage === vorlage.schluessel);
|
|
ok(!!meineB, "er sieht seine eigene Bewerbung");
|
|
ok(meineB?.selbst === true && meineB?.text === "ich habe Freitag Zeit",
|
|
` samt seinem Wort dazu ("${meineB?.text}")`);
|
|
ok(meine.daten?.darf_entscheiden === false,
|
|
"und der Server sagt ihm, dass er nicht entscheidet");
|
|
bewerbungsNr = meineB?.id ?? null;
|
|
|
|
/* Zweimal dieselbe Bitte ist eine Bitte. */
|
|
const doppelt = await hole(modiAn.keks, "/workspace/api/vorlagen/bewerben", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ vorlage: vorlage.schluessel }),
|
|
});
|
|
ok(doppelt.status === 409 && doppelt.daten?.fehler === "schon_beworben",
|
|
`zweimal dieselbe Bewerbung wird abgewiesen (${doppelt.status})`);
|
|
}
|
|
|
|
/* ---------- 3b. Wer entscheidet -- und wer nicht ----------------------
|
|
|
|
DIE LINKE HAND IST HIER DIE SCHAERFSTE PROBE. Sie darf verteilen und
|
|
gehoert sonst ueberall dazu; Filipe hat bei der Entscheidung aber
|
|
genau zwei genannt. Wer statt "darf entscheiden" nur "darf
|
|
verteilen" prueft, laesst sie durch -- und niemand merkt es, weil
|
|
alles funktioniert.
|
|
--------------------------------------------------------------------- */
|
|
console.log("\n=== 3b. Wer über die Bewerbung entscheidet ===\n");
|
|
{
|
|
ok(Number.isInteger(bewerbungsNr), `die Bewerbung hat eine Nummer (${bewerbungsNr})`);
|
|
|
|
const alsLinke = await hole(linkeAn.keks, "/workspace/api/vorlagen");
|
|
ok(alsLinke.daten?.darf_verteilen === true,
|
|
"die linke Hand darf verteilen (sie bekommt den Übernehmen-Weg)");
|
|
ok(alsLinke.daten?.darf_entscheiden === false,
|
|
" aber NICHT entscheiden — woertlich nach Filipes Aufzaehlung");
|
|
const versuch = await hole(linkeAn.keks, "/workspace/api/vorlagen/bewerbung/annehmen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ id: bewerbungsNr, kommentar: "mach ich" }),
|
|
});
|
|
ok(versuch.status === 403,
|
|
` und am Knopf vorbei geht es auch nicht (${versuch.status})`);
|
|
|
|
/* GEGENPROBE: Der Modi selbst erst recht nicht -- sonst naehme er
|
|
seine eigene Bewerbung an. */
|
|
const selbst = await hole(modiAn.keks, "/workspace/api/vorlagen/bewerbung/annehmen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ id: bewerbungsNr }),
|
|
});
|
|
ok(selbst.status === 403, `und der Bewerber selbst schon gar nicht (${selbst.status})`);
|
|
|
|
/* Die rechte Hand sieht sie -- mit Namen, denn sie entscheidet ueber
|
|
einen Menschen und nicht ueber einen Listeneintrag. */
|
|
const alsHand = await hole(handAn.keks, "/workspace/api/vorlagen");
|
|
const sieht = (alsHand.daten?.bewerbungen || []).find((x) => x.id === bewerbungsNr);
|
|
ok(alsHand.daten?.darf_entscheiden === true, "die rechte Hand entscheidet");
|
|
ok(!!sieht && sieht.person_name === "Marina",
|
|
` und sieht, wer sich beworben hat (${sieht?.person_name})`);
|
|
ok(sieht?.selbst === false, " als fremde Bewerbung, nicht als eigene");
|
|
}
|
|
|
|
/* ---------- 3c. Die Zusage macht daraus Arbeit ------------------------ */
|
|
console.log("\n=== 3c. Annehmen und ablehnen — mit Notiz ===\n");
|
|
{
|
|
const a = await hole(handAn.keks, "/workspace/api/vorlagen/bewerbung/annehmen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ id: bewerbungsNr, kommentar: "danke dir, mach das gern" }),
|
|
});
|
|
ok(a.status === 200 && a.daten?.zustand === "angenommen",
|
|
`die rechte Hand sagt zu (${a.status})`);
|
|
ok(Number.isInteger(a.daten?.aufgabe_id),
|
|
` und JETZT entsteht die Aufgabe (#${a.daten?.aufgabe_id})`);
|
|
|
|
const brett = await hole(modiAn.keks, "/workspace/api/aufgaben");
|
|
const neu = (brett.daten?.aufgaben || [])
|
|
.find((x) => x.vorlage === bewerbungsVorlage.schluessel);
|
|
ok(!!neu, "sie liegt auf seinem Brett");
|
|
/* SIE MUSS AUSSEHEN WIE EINE VERTEILTE. Ein zweiter Weg zum Anlegen
|
|
waere ein zweiter Satz Regeln -- deshalb geht die Zusage durch
|
|
dieselbe Funktion wie das Uebernehmen. */
|
|
ok(neu?.kategorie === bewerbungsVorlage.gruppe,
|
|
`mit ihrer Kategorie (${neu?.kategorie} / erwartet ${bewerbungsVorlage.gruppe})`);
|
|
ok(!!neu?.frist, `und mit Frist (${neu?.frist})`);
|
|
ok(neu?.verantwortlich_id === idModi, "und der Bewerber ist verantwortlich");
|
|
|
|
/* Die Bewerbung ist damit vom Tisch -- sonst entschiede gleich noch
|
|
jemand ein zweites Mal darueber. */
|
|
const danach = await hole(handAn.keks, "/workspace/api/vorlagen");
|
|
ok(!(danach.daten?.bewerbungen || []).some((x) => x.id === bewerbungsNr),
|
|
"und sie wartet nicht mehr auf eine Antwort");
|
|
const nochmal = await hole(handAn.keks, "/workspace/api/vorlagen/bewerbung/annehmen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ id: bewerbungsNr }),
|
|
});
|
|
ok(nochmal.status === 409 && nochmal.daten?.fehler === "keine_bewerbung",
|
|
`zweimal entscheiden geht nicht (${nochmal.status})`);
|
|
|
|
/* ---- UND DIE ABSAGE, mit Notiz ------------------------------------ */
|
|
const nr2 = katalog.aufgaben.findIndex((v) => v.gruppe === "events");
|
|
const v2 = katalog.aufgaben[nr2];
|
|
await hole(modiAn.keks, "/workspace/api/vorlagen/bewerben", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ vorlage: v2.schluessel }),
|
|
});
|
|
const offen2 = (await hole(handAn.keks, "/workspace/api/vorlagen"))
|
|
.daten?.bewerbungen?.find((x) => x.vorlage === v2.schluessel);
|
|
ok(!!offen2, "eine zweite Bewerbung liegt vor");
|
|
ok(offen2?.text === null,
|
|
" auch ohne Wort dazu — der Text ist freiwillig, wie bei den Aufgaben");
|
|
|
|
const ab = await hole(handAn.keks, "/workspace/api/vorlagen/bewerbung/ablehnen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ id: offen2.id, kommentar: "das macht diesmal jemand anders" }),
|
|
});
|
|
ok(ab.status === 200 && ab.daten?.zustand === "abgelehnt",
|
|
`und wird abgelehnt (${ab.status})`);
|
|
ok(ab.daten?.aufgabe_id === null,
|
|
" ohne dass Arbeit entsteht, die niemand bestellt hat");
|
|
|
|
/* DIE NOTIZ BLEIBT STEHEN -- das ist der Teil, den Filipe
|
|
ausdruecklich genannt hat ("mit einem text als notiz"). Gelesen
|
|
wird direkt aus der Datenbank: Ein Feld, das der Server zwar
|
|
annimmt, aber nirgends speichert, saehe von aussen genauso aus. */
|
|
const { DatabaseSync: DB2 } = await import("node:sqlite");
|
|
const d2 = new DB2(process.env.WORKSPACE_DB, { readOnly: true });
|
|
const zeilen = d2.prepare(`SELECT zustand, entscheid_text, entschieden_von, aufgabe_id
|
|
FROM vorlagen_bewerbungen ORDER BY id`).all();
|
|
d2.close();
|
|
const zu = zeilen.find((z) => z.zustand === "angenommen");
|
|
const weg = zeilen.find((z) => z.zustand === "abgelehnt");
|
|
ok(zu?.entscheid_text === "danke dir, mach das gern",
|
|
`die Notiz der Zusage steht in der Datenbank ("${zu?.entscheid_text}")`);
|
|
ok(weg?.entscheid_text === "das macht diesmal jemand anders",
|
|
`und die der Absage auch ("${weg?.entscheid_text}")`);
|
|
ok(zu?.entschieden_von === idHand && weg?.entschieden_von === idHand,
|
|
"bei beiden steht, WER entschieden hat");
|
|
ok(Number.isInteger(zu?.aufgabe_id) && weg?.aufgabe_id === null,
|
|
"und nur an der Zusage haengt eine Aufgabe");
|
|
|
|
/* ---- Nach einer Absage darf man es wieder versuchen ---------------- */
|
|
const nochmalBewerben = await hole(modiAn.keks, "/workspace/api/vorlagen/bewerben", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ vorlage: v2.schluessel, text: "doch noch Zeit" }),
|
|
});
|
|
ok(nochmalBewerben.status === 201,
|
|
`nach einer Absage kann man sich wieder bewerben (${nochmalBewerben.status})`);
|
|
/* GEGENPROBE zur Sperre oben: Waere der Index ueber ALLE Zustaende
|
|
eindeutig, kaeme hier 409 -- und eine Absage waere ein Bann. */
|
|
const wiederOffen = (await hole(handAn.keks, "/workspace/api/vorlagen"))
|
|
.daten?.bewerbungen?.filter((x) => x.vorlage === v2.schluessel).length;
|
|
ok(wiederOffen === 1, `und sie wartet wieder auf Antwort (${wiederOffen})`);
|
|
|
|
/* Und auf etwas, das man schon hat, bewirbt man sich nicht. */
|
|
const doppelt = await hole(modiAn.keks, "/workspace/api/vorlagen/bewerben", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ vorlage: bewerbungsVorlage.schluessel }),
|
|
});
|
|
ok(doppelt.status === 409 && doppelt.daten?.fehler === "liegt_schon_bei_dir",
|
|
`auf etwas, das schon auf dem Brett liegt, bewirbt man sich nicht (${doppelt.status})`);
|
|
}
|
|
|
|
/* ---------- 4. DogFather traegt fuer jemanden ein -------------------- */
|
|
console.log("\n=== 4. DogFather trägt für einen Modi ein ===\n");
|
|
{
|
|
const nr = katalog.aufgaben.findIndex((v) => v.gruppe === "analyse");
|
|
const a = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ art: "katalog", nr, gruppe: "analyse", person_id: idModi }),
|
|
});
|
|
ok(a.status === 200 && a.daten?.angelegt === 1, `für den Modi geht es (${a.status})`);
|
|
|
|
/* Aber nicht an einen Creator -- sonst laege eine Aufgabe mit
|
|
Modi-Kategorie an jemandem, der die Kategorien gar nicht kennt, und
|
|
die Trennung waere von innen aufgeweicht. */
|
|
const b = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ art: "katalog", nr, gruppe: "monat", person_id: idCreat }),
|
|
});
|
|
/* 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. */
|
|
const c = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ art: "katalog", nr, gruppe: "monat" }),
|
|
});
|
|
ok(c.status === 400 && /Person/i.test(c.daten?.fehler || ""),
|
|
`ohne Person wird gefragt (${c.status} ${c.daten?.fehler || ""})`);
|
|
}
|
|
|
|
/* ---------- 5. Der heikelste Fall ------------------------------------ */
|
|
console.log("\n=== 5. Ein Manager versucht es trotzdem ===\n");
|
|
{
|
|
const a = await hole(manAn.keks, "/workspace/api/vorlagen/uebernehmen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ art: "katalog", nr: 0, gruppe: "aufbau", person_id: idModi }),
|
|
});
|
|
const b = await hole(manAn.keks, "/workspace/api/vorlagen/uebernehmen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ art: "gibtesnicht", nr: 0 }),
|
|
});
|
|
/* WORTGLEICH. Ein eigener Fehlertext waere die Auskunft, dass es die
|
|
Art gibt -- und damit, dass es den Katalog gibt. */
|
|
ok(a.status === b.status && JSON.stringify(a.daten) === JSON.stringify(b.daten),
|
|
`klingt wie eine erfundene Art (${a.status} ${JSON.stringify(a.daten)})`);
|
|
}
|
|
|
|
/* ---------- 6. Im Browser -------------------------------------------- */
|
|
console.log("\n=== 6. Das Brett im echten Browser ===\n");
|
|
{
|
|
const { chromium } = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await chromium.launch();
|
|
try {
|
|
const kontext = await browser.newContext({ viewport: { width: 1280, height: 1000 } });
|
|
const seite = await kontext.newPage();
|
|
const konsole = [];
|
|
seite.on("console", (e) => { if (e.type() === "error") konsole.push(e.text()); });
|
|
seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message));
|
|
/* ==== ANGEMELDET UEBER DEN KEKS (01.10.2026) ================
|
|
|
|
Hier wurde auf die Creator-Kachel getippt und ein Modi-Code
|
|
eingegeben -- das ging, solange der stille Zugang jede Kachel
|
|
durchliess. Seit die Kachel bindend ist, braeuchte der Browser
|
|
die MODI-Kachel, und die gibt es nur auf der Crew-Wand; ein
|
|
Browser auf 127.0.0.1 bekommt die Agenturwand.
|
|
|
|
Gemessen werden soll hier, was ein Modi SIEHT -- nicht, wie er
|
|
hereinkommt. Das steht in pruef-modi-verborgen, und dort wird es
|
|
an der echten Wand gemessen. */
|
|
const modiKeks = String((await anmelden("modi", "CODE-MODI-0001")).keks || "");
|
|
await kontext.addCookies(modiKeks.split("; ").filter(Boolean).map((z) => {
|
|
const [n2, ...r2] = z.split("=");
|
|
return { name: n2, value: r2.join("="), domain: "127.0.0.1", path: "/" };
|
|
}));
|
|
await seite.goto(BASIS + "/workspace/start.html", { waitUntil: "domcontentloaded" });
|
|
/* DER KATALOG STEHT SEIT DEM 23.09.2026 AUF "EURE AUFGABEN".
|
|
Filipe: "das soll auch bitte nicht in der kategorie aufgaben
|
|
sondern eure aufgaben sein bitte." */
|
|
await seite.goto(BASIS + "/workspace/entwicklung.html", { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1600);
|
|
|
|
/* Aufklappen, falls es zugeklappt startet. */
|
|
const zu = await seite.evaluate(() =>
|
|
document.getElementById("vorlagenbrett")?.dataset.auf !== "ja");
|
|
if (zu) {
|
|
await seite.click(".vorlagenbrett__kopf").catch(() => {});
|
|
await seite.waitForTimeout(700);
|
|
}
|
|
|
|
const bild = await seite.evaluate(() => ({
|
|
sichtbar: !document.getElementById("vorlagenbrett")?.hidden,
|
|
/* ZWEI REIHEN SEIT DEM 16.09.2026: oben die Kategorien, darunter
|
|
die Stufen. Getrennt gezaehlt -- eine gemeinsame Zahl haette
|
|
nicht gesagt, WELCHE der beiden fehlt. */
|
|
/* UEBER DIE BESCHRIFTUNG UNTERSCHIEDEN, nicht ueber eine
|
|
Hilfsklasse (16.09.2026). Seit die Stufenreihe genauso aussieht
|
|
wie die Kategorienreihe -- so wollte Filipe es -- gibt es im
|
|
Aussehen keinen Unterschied mehr. Den gibt es aber im SINN, und
|
|
der steht ohnehin da: in dem Wort, das ein Vorleseprogramm
|
|
ansagt. Eine Klasse nur zum Wiederfinden waere eine zweite
|
|
Wahrheit ueber dieselbe Sache. */
|
|
kategorien: [...document.querySelectorAll(
|
|
'[aria-label="Kategorie wählen"] .stufenleiter__name')]
|
|
.map((z) => z.textContent.trim()),
|
|
stufen: [...document.querySelectorAll(
|
|
'[aria-label="Stufe wählen"] .stufenleiter__name')]
|
|
.map((z) => z.textContent.trim()),
|
|
karten: document.querySelectorAll(".vorlagenbrett__karte").length,
|
|
/* WIE VIELE ZEILEN BRAUCHT DIE FRIST? Gemessen ueber einen
|
|
Bereich um den TEXT, nicht ueber das Element.
|
|
|
|
ERST FALSCH GEMESSEN, und die Gegenprobe hat es verraten:
|
|
`element.getClientRects()` liefert hier IMMER genau ein
|
|
Rechteck, weil ein Flex-Kind zu einem Block wird -- die
|
|
Pruefung blieb gruen, auch mit dem Fehler wieder eingebaut.
|
|
Ein Bereich ueber den Textknoten dagegen gibt pro Zeilenkasten
|
|
ein Rechteck, und genau das ist die Frage: Steht "in 2 Tagen"
|
|
auf einer Zeile oder als "in / 2 / Tagen" untereinander? */
|
|
fristZeilen: [...document.querySelectorAll(".vorlagenbrett__frist")]
|
|
.map((z) => {
|
|
const b = document.createRange();
|
|
b.selectNodeContents(z);
|
|
return b.getClientRects().length;
|
|
}),
|
|
/* Und wie viele Karten tragen ueberhaupt den DRITTEN Eintrag, an
|
|
dem es eng wurde. Ohne diese Zahl wuerde die Zeile darueber
|
|
auch dann gruen, wenn der enge Fall gar nicht auf dem Schirm
|
|
war. */
|
|
engeKarten: document.querySelectorAll(".vorlagenbrett__schon").length,
|
|
text: document.body.innerText,
|
|
kopf: (document.querySelector(".kopfleiste .marke__text") || {}).textContent || "",
|
|
}));
|
|
ok(bild.sichtbar, "das Brett steht da");
|
|
ok(bild.kategorien.length === 14,
|
|
`mit vierzehn Kategorien (${bild.kategorien.join(" · ")})`);
|
|
/* DREI STUFEN PLUS "Alle" -- der vierte Knopf ist die Ruecknahme.
|
|
Ohne ihn kaeme man aus einer Stufe nicht wieder heraus, ohne die
|
|
Kategorie zu wechseln. */
|
|
ok(bild.stufen.length === 4 && bild.stufen[0] === "Alle",
|
|
`und drei Stufen daneben (${bild.stufen.join(" · ")})`);
|
|
ok(bild.karten > 0, `und ${bild.karten} Karten in der offenen Etappe`);
|
|
/* Der Katalog des ANDEREN darf nirgends durchschlagen. */
|
|
ok(!/vier Bereichen und vier Stufen/.test(bild.text),
|
|
"kein Wort vom Creator-Katalog");
|
|
/* DIE KOPFLEISTE. Auf jeder Seite steht dort fest "Creator
|
|
Workspace" -- fuer jemanden, der moderiert statt einen Kanal
|
|
aufzubauen, ist das schlicht der falsche Name. Aufgefallen auf
|
|
dem Bildschirmfoto, wie schon die Zierzeile auf der Startseite. */
|
|
ok(bild.kopf.includes("Team") && !bild.kopf.includes("Creator"),
|
|
`die Kopfleiste nennt seinen Betrieb ("${bild.kopf.trim()}")`);
|
|
/* Und der Seitenname dahinter steht noch da -- ihn beim Austausch
|
|
zu verlieren waere lautlos passiert. */
|
|
ok(/Aufgaben/.test(bild.kopf), "der Seitenname dahinter blieb erhalten");
|
|
/* DIE FUSSZEILE DER KARTEN (16.09.2026, auf dem Bildschirmfoto
|
|
gefunden). Bei einer schon uebernommenen Aufgabe stehen dort
|
|
drei Dinge statt zwei, und "Frist: in 2 Tagen" brach mitten im
|
|
Wort auf drei Zeilen um. Die Karten sind alle gleich breit --
|
|
eine einzige gequetschte reicht, um den Fehler zu zeigen. */
|
|
ok(bild.engeKarten > 0,
|
|
`${bild.engeKarten} Karten tragen den dritten Fusseintrag (der enge Fall)`);
|
|
const zuViel = bild.fristZeilen.filter((n) => n !== 1).length;
|
|
ok(bild.fristZeilen.length > 0 && zuViel === 0,
|
|
`jede der ${bild.fristZeilen.length} Fristangaben steht auf einer Zeile`
|
|
+ (zuViel ? ` -- ${zuViel} umgebrochen` : ""));
|
|
|
|
/* UMLAUTE AUF DEM FERTIGEN BILDSCHIRM (16.09.2026).
|
|
Am 16.09. stand unter jeder Kategorie eine Erklaerung in ASCII:
|
|
"Was im Livechat passiert, waehrend gesendet wird. Loeschen,
|
|
stummschalten, begruessen". Alles gruen, niemand hat es gemerkt.
|
|
|
|
HIER und nicht im Quelltext, weil hier der Unterschied sichtbar
|
|
wird, auf den es ankommt: `innerText` ist, was ein Mensch liest.
|
|
Feldnamen, Stilklassen und Adressen -- die ASCII sein MUESSEN --
|
|
stehen nicht drin. Im Quelltext derselben Skripte schlaegt
|
|
dieselbe Liste 71 Mal an, und kein einziges Mal zu Recht.
|
|
|
|
Die Kataloge und die festen Seitentexte prueft
|
|
pruef-deutsche-texte, ohne Browser. */
|
|
const ersatz = ersatzschreibungen(bild.text);
|
|
ok(bild.text.length > 400 && ersatz.length === 0,
|
|
ersatz.length
|
|
? `Ersatzschreibungen auf dem Brett: ${ersatz.join(", ")}`
|
|
: `der Text auf dem Brett traegt Umlaute (${bild.text.length} Zeichen geprueft)`);
|
|
/* GEGENPROBE, sonst hiesse "nichts gefunden" nur, dass nichts
|
|
gesucht wurde -- etwa wenn `innerText` leer bliebe. */
|
|
ok(ersatzschreibungen(`${bild.text} waehrend`).length === 1,
|
|
"und ein eingeschmuggeltes waehrend wuerde auffallen");
|
|
/* ==== AM BILDSCHIRM: BEWERBEN STATT UEBERNEHMEN (23.09.2026) ===
|
|
|
|
Filipe: „die modis sollen bei all diesen voschlaegen auch nur
|
|
bewerben koennen."
|
|
|
|
ALLES BISHERIGE HAT DEN SERVER GEPRUEFT. Hier geht es um die
|
|
andere Haelfte: Steht der richtige Knopf da? Ein Modi, der
|
|
„Uebernehmen" liest und dann eine Absage bekommt, haelt die
|
|
Seite fuer kaputt. */
|
|
const knoepfe = await seite.evaluate(() =>
|
|
[...document.querySelectorAll(".vorlagenbrett__karte .knopf")]
|
|
.map((k) => k.textContent.trim()));
|
|
/* DIE ZAHL GEHOERT IN DIE BEDINGUNG. Bei null Karten waere
|
|
`every()` wahr, ohne einen Knopf angesehen zu haben -- genau der
|
|
Fehler, der am 01.09. dreimal gruen dastand. */
|
|
ok(knoepfe.length >= 10 && knoepfe.every((t) => t === "Bewerben"),
|
|
`alle ${knoepfe.length} Knoepfe heissen "Bewerben" (${[...new Set(knoepfe)].join(", ")})`);
|
|
/* GEGENPROBE: „Uebernehmen" kommt nirgends mehr vor -- weder als
|
|
Knopf noch als „Alle 12 uebernehmen" darunter. */
|
|
const uebernahme = await seite.evaluate(() =>
|
|
[...document.querySelectorAll("#vorlagenbrett button")]
|
|
.filter((k) => /übernehmen/i.test(k.textContent)).length);
|
|
ok(uebernahme === 0,
|
|
`Gegenprobe: kein einziger Uebernehmen-Knopf mehr (${uebernahme})`);
|
|
|
|
/* Die schon beantragte Karte sagt, dass sie wartet -- und WER
|
|
antwortet. Sonst wartet man auf niemanden.
|
|
|
|
ERST ZUR RICHTIGEN KATEGORIE. Die offene Bewerbung liegt bei
|
|
„Events", das Brett startet bei der ersten Kategorie. Ohne
|
|
diesen Klick maesse die Pruefung eine Karte, die gar nicht auf
|
|
dem Bildschirm steht -- und waere rot, ohne dass etwas kaputt
|
|
ist. (Beim Bauen genau so passiert.) */
|
|
await seite.evaluate(() => {
|
|
const reihe = document.querySelector('.vorlagenbrett .stufenleiter');
|
|
const k = [...(reihe?.querySelectorAll('button') || [])]
|
|
.find((b) => /Events/i.test(b.textContent));
|
|
if (k) k.click();
|
|
});
|
|
await seite.waitForTimeout(700);
|
|
const wartet = await seite.evaluate(() => {
|
|
const w = document.querySelector(".vorlagenbrett__warte");
|
|
const karte = w?.closest(".vorlagenbrett__karte");
|
|
return w ? {
|
|
satz: w.textContent.trim(),
|
|
zurueck: !![...karte.querySelectorAll("button")]
|
|
.find((b) => /zurückziehen/i.test(b.textContent)),
|
|
} : null;
|
|
});
|
|
ok(!!wartet, "die Karte, auf die er sich beworben hat, sagt es");
|
|
ok(/rechte hand/i.test(wartet?.satz || ""),
|
|
` und nennt, wer antwortet ("${wartet?.satz}")`);
|
|
ok(wartet?.zurueck === true, " mit einem Weg zurueck");
|
|
|
|
ok(konsole.length === 0,
|
|
konsole.length ? `Fehler in der Konsole: ${konsole[0]}` : "keine Fehler in der Konsole");
|
|
/* DER PFAD GEHOERT AN DIESE DATEI, nicht ans Arbeitsverzeichnis.
|
|
Vorher stand hier ein blosser Dateiname: Das Bild landete dort,
|
|
von wo aus gestartet wurde, waehrend die Zeile darunter
|
|
"server/..." meldete. Man sucht es dann an der falschen Stelle
|
|
-- oder findet ein altes und haelt es fuer das neue. */
|
|
const bildPfad = fileURLToPath(new URL("pruef-modi-katalog.png", import.meta.url));
|
|
await seite.screenshot({ path: bildPfad, fullPage: false });
|
|
console.log(" -- Bildschirmfoto: server/pruef-modi-katalog.png");
|
|
await kontext.close();
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
}
|
|
|
|
/* =======================================================================
|
|
6b. AN WEN? — die Auswahl steht dort, wo entschieden wird (16.09.2026)
|
|
|
|
Filipe: „perfektionniere die kategorien wo ich aufgaben an die modis
|
|
verteile und so."
|
|
|
|
Bis heute kam die Person aus dem Formular GANZ OBEN auf der Seite:
|
|
dort wählen, herunterscrollen, „Übernehmen" drücken. Wer das nicht
|
|
weiß, bekommt „Bitte zuerst eine Person wählen" und sucht.
|
|
|
|
Und man sah beim Vergeben nicht, wer schon wie viel offen hat. Zu
|
|
wenig Zeit ist der meistgenannte Grund, warum Moderatoren aufhören;
|
|
die Person, die verteilt, ist die einzige, die das verhindern kann.
|
|
Dafür muss die Zahl dort stehen, wo entschieden wird — nicht auf
|
|
einer Auswertung, die man hinterher aufruft.
|
|
======================================================================= */
|
|
console.log("");
|
|
console.log("=== 6b. An wen die Aufgabe geht ===");
|
|
console.log("");
|
|
{
|
|
const { chromium } = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await chromium.launch();
|
|
try {
|
|
const kontext = await browser.newContext({ viewport: { width: 1280, height: 1100 } });
|
|
const seite = await kontext.newPage();
|
|
const konsole = [];
|
|
seite.on("console", (e) => { if (e.type() === "error") konsole.push(e.text()); });
|
|
seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message));
|
|
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
|
await seite.click('.rolle[data-rolle="admin"]');
|
|
await seite.fill("#code", "CODE-DOGI-0001");
|
|
await seite.click("button[type=submit]");
|
|
await seite.waitForURL("**/start.html", { timeout: 15000 });
|
|
/* Siehe oben: der Katalog ist umgezogen. */
|
|
await seite.goto(BASIS + "/workspace/entwicklung.html", { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1800);
|
|
const zu = await seite.evaluate(() =>
|
|
document.getElementById("vorlagenbrett")?.dataset.auf !== "ja");
|
|
if (zu) {
|
|
await seite.click(".vorlagenbrett__kopf").catch(() => {});
|
|
await seite.waitForTimeout(800);
|
|
}
|
|
|
|
/* ==== AN WEN? DIE FRAGE IST JETZT SCHON BEANTWORTET ============
|
|
|
|
Bis zum 23.09.2026 brachte der Katalog eine EIGENE Personenreihe
|
|
mit -- eine dritte Knopfreihe unter zwei anderen. Sie war noetig,
|
|
weil "Aufgaben" keine Personenwahl hat.
|
|
|
|
Auf "Eure Aufgaben" gibt es sie: die Kacheln unter "Wen gehst du
|
|
durch?". Zwei Auswahlen fuer dieselbe Frage waeren die
|
|
Gelegenheit, oben den einen und unten den anderen zu waehlen --
|
|
und dann stuenden zwei Antworten auf einem Bildschirm.
|
|
|
|
GEPRUEFT WIRD DESHALB DIE VERBINDUNG: Was die Seite oben sagt,
|
|
muss der Katalog unten meinen. */
|
|
const ohne = await seite.evaluate(() => {
|
|
const reihe = document.querySelector('#vorlagenbrett [aria-label="Person wählen"]');
|
|
return {
|
|
knopf: document.querySelector(".vorlagenbrett__karte .knopf")?.textContent.trim(),
|
|
satz: [...document.querySelectorAll("#vorlagenbrett .stufenleiter__hilfe")]
|
|
.map((z) => z.textContent.trim()).join(" | "),
|
|
hin: !!document.querySelector("#vorlagenbrett .knopf-still"),
|
|
reihen: document.querySelectorAll("#vorlagenbrett .stufenleiter").length,
|
|
namen: [...(reihe?.querySelectorAll(".stufenleiter__name") || [])]
|
|
.map((z) => z.textContent.trim()),
|
|
zahlen: [...(reihe?.querySelectorAll(".stufenleiter__zahl") || [])].length,
|
|
};
|
|
});
|
|
/* DREI REIHEN: Kategorie, Stufe, Person.
|
|
|
|
Hier stand zwischenzeitlich "zwei Auswahlreihen statt drei" --
|
|
weil ich die Personenreihe beim Umzug entfernt hatte. Das war
|
|
nicht verlangt; sie ist wieder da. Was sich geaendert hat, ist
|
|
nur, WAS sie setzt: dieselbe Person wie die Kacheln oben.
|
|
Gemessen wird das ein paar Zeilen weiter unten. */
|
|
ok(ohne.reihen === 3,
|
|
"drei Auswahlreihen (Kategorie, Stufe, Person): " + ohne.reihen);
|
|
ok(ohne.namen.includes("Marina"),
|
|
"der Modi steht zur Auswahl (" + ohne.namen.join(", ") + ")");
|
|
ok(!ohne.namen.includes("Tili") && !ohne.namen.includes("Schulle"),
|
|
"ein Creator und eine Managerin nicht — sie arbeiten im anderen Haus");
|
|
ok(ohne.zahlen === ohne.namen.length && ohne.namen.length > 0,
|
|
"neben jedem Namen steht, wie viel offen ist (" + ohne.zahlen + ")");
|
|
ok(ohne.knopf === "Übernehmen",
|
|
'solange niemand gewaehlt ist, heisst der Knopf schlicht "Übernehmen" ('
|
|
+ ohne.knopf + ")");
|
|
ok(/niemand gewählt/.test(ohne.satz), "und ein Satz sagt genau das");
|
|
/* KEINE WEGBESCHREIBUNG, SONDERN EIN WEG. "Waehle oben jemanden"
|
|
waere falsch -- die Kacheln stehen weiter UNTEN. Der Knopf
|
|
weiss, wo sie sind, auch nach dem naechsten Umbau. */
|
|
ok(ohne.hin, "mit einem Knopf, der zur Personenwahl fuehrt");
|
|
|
|
/* JETZT WIRKLICH WAEHLEN -- und zwar IN DER REIHE, nicht an der
|
|
Kachel. Genau das ist die Frage seit dem 23.09.2026: Die Reihe
|
|
hat keine eigene Auswahl mehr, sie setzt dieselbe Person wie die
|
|
Kacheln oben. Ein Tipp hier muss also dort ankommen. */
|
|
await seite.evaluate(() => {
|
|
const reihe = document.querySelector('#vorlagenbrett [aria-label="Person wählen"]');
|
|
[...(reihe?.querySelectorAll(".stufenleiter__knopf") || [])]
|
|
.find((k) => k.textContent.includes("Marina"))?.click();
|
|
});
|
|
await seite.waitForTimeout(2600);
|
|
/* DIE KACHEL OBEN MUSS ES WISSEN. Ohne diese Zeile bewiese der
|
|
Rest nur, dass der Katalog mit sich selbst einig ist. */
|
|
const obenAn = await seite.evaluate(() =>
|
|
[...document.querySelectorAll("#personen .e-person")]
|
|
.filter((k) => k.getAttribute("aria-pressed") === "true")
|
|
.map((k) => k.dataset.name || k.textContent.trim().split("\n")[0]));
|
|
ok(obenAn.some((t) => t.includes("Marina")),
|
|
"ein Tipp in der Reihe waehlt die Person auch oben (" + (obenAn.join(", ") || "keine") + ")");
|
|
const nachWahl = await seite.evaluate(() => {
|
|
const karten = [...document.querySelectorAll(".vorlagenbrett__karte")];
|
|
const frisch = karten.find((k) => k.dataset.schon !== "ja");
|
|
const schon = karten.find((k) => k.dataset.schon === "ja");
|
|
return {
|
|
satz: [...document.querySelectorAll("#vorlagenbrett .stufenleiter__hilfe")]
|
|
.map((z) => z.textContent.trim()).find((t) => t.includes("geht an")) || "",
|
|
/* ZWEI VERSCHIEDENE KARTEN, ZWEI VERSCHIEDENE KNOEPFE -- und
|
|
die erste im Feld ist nicht zwangslaeufig eine frische.
|
|
Die erste Fassung nahm schlicht "die erste Karte" und
|
|
meldete einen Fehler, weil dort "Nochmal" stand: genau das
|
|
richtige Wort fuer eine, die schon liegt. */
|
|
frisch: frisch?.querySelector(".knopf")?.textContent.trim() || null,
|
|
schon: schon?.querySelector(".knopf")?.textContent.trim() || null,
|
|
liste: document.getElementById("e-aufgaben-titel")?.textContent || "",
|
|
};
|
|
});
|
|
ok(nachWahl.satz.includes("Marina"), "nach dem Klick steht es da: " + nachWahl.satz);
|
|
/* DER KNOPF NENNT DEN NAMEN. "Uebernehmen" allein laesst offen,
|
|
wer sie bekommt -- und die Antwort steht weit oben. */
|
|
ok(nachWahl.frisch === "An Marina",
|
|
'eine frische Karte traegt ihren Namen ("' + nachWahl.frisch + '")');
|
|
/* Und eine, die sie schon hat, sagt das statt des Namens -- sonst
|
|
saehen beide gleich aus und man holte sie zweimal. */
|
|
ok(nachWahl.schon === null || nachWahl.schon === "Nochmal",
|
|
'eine schon uebernommene sagt "Nochmal" ('
|
|
+ (nachWahl.schon === null ? "keine im Feld" : nachWahl.schon) + ")");
|
|
ok(/Marina/.test(nachWahl.liste),
|
|
'die Aufgabenliste darunter gehoert ihr ("' + nachWahl.liste + '")');
|
|
|
|
/* ==== DIE KACHEL DREHT SICH UM (25.09.2026) ======================
|
|
|
|
Filipe: „wenn ich drauf drücke dreht sich die kachel und dan seh
|
|
ich wer es gemacht hat, und immer noch die option es nochmal zu
|
|
verteilen falls neue leute ins team zustoßen."
|
|
|
|
DREI DINGE, UND JEDES EINZELN:
|
|
1. Bei „Alle" gibt es ueberhaupt einen Weg auf die Rueckseite.
|
|
2. Dort stehen die NAMEN -- so viele, wie es Zielbare gibt.
|
|
3. Und ein Knopf, der wieder verteilt.
|
|
|
|
DIE ZAHL GEHOERT IN DIE BEDINGUNG: Eine leere Rueckseite wuerde
|
|
sonst als "gedreht, alles gut" durchgehen -- der Fehler, der in
|
|
diesem Haus am 01.09. dreimal gruen dastand. */
|
|
await seite.evaluate(() => {
|
|
document.querySelector('.stufenleiter--wer [data-alle="ja"]')?.click();
|
|
});
|
|
await seite.waitForTimeout(900);
|
|
const dreh = await seite.evaluate(() => {
|
|
const k = document.querySelector('.vorlagenbrett__karte[data-dreh="ja"]');
|
|
if (!k) return { drehbar: false };
|
|
const knopf = k.querySelector(".vorlagenbrett__drehknopf");
|
|
knopf?.click();
|
|
const hinten = k.querySelector(".vorlagenbrett__seite--hinten");
|
|
return {
|
|
drehbar: true,
|
|
gedreht: k.dataset.gedreht === "ja",
|
|
namen: [...(hinten?.querySelectorAll(".vorlagenbrett__wer-name") || [])]
|
|
.map((x) => x.textContent),
|
|
knoepfe: [...(hinten?.querySelectorAll("button") || [])]
|
|
.map((b) => b.textContent.trim()),
|
|
/* Und vorne darf danach NICHTS mehr von der Rueckseite stehen
|
|
-- sonst waere es kein Umdrehen, sondern ein Aufklappen. */
|
|
vorneNamen: k.querySelectorAll(
|
|
".vorlagenbrett__seite--vorn .vorlagenbrett__wer-name").length,
|
|
};
|
|
});
|
|
ok(dreh.drehbar, "bei \u201eAlle\u201c ist die Kachel drehbar");
|
|
ok(dreh.gedreht === true, `und ein Druck dreht sie um (${dreh.gedreht})`);
|
|
ok((dreh.namen || []).length >= 2,
|
|
`hinten stehen die Namen (${(dreh.namen || []).join(", ") || "keine"})`);
|
|
ok((dreh.knoepfe || []).some((t) => /Nachholen|Nochmal/.test(t))
|
|
&& (dreh.knoepfe || []).some((t) => /Zur\u00fcck/.test(t)),
|
|
`mit Verteil- und Zurueck-Knopf (${(dreh.knoepfe || []).join(" | ") || "keine"})`);
|
|
ok(dreh.vorneNamen === 0,
|
|
`und vorne steht kein Name mehr (${dreh.vorneNamen})`);
|
|
|
|
/* ==== UND DIE AUSWAHL WIEDER ZURUECKSTELLEN =====================
|
|
|
|
Der Klick auf "Alle" hat die gewaehlte Person veraendert -- und
|
|
die Pruefungen weiter unten arbeiten mit Marina. Beim ersten
|
|
Anlauf fielen dadurch zwei Zeilen um ("die Aufgabe liegt in
|
|
ihrer Liste (2 -> 2)"), und zwar an Stellen, an denen nichts
|
|
kaputt war. Eine Pruefung, die den Zustand fuer die naechste
|
|
verstellt, misst deren Ergebnis mit.
|
|
|
|
ES WIRD NACHGEWIESEN, NICHT GEHOFFT: Ohne die Zeile darunter
|
|
waere ein misslungenes Zuruecksetzen wieder ein Fehler an der
|
|
falschen Stelle -- und man suchte ihn dort. */
|
|
await seite.evaluate(() => {
|
|
const b = [...document.querySelectorAll(".stufenleiter--wer .stufenleiter__knopf")]
|
|
.find((x) => /Marina/.test(x.textContent));
|
|
b?.click();
|
|
});
|
|
await seite.waitForTimeout(900);
|
|
const zurueckGestellt = await seite.evaluate(() =>
|
|
[...document.querySelectorAll(".stufenleiter--wer .stufenleiter__knopf")]
|
|
.filter((x) => x.getAttribute("aria-pressed") === "true")
|
|
.map((x) => x.textContent.trim().replace(/\s+/g, " ")));
|
|
ok(zurueckGestellt.some((t) => /Marina/.test(t)),
|
|
`die Auswahl steht wieder auf Marina (${zurueckGestellt.join(", ") || "keine"})`);
|
|
|
|
/* GEZAEHLT WIRD IN IHRER LISTE, NICHT AUF DEM BRETT.
|
|
|
|
Auf "Aufgaben" stand unter dem Katalog das Brett, und die
|
|
Pruefung zaehlte dort die Karten. Hier steht darunter die
|
|
Aufgabenliste der gewaehlten Person -- und die ist die bessere
|
|
Antwort auf die Frage, die der Abschnitt stellt: Landet die
|
|
Aufgabe bei DIESEM Menschen? Auf dem Brett musste man das aus
|
|
dem Namen auf der Karte herauslesen; hier ist es die Liste
|
|
selbst, die ihren Namen traegt. */
|
|
const zaehleListe = () =>
|
|
document.querySelectorAll("#e-aufgaben-liste .e-aufgabe").length;
|
|
const vorher = await seite.evaluate(zaehleListe);
|
|
const welche = await seite.evaluate(() =>
|
|
document.querySelector(".vorlagenbrett__karte .vorlagenbrett__titel")
|
|
?.textContent.trim() || "");
|
|
await seite.evaluate(() => {
|
|
[...document.querySelectorAll(".vorlagenbrett__karte")][0]
|
|
?.querySelector("button")?.click();
|
|
});
|
|
await seite.waitForTimeout(2600);
|
|
const danach = await seite.evaluate((titel) => ({
|
|
liste: document.querySelectorAll("#e-aufgaben-liste .e-aufgabe").length,
|
|
steht: [...document.querySelectorAll("#e-aufgaben-liste .e-aufgabe")]
|
|
.some((a) => a.textContent.includes(titel)),
|
|
ueberschrift: document.getElementById("e-aufgaben-titel")?.textContent || "",
|
|
markiert: [...document.querySelectorAll(".vorlagenbrett__karte")]
|
|
.filter((k) => k.dataset.schon === "ja").length,
|
|
}), welche);
|
|
ok(danach.liste === vorher + 1,
|
|
"die Aufgabe liegt in ihrer Liste (" + vorher + " -> " + danach.liste + ")");
|
|
ok(danach.steht, 'und zwar genau die uebernommene ("' + welche + '")');
|
|
ok(/Marina/.test(danach.ueberschrift),
|
|
"die Liste ist ihre (" + danach.ueberschrift.trim() + ")");
|
|
/* UND DER KATALOG WEISS ES. Ohne diese Zeile koennte man dieselbe
|
|
Vorlage gleich noch einmal holen, ohne Hinweis -- genau das war
|
|
der Grund fuer die Markierung. */
|
|
ok(danach.markiert >= 1,
|
|
"der Katalog markiert sie als uebernommen (" + danach.markiert + ")");
|
|
|
|
/* ==== ANPASSEN AM KATALOG -- DORT, WO VANVAN ARBEITET =========
|
|
==============================================================
|
|
|
|
VanVan im Support, Meldung #6: „Es waere gut, wenn man auch
|
|
dort die Moeglichkeit haette die Frist anzupassen, sie als
|
|
dauerhafte Aufgabe einstellen zu koennen oder eventuell noch
|
|
eine Anmerkung hinzufuegen."
|
|
|
|
WARUM DIESE ZEILEN HIER STEHEN UND NICHT NUR IN
|
|
pruef-aufgaben-vorlagen: Dort wird der CREATOR-Zweig gemessen
|
|
-- das Vorlagenbrett auf „Aufgaben", angemeldet als Creator.
|
|
VanVan benutzt den KATALOG auf „Eure Aufgaben". Das sind zwei
|
|
verschiedene Karten in derselben Datei, und der Knopf haengt
|
|
an beiden einzeln.
|
|
|
|
Nach dem Einbau war nur der eine Zweig im Browser geprueft.
|
|
Beide liefern denselben Haken NICHT: Ein Creator darf
|
|
„dauerhaft" nicht setzen, die Leitung schon -- und genau
|
|
dieser Unterschied ist hier die Gegenprobe zu dort.
|
|
|
|
Dass man sich bei diesen beiden Brettern vertun kann, ist in
|
|
diesem Haus schon vorgekommen: In pruef-bewerbung-aufgaben
|
|
steht es als Messfehler notiert, „der wie ein Befund
|
|
aussieht". */
|
|
const anpassbar = await seite.evaluate(() => ({
|
|
karten: document.querySelectorAll(".vorlagenbrett__karte").length,
|
|
knoepfe: document.querySelectorAll(".vorlagenbrett__anpassen").length,
|
|
}));
|
|
ok(anpassbar.karten > 0 && anpassbar.knoepfe === anpassbar.karten,
|
|
`jede Katalogkarte hat einen „Anpassen …"-Knopf `
|
|
+ `(${anpassbar.knoepfe} von ${anpassbar.karten})`);
|
|
|
|
await seite.click(".vorlagenbrett__karte .vorlagenbrett__anpassen");
|
|
await seite.waitForSelector("dialog.nachfrage[open]", { timeout: 8000 });
|
|
const felder = await seite.evaluate(() => {
|
|
const dlg = document.querySelector("dialog.nachfrage");
|
|
const sicht = (teil) => {
|
|
const z = dlg.querySelector(`[data-teil="${teil}"]`);
|
|
return !!z && !z.hidden;
|
|
};
|
|
return {
|
|
datum: sicht("datumzeile"),
|
|
haken: sicht("hakenzeile"),
|
|
grund: sicht("grundzeile"),
|
|
wert: dlg.querySelector("#nachfrage-datum")?.value || "",
|
|
min: dlg.querySelector("#nachfrage-datum")?.min || "",
|
|
};
|
|
});
|
|
ok(felder.datum && felder.grund,
|
|
`das Fenster hat Frist und Anmerkung (${felder.datum}, ${felder.grund})`);
|
|
/* DIE GEGENPROBE ZU pruef-aufgaben-vorlagen: Dort bekommt ein
|
|
Creator den Haken NICHT. Hier, bei der Leitung, MUSS er da
|
|
sein -- sonst waere „dauerhaft" eine Einstellung, die es nur
|
|
im Quelltext gibt. */
|
|
ok(felder.haken === true,
|
|
`und die Leitung bekommt den „dauerhaft"-Haken (${felder.haken})`);
|
|
ok(/^\d{4}-\d{2}-\d{2}$/.test(felder.wert) && felder.wert >= felder.min,
|
|
`die Frist der Vorlage steht als Vorgabe drin (${felder.wert}, `
|
|
+ `fruehestens ${felder.min})`);
|
|
|
|
/* DER HAKEN SPERRT DAS DATUM -- eine dauerhafte Aufgabe hat
|
|
keine Frist. Ein Datum, das dasteht und nicht gilt, ist
|
|
schlimmer als keins. */
|
|
await seite.check("dialog.nachfrage #nachfrage-haken");
|
|
await seite.waitForTimeout(200);
|
|
ok((await seite.evaluate(() =>
|
|
document.querySelector("dialog.nachfrage #nachfrage-datum").disabled)) === true,
|
|
"mit Haken wird die Frist gesperrt");
|
|
|
|
const vorDauer = await seite.evaluate(zaehleListe);
|
|
await seite.fill("dialog.nachfrage #nachfrage-grund",
|
|
"Laeuft dauerhaft mit, bitte woechentlich kurz durchgehen.");
|
|
await seite.click('dialog.nachfrage [data-teil="ja"]');
|
|
await seite.waitForTimeout(2600);
|
|
ok(await seite.evaluate(zaehleListe) === vorDauer + 1,
|
|
`die angepasste Aufgabe liegt in ihrer Liste `
|
|
+ `(${vorDauer} -> ${await seite.evaluate(zaehleListe)})`);
|
|
|
|
/* UND JETZT DIE TATSACHEN, nicht die Anzeige. Was auf dem Brett
|
|
steht, ist eine Darstellung; was in der Aufgabe steht, gilt. */
|
|
{
|
|
const { DatabaseSync: DB5 } = await import("node:sqlite");
|
|
const d5 = new DB5(process.env.WORKSPACE_DB);
|
|
const neueste = d5.prepare(`SELECT a.id, a.dauerhaft, a.frist,
|
|
(SELECT COUNT(*) FROM aufgaben_notizen n WHERE n.aufgabe_id = a.id) AS notizen,
|
|
(SELECT text FROM aufgaben_notizen n WHERE n.aufgabe_id = a.id LIMIT 1) AS wort
|
|
FROM aufgaben a ORDER BY a.id DESC LIMIT 1`).get();
|
|
d5.close();
|
|
ok(neueste?.dauerhaft === 1,
|
|
`sie ist wirklich dauerhaft (${neueste?.dauerhaft})`);
|
|
/* KEINE FRIST -- sonst waere sie ab morgen fuer immer
|
|
ueberfaellig, und eine Warnung, die immer kommt, ist keine
|
|
mehr. Dieselbe Regel wie im Aenderungsweg. */
|
|
ok(!neueste?.frist,
|
|
`und hat keine Frist (${neueste?.frist ?? "keine"})`);
|
|
ok(neueste?.notizen === 1 && /woechentlich/.test(neueste?.wort || ""),
|
|
`die Anmerkung steht als Notiz dabei („${neueste?.wort}")`);
|
|
}
|
|
|
|
/* ==== DIE BEWERBUNG AM BILDSCHIRM ENTSCHEIDEN (23.09.2026) ======
|
|
|
|
Filipe: „nur dogfather und die rechte hand sollen annehmen oder
|
|
ablehnen koennen, mit einem text als notiz."
|
|
|
|
DER SERVERTEIL IST OBEN GEPRUEFT. Hier geht es um das, was ein
|
|
Mensch tut: Sieht er die Bewerbung, steht der Name dabei, und
|
|
bekommt er ein Feld fuer seine Notiz?
|
|
|
|
ZUR RICHTIGEN KATEGORIE -- die offene Bewerbung liegt bei
|
|
„Events". Ohne den Klick maesse die Pruefung eine Karte, die gar
|
|
nicht auf dem Bildschirm steht. */
|
|
await seite.evaluate(() => {
|
|
const reihe = document.querySelector(".vorlagenbrett .stufenleiter");
|
|
const k = [...(reihe?.querySelectorAll("button") || [])]
|
|
.find((b) => /Events/i.test(b.textContent));
|
|
if (k) k.click();
|
|
});
|
|
await seite.waitForTimeout(800);
|
|
|
|
const bw = await seite.evaluate(() => {
|
|
const reihe = document.querySelector(".vorlagenbrett__bewerbung");
|
|
if (!reihe) return null;
|
|
const karte = reihe.closest(".vorlagenbrett__karte");
|
|
return {
|
|
name: reihe.querySelector(".vorlagenbrett__bname")?.textContent.trim() || "",
|
|
wort: reihe.querySelector(".vorlagenbrett__bwort")?.textContent.trim() || "",
|
|
schild: karte.querySelector(".vorlagenbrett__bschild")?.textContent.trim() || "",
|
|
knoepfe: [...reihe.querySelectorAll("button")].map((b) => b.textContent.trim()),
|
|
auffaellig: karte.dataset.beworben === "ja",
|
|
titel: karte.querySelector(".vorlagenbrett__titel")?.textContent.trim() || "",
|
|
};
|
|
});
|
|
ok(!!bw, "DogFather sieht die Bewerbung an der Karte");
|
|
ok(bw?.name === "Marina", `mit dem Namen dessen, der sie geschrieben hat (${bw?.name})`);
|
|
/* MAN ENTSCHEIDET UEBER EINEN MENSCHEN, nicht ueber einen
|
|
Listeneintrag -- deshalb stehen beide Knoepfe an SEINER Zeile. */
|
|
ok(JSON.stringify(bw?.knoepfe) === JSON.stringify(["Annehmen", "Ablehnen"]),
|
|
`und beiden Wegen daneben (${(bw?.knoepfe || []).join(", ")})`);
|
|
ok(bw?.wort.includes("doch noch Zeit"),
|
|
`sein Wort dazu steht dabei (${bw?.wort})`);
|
|
/* Eine Karte mit Bewerbung ist der einzige Grund, dieses Brett
|
|
gerade zu oeffnen -- sie darf auffallen. */
|
|
ok(bw?.auffaellig === true, "und die Karte faellt auf");
|
|
|
|
/* ---- Jetzt wirklich druecken, samt Notiz ---------------------- */
|
|
/* Auch hier: die Liste der gewaehlten Person, nicht das Brett.
|
|
Marina ist oben gewaehlt, und ihre Bewerbung wird gleich
|
|
angenommen -- die Aufgabe muss also bei ihr auftauchen. */
|
|
const vorherK = await seite.evaluate(() =>
|
|
document.querySelectorAll("#e-aufgaben-liste .e-aufgabe").length);
|
|
await seite.evaluate(() => {
|
|
[...document.querySelectorAll(".vorlagenbrett__bewerbung button")]
|
|
.find((b) => b.textContent.trim() === "Annehmen")?.click();
|
|
});
|
|
await seite.waitForTimeout(600);
|
|
/* DER KASTEN MUSS BEANTWORTET WERDEN. Beim ersten Anlauf einer
|
|
aehnlichen Pruefung habe ich ihn stehen lassen und dann gemeldet,
|
|
der Weg sei kaputt -- er war es nie, ich hatte nur nicht geklickt. */
|
|
const kastenDa = await seite.evaluate(() =>
|
|
!!document.querySelector("dialog.nachfrage[open]"));
|
|
ok(kastenDa, "es kommt ein Kasten für die Notiz — kein stilles Durchdrücken");
|
|
const feldDa = await seite.evaluate(() => {
|
|
const f = document.getElementById("nachfrage-grund");
|
|
return !!f && !f.closest("[hidden]");
|
|
});
|
|
ok(feldDa, " mit einem Feld für den Text — „mit einem text als notiz“");
|
|
await seite.fill("#nachfrage-grund", "danke dir, mach das gern");
|
|
await seite.click('dialog.nachfrage [data-teil="ja"]');
|
|
await seite.waitForTimeout(2200);
|
|
|
|
const nachher = await seite.evaluate(() => ({
|
|
karten: document.querySelectorAll("#e-aufgaben-liste .e-aufgabe").length,
|
|
nochBeworben: document.querySelectorAll(".vorlagenbrett__bewerbung").length,
|
|
}));
|
|
ok(nachher.karten === vorherK + 1,
|
|
`die Aufgabe entsteht mit der Zusage (${vorherK} -> ${nachher.karten})`);
|
|
/* UND DIE BEWERBUNG IST VOM TISCH. Bliebe sie stehen, entschiede
|
|
gleich noch jemand ein zweites Mal ueber dieselbe Bitte. */
|
|
ok(nachher.nochBeworben === 0,
|
|
`und die Bewerbung wartet nicht mehr (${nachher.nochBeworben})`);
|
|
|
|
/* ==== UND WAS IN EINER ANDEREN KATEGORIE WARTET? (23.09.2026) ===
|
|
|
|
DAS WAR EIN LOCH, das ich selbst gebaut hatte. Die Bewerbung
|
|
steht an ihrer Karte -- und die Karte steht in EINER von
|
|
vierzehn Kategorien. Wer eine andere offen hat, sieht nichts:
|
|
Die Bewerbung ist da, das Brett ist offen, und trotzdem findet
|
|
er sie nie.
|
|
|
|
Geprueft wird genau dieser Fall: Eine Bewerbung in einer
|
|
Kategorie, die gerade NICHT gewaehlt ist. Ohne das Band waere
|
|
hier nichts zu sehen -- und die Pruefung war es, die das
|
|
beweisen muss, nicht mein Zutrauen. */
|
|
const fern = katalog.aufgaben.find((v) => v.gruppe === "wachstum");
|
|
ok(!!fern, `es gibt eine Vorlage in einer anderen Kategorie (${fern?.gruppe})`);
|
|
await hole(modiAn.keks, "/workspace/api/vorlagen/bewerben", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ vorlage: fern.schluessel, text: "das kann ich" }),
|
|
});
|
|
|
|
/* Zurueck auf eine ANDERE Kategorie -- die Ausgangslage. */
|
|
await seite.evaluate(() => {
|
|
const reihe = document.querySelector(".vorlagenbrett .stufenleiter");
|
|
const k = [...(reihe?.querySelectorAll("button") || [])]
|
|
.find((b) => /Chat-Moderation/i.test(b.textContent));
|
|
if (k) k.click();
|
|
});
|
|
await seite.reload({ waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1600);
|
|
const zu2 = await seite.evaluate(() =>
|
|
document.getElementById("vorlagenbrett")?.dataset.auf !== "ja");
|
|
if (zu2) {
|
|
await seite.click(".vorlagenbrett__kopf").catch(() => {});
|
|
await seite.waitForTimeout(800);
|
|
}
|
|
|
|
const band = await seite.evaluate(() => {
|
|
const b = document.querySelector(".vorlagenbrett__warten");
|
|
if (!b) return null;
|
|
const knoepfe = [...b.querySelectorAll(".vorlagenbrett__warten-knopf")];
|
|
const offen = document.querySelector('.stufenleiter__knopf[aria-pressed="true"]');
|
|
return {
|
|
schild: b.querySelector(".vorlagenbrett__warten-schild")?.textContent.trim() || "",
|
|
knoepfe: knoepfe.map((k) => k.textContent.trim()),
|
|
/* Steht die Karte dazu gerade ueberhaupt auf dem Brett? */
|
|
karteDa: !!document.querySelector(".vorlagenbrett__karte"),
|
|
offeneKategorie: offen?.textContent.trim() || "",
|
|
hoch: Math.round(knoepfe[0]?.getBoundingClientRect().height || 0),
|
|
};
|
|
});
|
|
ok(!!band, "das Band sagt, dass etwas wartet — auch aus einer anderen Kategorie");
|
|
ok(/wartet|warten/.test(band?.schild || ""),
|
|
`mit einem Satz, der sagt was zu tun ist ("${band?.schild}")`);
|
|
ok((band?.knoepfe || []).some((t) => /Marina/.test(t)),
|
|
`und nennt, wer sich beworben hat (${(band?.knoepfe || []).join(" | ")})`);
|
|
/* AM DAUMEN TREFFBAR -- 40 px ist die Grenze im Haus fuer alles,
|
|
was in einer Reihe steht. */
|
|
ok((band?.hoch || 0) >= 40, `der Knopf ist ${band?.hoch} px hoch`);
|
|
|
|
/* DER SPRUNG MUSS AUCH ANKOMMEN. Ein Knopf, der in die richtige
|
|
Kategorie fuehrt und dort nichts zeigt, ist schlimmer als
|
|
keiner -- genau das passiert, wenn der Stufenfilter stehen
|
|
bleibt. */
|
|
await seite.evaluate(() => {
|
|
document.querySelector(".vorlagenbrett__warten-knopf")?.click();
|
|
});
|
|
await seite.waitForTimeout(1400);
|
|
const nachSprung = await seite.evaluate(() => {
|
|
const hervor = document.querySelector('.vorlagenbrett__karte[data-hervor="ja"]');
|
|
return {
|
|
vorlage: hervor?.dataset.vorlage || "",
|
|
titel: hervor?.querySelector(".vorlagenbrett__titel")?.textContent.trim() || "",
|
|
bewerbungDa: !!hervor?.querySelector(".vorlagenbrett__bewerbung"),
|
|
};
|
|
});
|
|
ok(nachSprung.vorlage === fern.schluessel,
|
|
`der Sprung landet auf der richtigen Karte (${nachSprung.titel})`);
|
|
ok(nachSprung.bewerbungDa,
|
|
"und dort steht die Bewerbung mit Annehmen und Ablehnen");
|
|
|
|
/* GEGENPROBE: Wenn nichts mehr wartet, ist das Band WEG.
|
|
|
|
Ohne sie bewiesen die Zeilen darueber nur, dass das Band immer
|
|
dasteht -- und ein Hinweis, der immer da ist, wird ueberlesen.
|
|
Dieselbe Hausregel wie bei der Warnung, die immer kommt. */
|
|
const offeneAlle = (await hole(handAn.keks, "/workspace/api/vorlagen"))
|
|
.daten?.bewerbungen || [];
|
|
for (const b3 of offeneAlle) {
|
|
await hole(handAn.keks, "/workspace/api/vorlagen/bewerbung/ablehnen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ id: b3.id, kommentar: "Aufraeumen der Pruefung" }),
|
|
});
|
|
}
|
|
ok(offeneAlle.length > 0,
|
|
`${offeneAlle.length} offene Bewerbung(en) werden fuer die Gegenprobe beantwortet`);
|
|
await seite.reload({ waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1600);
|
|
const zu3 = await seite.evaluate(() =>
|
|
document.getElementById("vorlagenbrett")?.dataset.auf !== "ja");
|
|
if (zu3) {
|
|
await seite.click(".vorlagenbrett__kopf").catch(() => {});
|
|
await seite.waitForTimeout(800);
|
|
}
|
|
const ohneBand = await seite.evaluate(() =>
|
|
document.querySelectorAll(".vorlagenbrett__warten").length);
|
|
ok(ohneBand === 0,
|
|
`Gegenprobe: wartet nichts, steht auch nichts da (${ohneBand})`);
|
|
|
|
ok(konsole.length === 0,
|
|
konsole.length ? "Fehler in der Konsole: " + konsole[0].slice(0, 80)
|
|
: "keine Fehler in der Konsole");
|
|
} finally { await browser.close(); }
|
|
}
|
|
|
|
/* =======================================================================
|
|
6c. EINE BEWERBUNG, DIE NIEMAND SIEHT, IST KEINE (23.09.2026)
|
|
=======================================================================
|
|
|
|
Beide Bewerbungswege waren gebaut und beide funktionierten -- und
|
|
beide waren STUMM. Nachgemessen vor dem Bauen: `benachrichtige` kam
|
|
weder in workspace-zuteilung.js noch in workspace-vorlagen.js ein
|
|
einziges Mal vor.
|
|
|
|
Wer sich bewirbt, wartet. Warten ohne Rueckmeldung fuehlt sich nach
|
|
zwei Tagen an wie "interessiert keinen" -- und genau das soll eine
|
|
Bewerbung verhindern.
|
|
|
|
GEPRUEFT WIRD, WAS SICH OHNE PUSH-DIENST PRUEFEN LAESST, und das ist
|
|
fast alles: WER etwas erfaehrt (abgeleitet, nicht aufgezaehlt), WAS
|
|
darin steht (reine Funktionen), und DASS es zwei getrennte Schalter
|
|
gibt.
|
|
======================================================================= */
|
|
console.log("\n=== 6c. Erfaehrt es auch jemand? ===\n");
|
|
{
|
|
const melder = await import("./workspace-bewerbung-melden.js");
|
|
const { ARTEN } = await import("./workspace-push.js");
|
|
|
|
/* ---- DIE SCHALTER --------------------------------------------- */
|
|
const neu2 = ARTEN.find((a) => a.schluessel === "bewerbung_neu");
|
|
const antwort = ARTEN.find((a) => a.schluessel === "bewerbung_antwort");
|
|
ok(!!neu2 && !!antwort,
|
|
"beide Arten stehen in den Benachrichtigungs-Einstellungen");
|
|
ok(neu2?.vorgabe === true && antwort?.vorgabe === true,
|
|
"und sind von sich aus an");
|
|
/* ZWEI SCHALTER, ZWEI ENTSCHEIDUNGEN. Wer an einem lebhaften Tag
|
|
viele Bitten bekommt, will die stumm stellen koennen -- die
|
|
Antwort auf die EIGENE Bewerbung will niemand stumm stellen. Eine
|
|
gemeinsame Art hiesse: beides zusammen abschalten oder beides
|
|
zusammen ertragen. */
|
|
ok(neu2?.schluessel !== antwort?.schluessel,
|
|
"getrennt voneinander — die eine trifft den, der antwortet, die andere den, der wartet");
|
|
|
|
/* ---- WER ES ERFAEHRT ------------------------------------------- */
|
|
const wer = melder.entscheiderFuer({ id: idModi, name: "Marina" });
|
|
const rollen = wer.map((x) => x.rolle).sort();
|
|
ok(wer.length > 0, `die Bewerbung eines Modis erreicht ${wer.length} Leute`);
|
|
ok(rollen.includes("admin") && rollen.includes("hand"),
|
|
`DogFather und die rechte Hand sind dabei (${rollen.join(", ")})`);
|
|
/* DIE LINKE HAND NICHT -- sie darf verteilen, aber nicht ueber
|
|
Bewerbungen entscheiden. Eine Meldung ueber etwas, das man nicht
|
|
beantworten kann, ist Laerm. */
|
|
ok(!rollen.includes("linke"),
|
|
"die linke Hand nicht — sie entscheidet hier nicht mit");
|
|
/* UND DIE AGENTUR ERST RECHT NICHT. Ohne die Haeuser-Bedingung
|
|
erfuehre ein Manager ueber eine Push-Nachricht, dass es Team Dogi
|
|
ueberhaupt gibt. */
|
|
ok(!rollen.includes("manager") && !rollen.includes("creator"),
|
|
`niemand aus dem anderen Haus (${rollen.join(", ") || "niemand"})`);
|
|
/* Und der Bewerber nicht sich selbst. */
|
|
ok(!wer.some((x) => x.id === idModi), "und der Bewerber nicht sich selbst");
|
|
|
|
/* GEGENPROBE: Die Liste darf nicht einfach "alle Aktiven" sein --
|
|
sonst waere jede Zeile oben zufaellig richtig. */
|
|
const alleAktiven = (await import("node:sqlite")).DatabaseSync;
|
|
const d3 = new alleAktiven(process.env.WORKSPACE_DB, { readOnly: true });
|
|
const anzahl = d3.prepare("SELECT COUNT(*) AS n FROM personen WHERE aktiv = 1").get().n;
|
|
d3.close();
|
|
ok(wer.length < anzahl,
|
|
`Gegenprobe: es sind nicht einfach alle (${wer.length} von ${anzahl} Aktiven)`);
|
|
|
|
/* ---- WAS DRINSTEHT --------------------------------------------- */
|
|
const b1 = melder.bewerbungText({
|
|
name: "Marina", titel: "Die Regeln des Rudels einmal ganz lesen",
|
|
wort: "ich habe Freitag Zeit" });
|
|
ok(b1.titel === "Marina bewirbt sich", `der Titel nennt den Menschen (${b1.titel})`);
|
|
ok(b1.text.includes("Regeln des Rudels"), "und die Zeile darunter die Aufgabe");
|
|
/* DAS WORT IST OFT DIE GANZE AUSKUNFT. Wer es schon auf dem
|
|
Sperrbildschirm liest, kann entscheiden, ohne zu oeffnen. */
|
|
ok(b1.text.includes("ich habe Freitag Zeit"),
|
|
`samt dem Wort des Bewerbers (${b1.text})`);
|
|
|
|
const b2 = melder.bewerbungText({ name: "Marina", titel: "Kurz", wort: "" });
|
|
ok(!b2.text.includes("„"), `ohne Wort bleibt es sauber (${b2.text})`);
|
|
ok(b2.text === "Kurz", "und nur die Aufgabe steht da");
|
|
|
|
/* NAMENLOS DARF NICHT "undefined bewirbt sich" ergeben. */
|
|
const b3 = melder.bewerbungText({ name: "", titel: "X", wort: "" });
|
|
ok(b3.titel === "Jemand bewirbt sich", `ohne Namen steht ein Wort da (${b3.titel})`);
|
|
|
|
const a1 = melder.antwortText({ titel: "Die Regeln", ja: true, notiz: "mach das gern" });
|
|
const a2 = melder.antwortText({ titel: "Die Regeln", ja: false, notiz: "diesmal jemand anders" });
|
|
/* DAS ERGEBNIS STEHT IM TITEL -- man soll es lesen koennen, ohne zu
|
|
oeffnen. Auch die gute Nachricht. */
|
|
ok(/angenommen/.test(a1.titel), `die Zusage sagt es im Titel (${a1.titel})`);
|
|
ok(/nicht/.test(a2.titel) && a2.titel !== a1.titel,
|
|
`die Absage auch, und anders (${a2.titel})`);
|
|
/* DIE NOTIZ IST DER GRUND, WARUM FILIPE SIE VERLANGT HAT ("mit
|
|
einem text als notiz"). Sie hier wegzulassen hiesse, sie an genau
|
|
der Stelle zu verstecken, an der man sie liest. */
|
|
ok(a1.text.includes("mach das gern") && a2.text.includes("diesmal jemand anders"),
|
|
"und die Notiz steht in beiden");
|
|
|
|
/* LANGES WIRD GEKUERZT, ABER NICHT MITTEN IM WORT.
|
|
|
|
DIE ERSTE FASSUNG DIESER ZEILE WAR FALSCH und hat mich das
|
|
Richtige gelehrt: Sie nahm einen 68 Zeichen langen Titel und
|
|
erwartete eine Kuerzung bei 50 -- die gilt aber nur, wenn eine
|
|
Notiz danebensteht und Platz wegnimmt. Ohne Notiz darf der Titel
|
|
die ganzen 90 haben. Die Pruefung war rot, das Programm richtig.
|
|
Gemessen wird jetzt mit etwas, das JEDE Grenze reisst. */
|
|
const langerSatz = "Eine ganze Sendung neben einem Neuen sitzen und im Zweifel "
|
|
+ "eingreifen und danach noch drei Zeilen aufschreiben, damit am naechsten "
|
|
+ "Tag jemand weiss, was eigentlich los war";
|
|
const lang = melder.antwortText({ titel: langerSatz, ja: true, notiz: "" });
|
|
ok(lang.text.length < langerSatz.length && /…$/.test(lang.text),
|
|
`zu Langes endet mit Auslassung (${lang.text})`);
|
|
/* KEIN SCHNITT MITTEN IM WORT -- ein "eingrei…" liest sich wie ein
|
|
Fehler, nicht wie eine Kuerzung. */
|
|
ok(/\s…$/.test(lang.text) || /[a-zA-Zäöüß]{3,}\s?…$/.test(lang.text),
|
|
`und nicht mitten im Wort (${lang.text.slice(-24)})`);
|
|
/* GEGENPROBE: Kurzes bleibt unangetastet -- sonst bewiese die Zeile
|
|
darueber nur, dass immer gekuerzt wird. */
|
|
const kurzerSatz = melder.antwortText({ titel: "Kurzer Titel", ja: true, notiz: "" });
|
|
ok(kurzerSatz.text === "Kurzer Titel",
|
|
`Gegenprobe: Kurzes bleibt unangetastet (${kurzerSatz.text})`);
|
|
}
|
|
|
|
/* ---------- 7. Gegenprobe -------------------------------------------- */
|
|
console.log("\n=== 7. Gegenprobe: kann das hier ueberhaupt scheitern? ===\n");
|
|
{
|
|
/* Eine Vorlagennummer, die es nicht gibt. Kaeme dafuer ein 200,
|
|
wuerden die Haken oben nichts beweisen -- dann sagt der Server zu
|
|
allem ja. */
|
|
/* MIT DEM ZUGANG VON DOGFATHER, nicht mit dem des Modis: Seit dem
|
|
23.09. bekommt ein Modi auf dieser Route ohnehin 403 -- die
|
|
Gegenprobe haette also ab da nur noch bewiesen, dass die
|
|
Rechtepruefung greift, und nichts mehr ueber erfundene Nummern
|
|
gesagt. Genau so verliert eine Pruefung still ihren Sinn. */
|
|
const a = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ art: "katalog", nr: 9999, gruppe: "aufbau", person_id: idModi }),
|
|
});
|
|
ok(a.status === 400, `eine erfundene Vorlagennummer wird abgelehnt (${a.status})`);
|
|
const b = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ art: "katalog_alle", gruppe: "gibtesnicht", person_id: idModi }),
|
|
});
|
|
ok(b.status === 400, `und eine erfundene Etappe ebenso (${b.status})`);
|
|
/* Und eine erfundene Bewerbungsnummer. */
|
|
const c = await hole(handAn.keks, "/workspace/api/vorlagen/bewerbung/annehmen", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ id: 999999 }),
|
|
});
|
|
ok(c.status === 409, `eine erfundene Bewerbung ebenso (${c.status})`);
|
|
const e = await hole(modiAn.keks, "/workspace/api/vorlagen/bewerben", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ vorlage: "gibt-es-nicht" }),
|
|
});
|
|
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 */ }
|
|
process.exit(fehler ? 1 : 0);
|