Vorlagenbrett: Modis bewerben sich, DogFather und die rechte Hand entscheiden

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

WAS SICH AENDERT
----------------
Auf dem Vorlagenbrett steht fuer einen Modi jetzt "Bewerben" statt
"Uebernehmen". Wer sich beworben hat, sieht das an der Karte -- samt
dem Satz, WER antwortet, und einem Weg zurueck. DogFather und die
rechte Hand sehen die Bewerbung an derselben Karte, mit Namen und dem
Wort dazu, und daneben "Annehmen" und "Ablehnen". Beide fragen nach
einer Notiz.

"ALSO NUR" GILT AUCH AM SERVER, nicht nur im Browser: Die alte Tuer
antwortet einem Modi mit 403 und dem Satz, was stattdessen geht. Ein
ausgeblendeter Knopf ist eine Bitte, abgelehnt wird in der Route.

DIE LINKE HAND STEHT ABSICHTLICH NICHT BEI DEN ENTSCHEIDERN
------------------------------------------------------------
Sie gehoert seit dem 22.09. ueberall dazu ("ich will dass die linke
hand auch ueberall zu sehen ist"). Hier hat Filipe genau zwei genannt.
Das ist keine Vergesslichkeit von mir, sondern seine Aufzaehlung -- und
dieselbe Grenze zieht das Haus schon bei den Aufgaben-Bewerbungen
(entscheidetUeberAufgaben). Sie darf weiter VERTEILEN; das hat er nicht
angefasst.

Sie ist deshalb die schaerfste Probe in der Pruefung: Wer statt "darf
entscheiden" nur "darf verteilen" abfragt, laesst sie mitentscheiden --
und niemandem faellt es auf, weil alles funktioniert.

DIE AUFGABE ENTSTEHT ERST MIT DER ZUSAGE
----------------------------------------
Der naheliegende Weg waere gewesen, beim Bewerben gleich die Aufgabe
anzulegen und die vorhandene Bewerbung aus aufgaben_zuteilung
daranzuhaengen. Dann stuende nach zwoelf Absagen zwoelfmal Arbeit auf
dem Brett, die niemand bestellt hat -- und um das einzufangen, muesste
das Ablehnen Aufgaben LOESCHEN. Loeschen als Nebenwirkung einer Absage
ist genau die Sorte Regel, die irgendwann das Falsche trifft.

Also eine eigene, kleine Tabelle (vorlagen_bewerbungen). Bis jemand ja
sagt, gibt es nur eine Zeile. Die Woerter sind dieselben wie drueben
(zustand, entscheid_text, entschieden_von) -- zwei Namen fuer dieselbe
Sache waeren zwei Sprachen im selben Haus.

Und die Zusage legt die Aufgabe ueber DIESELBE Funktion an wie das
Uebernehmen (katalogAufgabeAnlegen, neu, aus dem Katalog-Zweig
herausgeloest). Damit sieht eine erbetene Aufgabe aus wie eine
verteilte: gleiche Frist, gleiche Kategorie, gleiche Kennung. Ein
zweiter Weg waere ein zweiter Satz Regeln.

KLEINIGKEITEN, DIE SONST WEHTUN
-------------------------------
  * "Alle 12 uebernehmen" gibt es nur fuer die, die verteilen. Ein
    "Alle bewerben" waere der schnellste Weg, zwoelf Bitten auf einmal
    loszuschicken -- und damit zwoelf Entscheidungen fuer jemand anderen.
  * Wer eine Aufgabe schon hat, bekommt keinen Bewerben-Knopf. Der
    Server lehnt das ohnehin ab; ein Knopf, der eine Absage holt, ist
    schlimmer als keiner.
  * Nach einer Absage darf man sich wieder bewerben. Der eindeutige
    Index gilt deshalb nur fuer OFFENE Bewerbungen -- ueber alle
    Zustaende waere eine Absage ein Bann.
  * Gesucht wird ueber den SCHLUESSEL der Vorlage, nicht ueber die
    Nummer in der Liste. Die Nummer verschiebt sich, sobald jemand eine
    Vorlage einfuegt -- genau dieser Fehler ist am 16.09. schon einmal
    passiert.

GEPRUEFT
--------
pruef-modi-katalog: 95 Pruefungen, 0 Fehler (vorher 49).

Die Pruefung ist beim Umbau ROT geworden -- 9 Zeilen, alle dort, wo ein
Modi sich selbst etwas nahm. Richtig so, sie hat die Aenderung bemerkt.
Sie steht jetzt auf dem neuen Weg und misst ihn ganz:

  * der Modi kommt an die alte Tuer nicht mehr heran (403, erst_bewerben)
  * die Bewerbung legt NOCH KEINE Aufgabe an
  * die linke Hand darf verteilen, aber nicht entscheiden (403)
  * der Bewerber selbst erst recht nicht (403)
  * die Zusage erzeugt die Aufgabe -- mit Kategorie, Frist, Besitzer
  * die Notizen stehen in der Datenbank, samt WER entschieden hat
    (direkt gelesen: ein Feld, das der Server annimmt und nirgends
    speichert, saehe von aussen genauso aus)
  * nach einer Absage geht es wieder
  * am Bildschirm: alle Knoepfe heissen "Bewerben", kein einziger
    "Uebernehmen" mehr, die wartende Karte nennt, wer antwortet --
    und DogFather klickt sich durch Annehmen samt Notizfeld, bis die
    Aufgabe auf dem Brett steht

Die Gegenprobe in Abschnitt 7 lief mit dem Zugang des Modis und haette
ab heute nur noch bewiesen, dass die Rechtepruefung greift -- sie
benutzt jetzt DogFather. Genau so verliert eine Pruefung still ihren
Sinn.

pruef-aufgaben-vorlagen: unveraendert gruen.

ZWEI FUNDE NEBENHER, BEIDE AELTER ALS DIESE AENDERUNG -- gemessen, nicht
vermutet (mit gestashten Aenderungen gegengeprueft):
  * pruef-modi-wortleck ist seit dem 22.09. rot: Der Rollenname steht
    in team.css und teamlage.js, also in Dateien, die jeder bekommt.
  * pruef-zuteilung stuerzt seit laengerem ab (#neu-oeffnen ist
    verborgen). Beides kommt als naechstes, getrennt.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-23 10:59:51 +02:00
co-authored by Claude Opus 5
parent 256ea6f7c0
commit e736a12cff
45 changed files with 1652 additions and 649 deletions
+370 -28
View File
@@ -76,6 +76,14 @@ 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;
@@ -100,6 +108,10 @@ const modiAn = await anmelden("creator", "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("creator", "CODE-HAND-0001");
const linkeAn = await anmelden("creator", "CODE-LINK-0001");
/* ---------- 1. Ist er vollstaendig da? ------------------------------- */
console.log("\n=== 1. Der Katalog, wie ihn das Dokument beschreibt ===\n");
@@ -196,42 +208,223 @@ for (const [name, an] of [["Manager", manAn], ["Creator", creatAn]]) {
"und der Modi bekommt umgekehrt den Creator-Katalog nicht");
}
/* ---------- 3. Uebernehmen ------------------------------------------- */
console.log("\n=== 3. Aus der Vorlage wird eine echte Aufgabe ===\n");
/* ---------- 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];
const a = await hole(modiAn.keks, "/workspace/api/vorlagen/uebernehmen", {
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(a.status === 200 && a.daten?.angelegt === 1, `übernommen (${a.status})`);
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 b = await hole(modiAn.keks, "/workspace/api/aufgaben");
const neu = (b.daten?.aufgaben || []).find((x) => x.vorlage === vorlage.schluessel);
ok(!!neu, `sie liegt auf seinem Brett (${neu?.titel || "nicht gefunden"})`);
/* DIE KATEGORIE IST DER HALBE WERT DER VORLAGE. Ginge sie unterwegs
verloren, muesste man 60-mal von Hand einsortieren, was im Dokument
danebensteht -- und niemand merkte es, weil die Aufgabe ja da ist. */
/* DIE KATEGORIE IST DER HALBE WERT DER VORLAGE -- und sie heisst im
ausgelieferten Katalog `gruppe`, weil die Oberflaeche damit
zeichnet. Ginge sie unterwegs verloren, muesste man von Hand
einsortieren, was danebensteht. */
ok(neu?.kategorie === vorlage.gruppe,
`mit ihrer Kategorie (${neu?.kategorie} / erwartet ${vorlage.gruppe})`);
ok(!!neu?.frist, `und mit Frist (${neu?.frist})`);
ok(neu?.verantwortlich_id === idModi, "und er selbst ist verantwortlich");
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})`);
}
/* Eine ganze Etappe auf einmal. */
/* ---------- 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");
{
const wieViele = katalog.aufgaben.filter((v) => v.gruppe === "technik").length;
const a = await hole(modiAn.keks, "/workspace/api/vorlagen/uebernehmen", {
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({ art: "katalog_alle", gruppe: "technik" }),
body: JSON.stringify({ id: bewerbungsNr, kommentar: "mach ich" }),
});
ok(a.status === 200 && a.daten?.angelegt === wieViele,
`eine ganze Kategorie auf einmal: ${a.daten?.angelegt} von ${wieViele}`);
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 -------------------- */
@@ -406,6 +599,60 @@ console.log("\n=== 6. Das Brett im echten Browser ===\n");
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.
@@ -528,6 +775,85 @@ console.log("");
ok(danach.karten === vorher + 1,
"die Aufgabe liegt auf dem Brett (" + vorher + " -> " + danach.karten + ")");
ok(danach.fuerMarina > 0, "und zwar bei Marina (" + danach.fuerMarina + ")");
/* ==== 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 ---------------------- */
const vorherK = await seite.evaluate(() => document.querySelectorAll(".karte").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(".karte").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})`);
ok(konsole.length === 0,
konsole.length ? "Fehler in der Konsole: " + konsole[0].slice(0, 80)
: "keine Fehler in der Konsole");
@@ -540,16 +866,32 @@ 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. */
const a = await hole(modiAn.keks, "/workspace/api/vorlagen/uebernehmen", {
/* 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" }),
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(modiAn.keks, "/workspace/api/vorlagen/uebernehmen", {
const b = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", {
method: "POST", headers: jsonKopf,
body: JSON.stringify({ art: "katalog_alle", gruppe: "gibtesnicht" }),
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})`);
}
console.log(`\n${fehler ? "FEHLGESCHLAGEN" : "BESTANDEN"} -- `