Talente: der Trichter bekommt einen Ausgang

Gemessen vor dem Bau: talentNaechste kettet aufgefallen -> beobachtet ->
angesprochen -> probe -> imteam, und danach kommt nichts. Eine Suche nach
"beenden|abgesagt|verworfen|archiv" in workspace-entwicklung.js fand
nichts. Der Trichter hatte genau einen Ausgang -- obwohl der Text der
Probe-Stufe woertlich verspricht: "Nach 30 Tagen: uebernehmen -- der
Zugang entsteht dabei -- oder sauber beenden."

Wer nicht passte, blieb also auf seiner Stufe stehen, mit wachsender
Standzeit und rotem "liegt"-Zeichen. Nach einem Jahr hat die Liste mehr
Karteileichen als Kandidaten, und dann schaut niemand mehr hin.

DREI SPALTEN, KEINE NEUE STUFE. Eine neue Stufe haette einen
CHECK-Umbau auf laufenden Daten verlangt -- und eine Auskunft zerstoert:
Die Stufe haelt fest, WO es geendet hat. "Beendet in der Probe" und
"beendet nach dem ersten Hinsehen" sind zwei verschiedene Saetze, und
genau der Unterschied ist die Frage, wenn dieselbe Person in einem
halben Jahr wieder auffaellt.

DER SATZ IST PFLICHT (10 Zeichen, wie beim Ablehnen einer Anfrage) --
aus zwei Richtungen: Nach vorn, weil beim zweiten Anlauf niemand mehr
weiss, was damals war. Nach innen, weil wer schreiben muss, anders
entscheidet als wer klickt. "Passt nicht" hat neun Zeichen.

WIEDER AUFNEHMEN setzt auf "aufgefallen" zurueck -- Menschen aendern
sich, ein Urteil von damals gilt nicht weiter -- und rettet den alten
Satz in die Notiz, statt ihn zu loeschen.

AM BILDSCHIRMFOTO GEFUNDEN, nicht an einer Zahl: Auf einer Karte auf
"Im Team" stand "Nichts mehr zu tun -- der Zugang steht" und direkt
darunter "Passt doch nicht?". Ein Beenden dort haette die Karte
stillgelegt und den ZUGANG offengelassen -- eine halbe Handlung, die
sich wie eine ganze anfuehlt. Jetzt steht dort der Weg zu "Personen &
Zugaenge", und der Server weist es mit 409 ab.

pruef-nachwuchs 152 -> 200, davon 21 im echten Browser (der erste
Browserblick auf diese Seite ueberhaupt -- mit einer Messung, ob Name
und Datum bei 390 px uebereinanderliegen, statt einer Zahl im CSS).
Sechs Gegenproben, jede genau auf ihr Ziel:
  Grenze auf 0          -> 3 rot
  Beendete bleiben drin -> 2 rot
  beendete: []          -> 5 rot
  alter Grund verworfen -> 1 rot
  Serverriegel imteam   -> 5 rot
  Browserzweig imteam   -> 3 rot
Dazu gruen: css-klassen, struktur, entwicklung, uebergang.

Nebenbefund behoben: absage() im Browser warf einen erklaerenden Satz
des Servers lautlos weg und zeigte "Ging nicht." -- die Auskunft war da.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-17 18:39:06 +02:00
co-authored by Claude Opus 5
parent 3a49e30aa4
commit e54c6829f1
37 changed files with 1269 additions and 406 deletions
+317 -1
View File
@@ -49,6 +49,13 @@ process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):"
let fehler = 0, geprueft = 0;
const melde = (t) => console.log(t);
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
/* DER DRITTE AUSGANG: nicht in Ordnung ist etwas anderes als "konnte
nicht nachsehen". Ohne ihn wuerde ein fehlendes Playwright entweder
als Fehler gemeldet (falscher Alarm) oder stillschweigend
uebersprungen (schlimmer -- die Zahl saenke, und niemand wuesste
warum). */
let offen = 0;
const unklar = (t) => { offen++; console.log(" -- " + t); };
const CREW = "crew.dogfather-universe.com";
const HAUS = "workspace.dogfather-universe.com";
@@ -920,9 +927,318 @@ melde("=== 13. Die Probezeit ===");
}
}
/* =======================================================================
14. DER AUSGANG AUS DEM TRICHTER (17.09.2026)
Filipe: „alles moegliche, wie gesagt ich will hoch profissionel
arbeiten."
Gemessen vor dem Bau: talentNaechste kettet aufgefallen -> beobachtet
-> angesprochen -> probe -> imteam, und danach kommt nichts. Ein
Kandidat, der nicht passt, blieb auf seiner Stufe stehen -- mit
wachsender Standzeit und rotem "liegt"-Zeichen -- obwohl der Text der
Stufe woertlich verspricht: "uebernehmen ... oder sauber beenden".
Geprueft wird hier DREIERLEI, und der dritte Punkt ist der, wegen dem
es die Spalten und nicht eine neue Stufe geworden ist:
1. Der Satz ist Pflicht -- und die Grenze beisst wirklich.
2. Beendete verschwinden aus der Arbeitsliste, nicht aus dem Haus.
3. Die STUFE bleibt stehen: Man sieht, WO es geendet hat.
======================================================================= */
melde("");
melde("=== 14. Der Ausgang ===");
{
/* Jonas steht aus Abschnitt 13 in der Probe -- genau der interessante
Fall: Wer in der Probe aussteigt, hat eine andere Geschichte als
wer nach dem ersten Hinsehen aussteigt. */
const vorher = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi }));
const jonas = (vorher.kandidaten || []).find((k) => k.titel === "Jonas aus dem Chat");
ok(!!jonas && jonas.stufe === "probe",
`Jonas steht in der Arbeitsliste, Stufe ${jonas?.stufe}`);
const stufeVorher = (vorher.trichter || []).find((x) => x.schluessel === "probe")?.anzahl;
/* ---- Der Satz ist Pflicht ---- */
{
const r = await roh(`/workspace/api/talente/${jonas.id}/beenden`, HAUS,
{ cookie: kDogi }, { grund: "" }, "PUT");
ok(r.code === 400, `ohne Satz geht es nicht (${r.code})`);
ok((json(r).fehler || "").length > 20,
"und es steht dabei, warum der Satz verlangt wird");
}
{
/* DIE GRENZE BEISST WIRKLICH. "Passt nicht" hat neun Zeichen -- die
haeufigste Nicht-Begruendung ueberhaupt, und genau die soll
auffallen. Ohne diese Zeile wuesste man nur, dass ein LEERES Feld
abgewiesen wird, und das ist eine andere, viel schwaechere
Aussage. */
const r = await roh(`/workspace/api/talente/${jonas.id}/beenden`, HAUS,
{ cookie: kDogi }, { grund: "passt nix" }, "PUT");
ok(r.code === 400, `neun Zeichen sind noch kein Satz (${r.code})`);
}
/* ---- Beenden ---- */
const GRUND = "Hat sich nach der zweiten Schicht nicht mehr gemeldet.";
{
const r = await roh(`/workspace/api/talente/${jonas.id}/beenden`, HAUS,
{ cookie: kDogi }, { grund: GRUND }, "PUT");
ok(r.code === 200 && json(r).beendet === true, `mit Satz geht es (${r.code})`);
}
{
const l = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi }));
ok(!(l.kandidaten || []).some((k) => k.id === jonas.id),
"er steht nicht mehr in der Arbeitsliste");
const b = (l.beendete || []).find((k) => k.id === jonas.id);
ok(!!b, "aber er ist da -- unter den Beendeten");
ok(b?.beendet?.grund === GRUND, `der Satz steht dabei (${b?.beendet?.grund})`);
ok(b?.beendet?.von === "Filipe", `und wer es war (${b?.beendet?.von})`);
/* DER EIGENTLICHE PUNKT: Die Stufe blieb stehen. Waere daraus ein
eigener Stufenwert "beendet" geworden, stuende hier "beendet" --
und die Frage "wie weit war er gekommen?" waere fuer immer
unbeantwortbar. */
ok(b?.stufe === "probe",
`und WO es geendet hat: ${b?.stufe_name} -- nicht bloss "beendet"`);
ok(b?.liegt === false,
"er liegt nicht mehr -- eine beendete Karte wird nicht rot");
const nachher = (l.trichter || []).find((x) => x.schluessel === "probe")?.anzahl;
ok(nachher === stufeVorher - 1,
`der Trichter zaehlt ihn nicht mehr mit (${stufeVorher} -> ${nachher})`);
}
/* ---- Wieder aufnehmen ---- */
{
const r = await roh(`/workspace/api/talente/${jonas.id}/wieder`, HAUS,
{ cookie: kDogi }, null, "PUT");
ok(r.code === 200, `wieder aufnehmen geht (${r.code})`);
const l = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi }));
const k = (l.kandidaten || []).find((x) => x.id === jonas.id);
ok(!!k, "er steht wieder in der Arbeitsliste");
ok(k?.stufe === "aufgefallen",
`und faengt vorne an (${k?.stufe}) -- ein Jahr altes Urteil gilt nicht weiter`);
/* DER SATZ VON DAMALS BLEIBT. Er ist beim zweiten Anlauf das
Wertvollste, was man hat -- und genau er wuerde beim Zuruecksetzen
am leichtesten verschwinden. */
ok((k?.notiz || "").includes(GRUND),
"der Grund von damals steht in der Notiz");
ok(!k?.beendet, "und die Karte ist nicht mehr als beendet gezeichnet");
/* Die Probe faengt neu an -- alte Haken wuerden eine Probezeit
beschreiben, die vier Wochen zurueckliegt. */
ok(!(l.beendete || []).some((x) => x.id === jonas.id),
"aus der Sammlung der Beendeten ist er heraus");
}
{
const r = await roh(`/workspace/api/talente/${jonas.id}/wieder`, HAUS,
{ cookie: kDogi }, null, "PUT");
ok(r.code === 409, `wer laeuft, kann nicht wieder aufgenommen werden (${r.code})`);
}
/* ---- Wer im Team ist, wird nicht hier beendet ----
Gefunden am Bildschirmfoto: Auf einer Karte auf "Im Team" stand
"Nichts mehr zu tun -- der Zugang steht" und direkt darunter
"Passt doch nicht?". Ein Beenden dort haette die KARTE stillgelegt
und den ZUGANG offengelassen -- und das ist schlimmer als gar kein
Ausgang, weil man glaubt, gehandelt zu haben. */
{
const l = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi }));
const drin = (l.kandidaten || []).find((k) => k.stufe === "imteam");
ok(!!drin, `es gibt einen im Team zum Gegenprobieren (${drin?.titel})`);
const r = await roh(`/workspace/api/talente/${drin.id}/beenden`, HAUS,
{ cookie: kDogi }, { grund: GRUND }, "PUT");
ok(r.code === 409, `wer im Team ist, wird nicht hier beendet (${r.code})`);
ok((json(r).fehler || "").includes("Zug"),
`und es steht dabei, wo stattdessen (${(json(r).fehler || "").slice(0, 40)}...)`);
const l2 = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi }));
ok((l2.kandidaten || []).some((k) => k.id === drin.id),
"er steht unveraendert in der Liste -- nichts halb passiert");
}
/* ---- Und die Gegenproben ---- */
{
const r = await roh("/workspace/api/talente/999999/beenden", HAUS,
{ cookie: kDogi }, { grund: GRUND }, "PUT");
ok(r.code === 404, `eine erfundene Nummer wird abgewiesen (${r.code})`);
}
{
const r = await roh(`/workspace/api/talente/${jonas.id}/beenden`, CREW,
{ cookie: kModi }, { grund: GRUND }, "PUT");
ok(r.code === 404,
`ein Modi kann niemanden beenden -- und erfaehrt nicht, dass es das gibt (${r.code})`);
}
{
const r = await roh(`/workspace/api/talente/${jonas.id}/wieder`, CREW,
{ cookie: kModi }, null, "PUT");
ok(r.code === 404, `dasselbe beim Wiederaufnehmen (${r.code})`);
}
}
/* =======================================================================
15. DIE TALENTE-SEITE IM ECHTEN BROWSER (17.09.2026)
Der erste Browserblick auf diese Seite ueberhaupt. Er ist faellig:
Heute Mittag hat ein Bildschirmfoto zwei Dinge gefunden, die keine
Zahl gefunden haette -- ein Wasserzeichen, das durch vertraulichen
Text hindurchschien (color-mix mit DURCHSICHTIG statt mit Weiss),
und eine Klassenkollision, bei der .t-schritt fuer zwei verschiedene
Dinge stand. Beides war gruen in jeder Prueflinie und falsch auf dem
Schirm.
GEKLICKT WIRD WIRKLICH, nicht ueber die Schnittstelle. Der ganze Weg
-- aufklappen, tippen, absenden -- ist der, der kaputtgehen kann; was
darunter liegt, ist in Abschnitt 14 schon bewiesen.
======================================================================= */
melde("");
melde("=== 15. Im echten Browser ===");
{
let chromium;
try {
({ chromium } = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"));
} catch { chromium = null; }
if (!chromium) {
unklar("kein Playwright gefunden -- die Oberflaeche wurde nicht angesehen");
} else {
const browser = await chromium.launch();
try {
/* 390 x 844 -- ein Handy. Filipe arbeitet am Handy, und die
Kopfleiste ist genau dort im September zerbrochen. */
const sicht = await browser.newContext({ viewport: { width: 390, height: 844 } });
await sicht.addCookies(kDogi.split("; ").map((c) => {
const [name, ...rest] = c.split("=");
return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" };
}));
const seite = await sicht.newPage();
const konsole = [];
seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); });
await seite.goto(`http://127.0.0.1:${PORT}/workspace/talente.html`,
{ waitUntil: "domcontentloaded" });
await seite.waitForTimeout(1200);
const karten = await seite.locator(".t-karte").count();
ok(karten > 0, `die Seite zeigt ${karten} Karten`);
/* ABGELEITET, NICHT ABGESCHRIEBEN: An jeder Karte IM TRICHTER
steht ein Ausgang -- an denen im Team ausdruecklich nicht.
Feste Zahlen waeren hier die Rechnung von gestern: Kommt ein
Kandidat dazu, muesste man sie nachziehen. */
const imTeam = await seite.locator('.t-karte[data-stufe="imteam"]').count();
const ausgaenge = await seite.locator(".t-ausgang__auf").count();
ok(ausgaenge === karten - imTeam,
`an jeder Karte im Trichter ein Ausgang (${ausgaenge} von ${karten} `
+ `- ${imTeam} im Team)`);
ok(imTeam > 0 && await seite.locator(".t-ausgang__anderswo").count() === imTeam,
`und an denen im Team steht stattdessen der Weg zum Zugang (${imTeam})`);
ok(await seite.locator('.t-karte[data-stufe="imteam"] .t-ausgang__auf').count() === 0,
"dort gibt es das Feld nicht einmal zum Aufklappen");
/* ---- Der Weg: aufklappen, zu kurz, dann richtig ----
Gezielt eine Karte AUS DEM TRICHTER, nicht einfach die erste:
Welche oben steht, entscheidet die Sortierung, und die haengt
an Standzeiten. */
const erste = seite.locator('.t-karte:not([data-stufe="imteam"])').first();
const titel = (await erste.locator(".t-karte__titel").textContent())?.trim();
ok(await erste.locator(".t-ausgang__form").isHidden(),
"das Feld ist zugeklappt -- der Ausgang draengt sich nicht auf");
await erste.locator(".t-ausgang__auf").click();
await seite.waitForTimeout(200);
ok(await erste.locator(".t-ausgang__form").isVisible(),
"nach einem Klick steht es da");
{
/* ZU KURZ -- und zwar im Browser, nicht nur am Server. Was der
Browser prueft, ist Hoeflichkeit; faellt sie aus, sieht man
statt eines Satzes eine rohe Fehlermeldung. */
await erste.locator(".t-ausgang__feld").fill("passt nix");
await erste.locator(".t-ausgang__knopf").click();
await seite.waitForTimeout(400);
const meldung = (await seite.locator("#fehler").textContent())?.trim() || "";
ok(meldung.length > 10, `ein zu kurzer Satz wird abgewiesen ("${meldung}")`);
ok(await seite.locator(".t-karte").count() === karten,
"und die Karte bleibt stehen");
}
const GRUND_B = "Kam dreimal nicht zur Schicht, ohne Bescheid zu sagen.";
{
await erste.locator(".t-ausgang__feld").fill(GRUND_B);
await erste.locator(".t-ausgang__knopf").click();
await seite.waitForTimeout(900);
const uebrig = await seite.locator(".t-karte").count();
ok(uebrig === karten - 1,
`mit Satz verschwindet sie aus der Arbeitsliste (${karten} -> ${uebrig})`);
ok(await seite.locator(".t-beendet__auf").count() === 1,
"und unten steht die Sammlung der Beendeten");
const wort = (await seite.locator(".t-beendet__auf").textContent())?.trim() || "";
ok(/[0-9]/.test(wort), `zugeklappt mit Zahl ("${wort}")`);
}
{
await seite.locator(".t-beendet__auf").click();
await seite.waitForTimeout(300);
const karte = seite.locator(".t-beendet__karte").first();
ok(await karte.isVisible(), "aufgeklappt steht die Karte da");
ok((await karte.locator(".t-beendet__titel").textContent())?.trim() === titel,
"mit dem richtigen Namen");
ok((await karte.locator(".t-beendet__grund").textContent())?.includes(GRUND_B),
"und dem Satz von damals");
const stufe = (await karte.locator(".t-beendet__stufe").textContent())?.trim() || "";
ok(stufe.length > 3, `und WO es geendet hat (${stufe})`);
/* GEGENPROBE ZUR SICHTBARKEIT: An einer beendeten Karte steht
KEIN zweiter Ausgang -- sonst koennte man beenden, was schon
beendet ist, und der Grund wuerde ueberschrieben. */
ok(await karte.locator(".t-ausgang__auf").count() === 0,
"an einer beendeten Karte steht kein zweiter Ausgang");
ok(await karte.locator(".t-beendet__wieder").count() === 1,
"sondern der Weg zurueck");
}
{
/* DIE KOPFZEILE BEI 390 px. margin-inline-start: auto schiebt
das Datum nach rechts -- genau die Bauart, die im September
in der Kopfleiste zwei Knoepfe uebereinandergelegt hat. Eine
Messung statt einer Zahl im CSS. */
const k = seite.locator(".t-beendet__karte").first();
const a = await k.locator(".t-beendet__titel").boundingBox();
const b = await k.locator(".t-beendet__zeit").boundingBox();
const ueber = a && b
&& a.x < b.x + b.width && b.x < a.x + a.width
&& a.y < b.y + b.height && b.y < a.y + a.height;
ok(!!a && !!b && !ueber,
`Name und Datum liegen nicht uebereinander (${Math.round(a?.x ?? -1)}+`
+ `${Math.round(a?.width ?? 0)} | ${Math.round(b?.x ?? -1)})`);
/* Und der Weg zurueck muss mit einem Finger zu treffen sein. */
const w = await k.locator(".t-beendet__wieder").boundingBox();
ok(!!w && w.height >= 28 && w.width >= 100,
`der Weg zurueck ist treffbar (${Math.round(w?.width ?? 0)} x ${Math.round(w?.height ?? 0)})`);
}
{
await seite.locator(".t-beendet__wieder").first().click();
await seite.waitForTimeout(900);
const zurueck = await seite.locator(".t-karte").count();
ok(zurueck === karten, `wieder aufnehmen bringt sie zurueck (${zurueck})`);
ok(await seite.locator(".t-beendet__auf").count() === 0,
"und die Sammlung verschwindet, wenn nichts mehr drin ist");
}
ok(konsole.length === 0,
konsole.length ? `Konsolenfehler: ${konsole[0]}` : "kein einziger Konsolenfehler");
/* Das Bildschirmfoto -- der Teil, den keine Zahl ersetzt. */
await seite.locator('.t-karte:not([data-stufe="imteam"]) .t-ausgang__auf')
.first().click().catch(() => {});
await seite.waitForTimeout(300);
await seite.screenshot({ path: "server/pruef-nachwuchs-ausgang.png", fullPage: true });
melde(" -- Bildschirmfoto: server/pruef-nachwuchs-ausgang.png");
} finally {
await browser.close();
}
}
}
/* ------------------------------------------------------------------- */
melde("");
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
melde(`${geprueft} Pruefungen, ${fehler} Fehler`
+ (offen ? `, ${offen} nicht nachsehbar` : ""));
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ }
process.exit(fehler ? 1 : 0);