Aus der Probe wird ein Zugang - und die Seite sagt nicht mehr "Agentur"

Die drei offenen Punkte aus Abschnitt 9 des Plans, plus Filipes zweite
Ansage: "auf dieser seite soll nichts stehen von agentur, diese seite
hier ist nur fuer mich meine modis und meine community."

1. PROBE -> ZUGANG (ein Knopf statt eines Satzes)

Auf der Stufe "Probe" stand bisher "Zugang in Personen & Zugaenge
anlegen, Rolle Modi" - und wer das vergass, hatte eine Karte auf "Im
Team" und keinen Menschen darin. Jetzt haengt das Anlegen am Schritt
selbst.

Ueber DENSELBEN Weg wie in "Personen & Zugaenge", nicht ueber einen
zweiten: POST /workspace/api/verwaltung/personen, mit ihrer
Rechtepruefung, ihrem Protokolleintrag und ihrem genau einmal
angezeigten Code. Der Talente-Server legt selbst keinen einzigen Zugang
an, und die Pruefung zaehlt nach, dass es bei einem Weg bleibt.

Der Code steht im gleichen Kasten wie dort - die Gestaltung ist aus
personen.css nach start.css umgezogen, weil talente.html sie sonst nicht
laedt. Ein Geheimnis sieht im ganzen Haus gleich aus. Er steht
ausserhalb der Liste, sonst waere er in dem Moment weg, in dem er
entsteht: wenn die Karte auf "Im Team" springt.

Die rechte Hand bekommt an dieser Stelle KEINEN Knopf, sondern einen
Satz. Ein Knopf, der ihr jedes Mal "darfst du nicht" antwortet, waere
schlechter als gar keiner - er verspricht etwas.

2. DIE EIGENE KARTE (Abschnitt "Deine Karte")

Wer beschrieben wird, darf es lesen - aber erst, wenn ALLE gesetzt
haben (sonst waere die erste Einschaetzung eine Vorgabe fuer die
zweite), und ohne Namen und ohne Anlasstext. Gemessen in beide
Richtungen: vorher nicht sichtbar, nachher sichtbar.

3. DER VORLAGENTEXT

Gebaut aus den angeklickten Merkmalen, in einem Feld zum Aendern, nicht
zum Abschicken. Ohne Merkmale steht auch keines drin.

4. "GILT FUER" SAGT DER SERVER

In bereich.js stand woertlich "Agentur" - richtig auf der
Agenturadresse, falsch auf jeder anderen. Jetzt liefert der Server
`gehoert` ("Der Treff" / "Team Dogi" / "Agentur"); gemessen mit einem
einzigen Menschen auf zwei Adressen.

PRUEFUNGEN: treff 58 -> 66, nachwuchs 69 -> 109, neue-seiten 70 -> 93.
Die Katalogseiten werden jetzt auch mit den Augen der rechten Hand
angesehen - ohne das waere "Deine Karte" nie auf einem Bildschirm
gewesen. Und pruef-neue-seiten wartet nicht mehr 900 ms, sondern bis
sich der Text nicht mehr aendert: Die Karte stand da und wurde
trotzdem als fehlend gemeldet, weil zu frueh gelesen wurde.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-11 23:13:50 +02:00
co-authored by Claude Opus 5
parent 44e5ace109
commit bba3642664
41 changed files with 1447 additions and 420 deletions
+194 -1
View File
@@ -20,7 +20,7 @@
Aufrufen mit: node server/pruef-nachwuchs.mjs
===================================================================== */
import { mkdtempSync, rmSync } from "node:fs";
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { request as httpAnfrage } from "node:http";
@@ -454,6 +454,199 @@ melde("=== 8. Standzeit ===");
`einen Tag vor der Frist noch nicht (${r2.kandidaten[0].tage} Tage)`);
}
/* =======================================================================
9. DER VORLAGENTEXT
Er ist der Unterschied zwischen "Ansprechen" als Aufgabe und
"Ansprechen" als erledigter Sache. Geprueft wird deshalb nicht, DASS
ein Text kommt, sondern dass er aus den angeklickten Merkmalen
gebaut ist -- sonst waere es eine Floskel mit eingesetztem Namen.
======================================================================= */
melde("");
melde("=== 9. Der Vorlagentext ===");
{
const zwei = [tal.TALENT_ALLE[0], tal.TALENT_ALLE[1]];
const text = tal.ansprechtext("Kessi aus dem Chat", zwei.map((m) => m.schluessel));
ok(text.startsWith("Hey Kessi,"),
"die Anrede nimmt nur den ersten Namensteil, nicht den ganzen Eintragstitel");
ok(zwei.every((m) => text.includes(m.du)),
"beide angeklickten Merkmale stehen woertlich im Text");
ok(/kein Druck|nein/.test(text),
"und der Ausstieg steht drin -- ein Angebot ohne Nein ist keines");
/* GEGENPROBE: ohne ein einziges Merkmal darf KEINES drinstehen --
sonst baut die Funktion sich etwas zusammen, das niemand beobachtet
hat. Das waere die schlimmste Sorte Vorlage. */
const leer = tal.ansprechtext("Kessi", []);
ok(!tal.TALENT_ALLE.some((m) => leer.includes(m.du)),
"ohne angeklickte Merkmale steht auch keines im Text");
ok(leer.length > 100, `aber ein vollstaendiger Text bleibt es (${leer.length} Zeichen)`);
ok(leer !== text, "und er ist ein anderer als der mit Merkmalen");
/* HAUSREGEL: In Text, den ein Mensch liest, stehen echte Umlaute.
"Haettest du Lust" war genau der Fehler, den diese Zeile findet. */
const beide = text + " " + leer;
ok(!/\b(fuer|koenn|haett|muess|ueber|waer|moecht)/i.test(beide),
"keine ASCII-Ersatzumlaute in einem Text, den ein Mensch liest");
ok(/\b(fuer)/i.test("das ist fuer dich"), "und der Sucher findet so einen auch wirklich");
/* UND ER KOMMT UEBER DIE LEITUNG AN, nicht nur aus dem Modul. */
const lage = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi }));
const k = (lage.kandidaten || [])[0];
ok(typeof k?.ansprechtext === "string" && k.ansprechtext.length > 100,
"die Karte bringt ihren Text mit");
const gesetzt = (k?.merkmale || []).map((s) => tal.TALENT_NACH_SCHLUESSEL.get(s)?.du).filter(Boolean);
ok(gesetzt.length > 0 && gesetzt.every((d) => k.ansprechtext.includes(d)),
`und zwar aus ihren ${gesetzt.length} gesetzten Merkmalen`);
}
/* =======================================================================
10. DIE EIGENE KARTE
Sie ist die Gegenprobe zur ganzen Kategorie: Ueber Menschen wird hier
aufgeschrieben, und wer beschrieben wird, darf es lesen. Aber erst,
wenn ALLE gesetzt haben -- sonst waere die erste Einschaetzung eine
Vorgabe fuer die zweite -- und ohne Namen und ohne Anlasstext.
======================================================================= */
melde("");
melde("=== 10. Die eigene Karte ===");
{
const meine = json(await roh("/workspace/api/entwicklung/meine-karte", CREW, { cookie: kModi }));
ok(meine.hat_karte === true, "der Modi hat eine eigene Karte");
ok(meine.beurteiler === 2,
`zwei Augenpaare urteilen ueber ihn (${meine.beurteiler}) -- er selbst zaehlt nicht mit`);
const punkte = (meine.bloecke || []).flatMap((b) => b.punkte || []);
ok(punkte.length > 0, `${punkte.length} Punkte in seiner Karte`);
/* Aus Abschnitt 4: DIESEN Punkt haben beide gesetzt (laeuft/waechst). */
const beide = punkte.find((p) => p.schluessel === kat.ENTWICKLUNG_FREMD[0].schluessel);
ok(beide && beide.offen === false, "ein Punkt, den beide gesetzt haben, ist offen");
ok(beide?.antworten?.length === 2,
`mit beiden Antworten nebeneinander (${(beide?.antworten || []).map((a) => a.name).join(" / ")})`);
ok(beide?.einig === false, "und dem ehrlichen Hinweis, dass sie sich uneinig sind");
/* GEGENPROBE 1 -- und ohne sie beweist die Zeile darueber nichts:
Einen Punkt, den erst EINER gesetzt hat, bekommt er NICHT. */
const einer = punkte.find((p) => p.schluessel === kat.ENTWICKLUNG_FREMD[1].schluessel);
ok(einer && einer.offen === true,
"einen Punkt, den erst einer gesetzt hat, sieht er noch nicht");
ok(einer && einer.antworten === undefined,
"und die Antwort des einen reist dabei auch nicht heimlich mit");
/* GEGENPROBE 2: sobald die zweite dazukommt, oeffnet er sich. */
const nach = await roh(`/workspace/api/entwicklung/person/${idModi}/${kat.ENTWICKLUNG_FREMD[1].schluessel}`,
CREW, { cookie: kHand }, { stufe: "hakt", anlass: "Zweimal zu spaet zur Schicht." }, "PUT");
ok(nach.code === 200, `die rechte Hand setzt denselben Punkt (${nach.code})`);
const meine2 = await roh("/workspace/api/entwicklung/meine-karte", CREW, { cookie: kModi });
const jetztOffen = (json(meine2).bloecke || []).flatMap((b) => b.punkte || [])
.find((p) => p.schluessel === kat.ENTWICKLUNG_FREMD[1].schluessel);
ok(jetztOffen?.offen === false && jetztOffen?.antworten?.length >= 1,
"danach steht er in seiner Karte");
/* KEINE NAMEN UND KEIN ANLASS. Beides steht in der Karte, die
DogFather sieht -- und beides waere hier ein Vertrauensbruch:
"wer hat das ueber mich gesagt" macht aus einer Entwicklung eine
Abrechnung. */
const roh2 = meine2.text;
ok(!/Filipe|Rieke/.test(roh2), "kein Name eines Beurteilers in der ganzen Antwort");
ok(!/zu spaet zur Schicht|Am Samstag/.test(roh2), "und kein einziger Anlasstext");
ok(/hakt|laeuft|waechst/.test(roh2),
"die Antworten selbst kommen aber an -- sonst waere die Zeile darueber trivial");
/* Block 5 gehoert der Person -- er steht in IHRER Selbstauskunft,
nicht in der Fremdkarte. Waere er hier, stuenden Antworten
anderer auf Fragen, die nur sie sich stellt. */
const selbst = kat.ENTWICKLUNG_SELBST.map((p) => p.schluessel);
ok(!punkte.some((p) => selbst.includes(p.schluessel)),
"Block 5 taucht in der eigenen Karte nicht als Fremdurteil auf");
/* Wer gar nicht beurteilt wird, bekommt auch keine leere Karte
vorgesetzt -- eine Karte ohne Inhalt liest sich wie ein Versaeumnis. */
/* UND DIE RECHTE HAND HAT AUCH EINE. Ueber sie urteilt nur DogFather
-- sie selbst zaehlt nicht mit, sonst waere ihre Karte erst dann
vollstaendig, wenn sie sich selbst eingeschaetzt hat. */
const ihre = json(await roh("/workspace/api/entwicklung/meine-karte", CREW, { cookie: kHand }));
ok(ihre.hat_karte === true, `auch die rechte Hand hat eine eigene Karte (${JSON.stringify(ihre).slice(0, 90)})`);
ok(ihre.beurteiler === 1, `ueber sie urteilt genau einer (${ihre.beurteiler})`);
const chef = json(await roh("/workspace/api/entwicklung/meine-karte", HAUS, { cookie: kDogi }));
ok(chef.hat_karte === false, "DogFather selbst hat keine -- ueber ihn urteilt hier niemand");
}
/* =======================================================================
11. AUS DER PROBE WIRD EIN ZUGANG
Der letzte Schritt des Trichters ist kein Schritt, sondern eine
Entscheidung. Bis heute stand dort ein Satz ("Zugang in Personen &
Zugaenge anlegen"), und wer ihn vergass, hatte eine Karte auf "Im
Team" und keinen Menschen darin.
GEPRUEFT WIRD VOR ALLEM EINES: dass es dafuer keinen ZWEITEN Weg
gibt. Ein Zugang entsteht im ganzen Haus an genau einer Stelle.
======================================================================= */
melde("");
melde("=== 11. Probe → Zugang ===");
{
const letzte = tal.TALENT_STUFEN.at(-1);
ok(!/Personen/.test(letzte.schritt || ""),
"die letzte Stufe schickt niemanden mehr von Hand in die Personenverwaltung");
/* EIN EINZIGER WEG, und es ist der vorhandene. */
const js = readFileSync(new URL("../workspace/assets/js/talente.js", import.meta.url), "utf8");
/* OHNE DIE KOMMENTARE gezaehlt: Der Weg wird daneben auch erklaert,
und eine Erklaerung ist kein zweiter Weg. */
const jsCode = js.replace(/\/\*[\s\S]*?\*\//g, "");
const wege = jsCode.split("/workspace/api/verwaltung/personen").length - 1;
ok(wege === 1, `talente.js kennt genau einen Weg zum Anlegen (${wege})`);
ok(js.split("/workspace/api/verwaltung/personen").length - 1 > wege,
"und der Kommentar daneben nennt ihn beim Namen -- sonst zaehlt die Zeile darueber nichts weg");
const eigen = readFileSync(new URL("../server/workspace-entwicklung.js", import.meta.url), "utf8");
ok(!/personAnlegen|INSERT INTO personen/.test(eigen),
"und der Talente-Server legt selbst keinen einzigen Zugang an");
/* DIE RECHTE HAND SIEHT DEN TRICHTER, VERGIBT ABER KEINE ZUGAENGE. */
const hand = await roh("/workspace/api/verwaltung/personen", CREW, { cookie: kHand },
{ name: "Heimlich", rolle: "modi" });
ok(hand.code === 404, `die rechte Hand kommt am Anlegen nicht vorbei (${hand.code})`);
const einModi = await roh("/workspace/api/verwaltung/personen", CREW, { cookie: kModi },
{ name: "Heimlich", rolle: "modi" });
ok(einModi.code === 404, `ein Modi erst recht nicht (${einModi.code})`);
/* GEGENPROBE: DogFather kann es -- sonst waere "404" oben auch dann
gruen, wenn der Weg gar nicht existiert. */
const neu = await roh("/workspace/api/verwaltung/personen", HAUS, { cookie: kDogi },
{ name: "Kessi", rolle: "modi" });
ok(neu.code === 201, `DogFather legt den Zugang an (${neu.code})`);
const person = json(neu);
ok(person.rolle === "modi", `und zwar als Modi (${person.rolle})`);
ok(typeof person.code === "string" && person.code.length > 8,
"der Zugangscode kommt genau einmal zurueck");
/* UND DANACH IST ER WEG. Nirgends abrufbar -- auch nicht fuer den,
der ihn angelegt hat. */
const liste = await roh("/workspace/api/verwaltung/personen", HAUS, { cookie: kDogi });
ok(liste.code === 200 && !liste.text.includes(person.code),
"danach steht er in keiner Liste mehr");
/* Erst jetzt wandert die Karte -- in derselben Reihenfolge wie in der
Oberflaeche: erst der Mensch, dann der Haken. */
const stufe = await roh(`/workspace/api/talente/${idTalent}/stufe`, HAUS,
{ cookie: kDogi }, { stufe: "imteam" }, "PUT");
ok(stufe.code === 200, `und die Karte geht auf "Im Team" (${stufe.code})`);
const lage = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi }));
const k = (lage.kandidaten || []).find((x) => x.id === idTalent);
ok(k?.stufe === "imteam" && k?.knopf === null,
"dahinter kommt kein weiterer Knopf -- der Trichter ist zu Ende");
/* Der neue Mensch ist wirklich da, mit allem, was daran haengt. */
const dd = new DatabaseSync(process.env.WORKSPACE_DB);
const z = dd.prepare("SELECT rolle, aktiv FROM personen WHERE name = 'Kessi'").get();
dd.close();
ok(z?.rolle === "modi" && z?.aktiv === 1, "und er steht als aktiver Modi in der Datenbank");
}
/* ------------------------------------------------------------------- */
melde("");
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);