diff --git a/server/pruef-befinden.mjs b/server/pruef-befinden.mjs index 18a1f660..9fab9224 100644 --- a/server/pruef-befinden.mjs +++ b/server/pruef-befinden.mjs @@ -53,6 +53,15 @@ process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):" let fehler = 0, geprueft = 0; const melde = (t) => console.log(t); +/* WIE VIELE FRAGEN ES GIBT -- aus dem Katalog, nicht abgeschrieben. + + Vier Zeilen dieser Datei nannten frueher feste Zahlen ("2 Runden x 6", + "zwoelf Antworten", "alle sechs Fragen"). Am 17.09.2026 wurden aus + sechs Fragen zwoelf, und sie waren ab da rot, ohne dass irgendetwas + kaputt war. Eine feste Zahl ist eine Rechnung von gestern. */ +const anzahlFragen = (await import("./workspace-entwicklungs-punkte.js")) + .ENTWICKLUNG_PUNKTE.befinden.length; + const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; const CREW = "crew.dogfather-universe.com"; @@ -246,7 +255,14 @@ melde("=== 4. Privat ==="); ok(spur.length === 2, `${spur.length} Runden stehen im Protokoll`); ok(spur.every((z) => !/laeuft|hakt|waechst|unklar/.test(z.detail || "")), `aber keine einzige Antwort darin (${spur[0]?.detail})`); - ok(verlaufZeilen === 12, `im Verlauf liegen ${verlaufZeilen} Antworten (2 Runden x 6)`); + /* DIE ZAHL WIRD GERECHNET, NICHT ABGESCHRIEBEN. + + Hier stand `=== 12` mit dem Kommentar "2 Runden x 6". Am + 17.09.2026 wurden aus sechs Fragen zwoelf, und drei Zeilen in + dieser Datei waren ab da rot, ohne dass irgendetwas kaputt war. + Eine feste Zahl ist eine Rechnung von gestern. */ + ok(verlaufZeilen === 2 * anzahlFragen, + `im Verlauf liegen ${verlaufZeilen} Antworten (2 Runden x ${anzahlFragen})`); /* UND DIE AMPEL ZAEHLT WEITER NUR DEN AKTUELLEN STAND, ohne Namen. */ const a = await ruf("/workspace/api/entwicklung/ampel", CREW, "GET", { cookie: kDogi }); @@ -333,7 +349,8 @@ melde("=== 6. Im Browser ==="); /* FRIDA HAT ZWEI RUNDEN -- bei ihr MUSS er dastehen. */ ok(stand.verlauf === true, "ihr Verlauf steht auf der Seite"); ok(stand.spalten === 2, `mit zwei Ständen nebeneinander (${stand.spalten})`); - ok(stand.punkte === 12, `und zwölf Antworten darin (${stand.punkte})`); + ok(stand.punkte === 2 * anzahlFragen, + `und ${2 * anzahlFragen} Antworten darin (${stand.punkte})`); /* KEIN FREMDER NAME AUF DEM BILDSCHIRM. Ihr eigener darf oben in der Kopfleiste stehen -- gemessen wird deshalb nur der Inhalt, und gesucht wird nach DogFather, also nach dem, der hier nichts zu @@ -484,13 +501,15 @@ melde("=== 8. Die Analyse im Browser ==="); .every((x) => x.rel.includes("noopener")), })); ok(a.da, "die Analyse steht auf der Seite"); - ok(a.zeilen === 6, `mit allen sechs Fragen (${a.zeilen})`); + ok(a.zeilen === anzahlFragen, + `mit allen ${anzahlFragen} Fragen (${a.zeilen})`); /* DAS MUSTER STEHT OBEN. Alphabetisch oder in Katalogreihenfolge müsste man es suchen -- und wer sucht, liest es beim zweiten Mal nicht mehr. */ ok(a.ersteMarke === "hakt", `und das, was seit mehreren Runden hakt, steht zuoberst (${a.ersteMarke})`); - ok(a.welt === 6, `zu jeder Zeile die Einordnung (${a.welt})`); + ok(a.welt === anzahlFragen, + `zu jeder der ${anzahlFragen} Zeilen die Einordnung (${a.welt})`); ok(a.hilft === 1, `ein Rat, genau dort, wo es hakt (${a.hilft})`); ok(a.quellen.length === 3, `${a.quellen.length} Quellen stehen darunter`); ok(a.quellen.some((x) => /tiktok/i.test(x)), diff --git a/server/pruef-nachwuchs.mjs b/server/pruef-nachwuchs.mjs index 3c90a845..f048175c 100644 --- a/server/pruef-nachwuchs.mjs +++ b/server/pruef-nachwuchs.mjs @@ -25,6 +25,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { request as httpAnfrage } from "node:http"; import { portMussFreiSein } from "./helfer-port.mjs"; +import { tagLokal } from "./helfer-tag.mjs"; const PORT = await portMussFreiSein(4404, "pruef-nachwuchs"); @@ -719,6 +720,206 @@ melde("=== 12. Die Reihenfolge ==="); `auch auf der Team-Adresse steht sie vorn (${crewNamen.join(", ")})`); } + +/* ======================================================================= + 13. DIE PROBEZEIT HAT EINEN INHALT (17.09.2026) + + Filipe: „wie talente bewertet werden, aufgaben bekommen, analysiert + werden von vanvan und mir." + + Gemessen war vorher: Die Stufe „Probe" hatte eine Frist von 30 Tagen + und sonst nichts. Worauf die Entscheidung am Tag 30 sich gruenden + soll, stand nirgends -- und dann entscheidet das Bauchgefuehl + dessen, der zuletzt etwas gesehen hat. + ======================================================================= */ +melde(""); +melde("=== 13. Die Probezeit ==="); +{ + const probeplan = await import("./workspace-probeplan.js"); + + /* ---- Der Katalog selbst ---- */ + ok(probeplan.PROBE_ALLE.length >= 10, + `der Plan hat ${probeplan.PROBE_ALLE.length} Schritte in ${probeplan.PROBE_PHASEN.length} Phasen`); + { + const s = probeplan.PROBE_ALLE.map((x) => x.schluessel); + ok(new Set(s).size === s.length, "die Schluessel sind eindeutig"); + } + { + /* JEDER SCHRITT HAT EINEN ZUSTAENDIGEN. Ohne ihn wandern in jeder + Probezeit dieselben drei an niemanden. */ + const ohne = probeplan.PROBE_ALLE.filter((x) => !probeplan.PROBE_WER[x.wer]); + ok(ohne.length === 0, + ohne.length ? `ohne Zustaendigen: ${ohne.map((x) => x.schluessel).join(", ")}` + : "jeder Schritt sagt, WER ihn tut"); + const wer = new Set(probeplan.PROBE_ALLE.map((x) => x.wer)); + ok(wer.size === 3, + `und es sind wirklich drei verschiedene (${[...wer].join(", ")}) -- ` + + "eine Probe, die nur der Kandidat abarbeitet, prueft nichts"); + } + { + /* JEDER SCHRITT SAGT, WOFUER ER DA IST. Ein Haken, dessen Zweck man + nicht kennt, wird gesetzt, weil er dasteht. */ + const duenn = probeplan.PROBE_ALLE.filter((x) => (x.wofuer || "").length < 40); + ok(duenn.length === 0, + duenn.length ? `ohne Begruendung: ${duenn.map((x) => x.schluessel).join(", ")}` + : "und jeder sagt, welche Frage er beantwortet"); + } + + /* ---- Entscheidbar heisst NICHT „alles abgehakt" ---- */ + { + const leer = probeplan.probeStand([]); + const voll = probeplan.probeStand(probeplan.PROBE_ALLE.map((x) => x.schluessel)); + ok(leer.entscheidbar === false && leer.fehlt_noetig.length === 3, + `ohne einen Haken kann man nicht entscheiden (es fehlen ${leer.fehlt_noetig.length})`); + ok(voll.entscheidbar === true, "mit allen Haken schon"); + /* DER FALL DAZWISCHEN, und der ist der eigentliche Punkt: Neun von + zwoelf Haken reichen NICHT, wenn die drei noetigen fehlen. Eine + Schwelle nach Anzahl waere Fleiss beim Abhaken, keine + Beurteilung. */ + const ohneNoetige = probeplan.PROBE_ALLE + .map((x) => x.schluessel) + .filter((s) => !["pr-allein-schicht", "pr-team-gefragt", "pr-er-will-noch"].includes(s)); + const fast = probeplan.probeStand(ohneNoetige); + ok(fast.getan === probeplan.PROBE_ALLE.length - 3 && fast.entscheidbar === false, + `${fast.getan} von ${fast.gesamt} Haken reichen nicht -- es kommt auf die drei an, ` + + "nicht auf die Anzahl"); + /* Und die Gegenprobe dazu: NUR die drei genuegen. */ + const nurDrei = probeplan.probeStand( + ["pr-allein-schicht", "pr-team-gefragt", "pr-er-will-noch"]); + ok(nurDrei.entscheidbar === true, + `drei von ${nurDrei.gesamt} genuegen, wenn es die richtigen sind`); + } + + /* ---- Am lebenden Kandidaten ---- + + EIN EIGENER, FRISCHER. Der aus den Abschnitten davor ist laengst + durch den ganzen Trichter gelaufen und steht auf "imteam" -- an ihm + waere "vor der Probe steht keine Liste" nicht falsch, sondern + unpruefbar. Beim ersten Anlauf war genau das der Grund fuer vier + rote Zeilen: meine Annahme, nicht der Code. */ + const neuerTalent = await roh("/workspace/api/bereich/talente", HAUS, { cookie: kDogi }, + { art: "ruhig", titel: "Jonas aus dem Chat", text: "Faellt seit kurzem auf." }); + ok(neuerTalent.code === 201 || neuerTalent.code === 200, + `ein frischer Kandidat fuer diesen Abschnitt (${neuerTalent.code})`); + const lage = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi })); + const kandidat = (lage.kandidaten || []).find((k) => k.titel === "Jonas aus dem Chat"); + ok(!!kandidat, `er steht im Trichter (${kandidat?.stufe})`); + + /* SOLANGE ER NICHT IN DER PROBE IST, gibt es keine Liste. Eine leere + Zwoelferliste an einem Beobachteten waere Laerm -- und Laerm liest + man auch dann weg, wenn er gilt. */ + ok(!kandidat.probe, + `vor der Probe steht dort keine Liste (${kandidat.probe ? "doch eine" : "keine"})`); + { + const r = await roh(`/workspace/api/talente/${kandidat.id}/probe/pr-erste-schicht`, + HAUS, { cookie: kDogi }, {}, "PUT"); + ok(r.code === 409, + `und abhaken geht nicht, solange die Probe nicht laeuft (${r.code})`); + } + + /* ---- Jetzt in die Probe ---- */ + /* Ortszeit, nicht UTC: Zwischen 00:00 und 02:00 waere „morgen“ aus + UTC gerechnet heute -- und der Server lehnt ein Datum in der + Vergangenheit ab. Die Pruefung waere nachts rot, ohne dass + irgendetwas kaputt ist. */ + const morgen = tagLokal(1); + { + /* Ueber den vorhandenen Weg, nicht an ihm vorbei. */ + let stufe = kandidat.stufe; + let schutz = 0; + while (stufe !== "probe" && schutz++ < 6) { + const naechste = tal.talentNaechste(stufe)?.schluessel; + if (!naechste) break; + const r = await roh(`/workspace/api/talente/${kandidat.id}/stufe`, HAUS, + { cookie: kDogi }, + naechste === "probe" + ? { stufe: naechste, buddy_id: idModi, erste_schicht: morgen } + : { stufe: naechste }, + "PUT"); + if (r.code !== 200) break; + stufe = naechste; + } + ok(stufe === "probe", `der Kandidat steht jetzt in der Probe (${stufe})`); + } + + { + const k = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi })) + .kandidaten.find((x) => x.id === kandidat.id); + ok(!!k.probe, "jetzt steht die Liste an der Karte"); + ok(k.probe.gesamt === probeplan.PROBE_ALLE.length && k.probe.getan === 0, + `${k.probe.getan} von ${k.probe.gesamt} -- und noch nichts abgehakt`); + ok(k.probe.entscheidbar === false, + "und noch nicht entscheidbar -- am Tag 1 waere alles andere gelogen"); + const mitWer = k.probe.schritte.filter((s) => s.wer_name); + ok(mitWer.length === k.probe.gesamt, + `jeder Schritt bringt seinen Zustaendigen mit (${mitWer.length})`); + } + + /* ---- Abhaken, und WER es war ---- */ + { + const r = await roh(`/workspace/api/talente/${kandidat.id}/probe/pr-erste-schicht`, + HAUS, { cookie: kDogi }, {}, "PUT"); + ok(r.code === 200 && json(r).fertig === true, `abhaken geht (${r.code})`); + const k = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi })) + .kandidaten.find((x) => x.id === kandidat.id); + const s = k.probe.schritte.find((x) => x.schluessel === "pr-erste-schicht"); + ok(s.fertig === true && s.von === "Filipe", + `und es steht dabei, wer es war (${s.von})`); + ok(k.probe.getan === 1, `der Stand zieht mit (${k.probe.getan} von ${k.probe.gesamt})`); + } + { + /* EIN KNOPF, ZWEI RICHTUNGEN -- wie bei den Merkmalen. */ + const r = await roh(`/workspace/api/talente/${kandidat.id}/probe/pr-erste-schicht`, + HAUS, { cookie: kDogi }, {}, "PUT"); + ok(r.code === 200 && json(r).fertig === false, "ein zweiter Klick nimmt zurueck"); + const k = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi })) + .kandidaten.find((x) => x.id === kandidat.id); + ok(k.probe.getan === 0, "und der Stand geht mit zurueck"); + } + { + const r = await roh(`/workspace/api/talente/${kandidat.id}/probe/voellig-erfunden`, + HAUS, { cookie: kDogi }, {}, "PUT"); + ok(r.code === 404, `ein erfundener Schritt wird abgewiesen (${r.code})`); + } + + /* ---- Und die Frage, wegen der das alles da ist ---- */ + { + for (const s of ["pr-allein-schicht", "pr-team-gefragt", "pr-er-will-noch"]) { + await roh(`/workspace/api/talente/${kandidat.id}/probe/${s}`, HAUS, + { cookie: kDogi }, {}, "PUT"); + } + const k = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi })) + .kandidaten.find((x) => x.id === kandidat.id); + ok(k.probe.entscheidbar === true, + `mit den drei noetigen Schritten ist es entscheidbar (${k.probe.getan} von ${k.probe.gesamt})`); + ok(Array.isArray(k.probe.fragen) && k.probe.fragen.length === 3, + `und drei Fragen stehen bereit (${k.probe.fragen?.length})`); + } + + /* ---- Wer was gesehen hat ---- */ + { + const m = tal.TALENT_MERKMALE[tal.TALENT_GRUPPEN[0].schluessel][0].schluessel; + await roh(`/workspace/api/talente/${kandidat.id}/merkmal/${m}`, HAUS, + { cookie: kDogi }, {}, "PUT"); + const k = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi })) + .kandidaten.find((x) => x.id === kandidat.id); + ok(k.gesehen_von?.[m] === "Filipe", + `an einem Merkmal steht, wer es gesehen hat (${k.gesehen_von?.[m]})`); + /* GEGENPROBE: Ein nicht gesetztes Merkmal hat auch keinen Namen -- + sonst hiesse "steht dabei" nur, dass ueberall etwas steht. */ + const ander = tal.TALENT_MERKMALE[tal.TALENT_GRUPPEN[0].schluessel][1]?.schluessel; + ok(!ander || !k.gesehen_von?.[ander], + "und an einem nicht gesetzten steht keiner"); + } + + /* ---- Die Community kommt an nichts davon heran ---- */ + { + const r = await roh(`/workspace/api/talente/${kandidat.id}/probe/pr-erste-schicht`, + CREW, { cookie: kModi }, {}, "PUT"); + ok(r.code === 404, `ein Modi kann nichts abhaken -- und erfaehrt nicht, dass es das gibt (${r.code})`); + } +} + /* ------------------------------------------------------------------- */ melde(""); melde(`${geprueft} Pruefungen, ${fehler} Fehler`); diff --git a/server/pruef-schritt.mjs b/server/pruef-schritt.mjs index b733fc58..96f984bf 100644 --- a/server/pruef-schritt.mjs +++ b/server/pruef-schritt.mjs @@ -362,7 +362,17 @@ melde("=== 8. Im Browser ==="); alleKnopf: document.querySelector(".e-bloecke__alle")?.textContent.trim() || "", }; }); - ok(bloecke.anzahl === 4, `${bloecke.anzahl} Kategorien stehen auf der Karte`); + /* DIE ZAHL KOMMT AUS DEM KATALOG, nicht aus dieser Datei. + + Hier stand `=== 4`. Am 17.09.2026 kamen zwei Bloecke dazu ("Clips, + Schnitt und Kommentare" und "Waehrend der Stream laeuft"), und die + Zeile war ab da rot, ohne dass irgendetwas kaputt war -- eine feste + Zahl ist eine Rechnung von gestern. Was die Zeile eigentlich + behauptet, ist: Die Karte zeigt ALLE Bloecke. */ + const bloeckeErwartet = (await import("./workspace-entwicklungs-punkte.js")) + .ENTWICKLUNG_BLOECKE.filter((b) => !b.nurSelbst).length; + ok(bloecke.anzahl === bloeckeErwartet, + `${bloecke.anzahl} von ${bloeckeErwartet} Kategorien stehen auf der Karte`); ok(bloecke.schalter === bloecke.anzahl, "jede hat einen Knopf zum Auf- und Zuklappen"); /* GENAU EINE STEHT OFFEN -- die erste, in der noch etwas fehlt. Alle zu hieße jedes Mal suchen, alle auf wäre der Zustand von vorher. */ @@ -400,7 +410,8 @@ melde("=== 8. Im Browser ==="); offen: document.querySelectorAll('.e-block[data-offen="ja"]').length, knopf: document.querySelector(".e-bloecke__alle")?.textContent.trim() || "", })); - ok(alle.offen === 4, `„Alle aufklappen“ öffnet alle vier (${alle.offen})`); + ok(alle.offen === bloeckeErwartet, + `„Alle aufklappen“ öffnet wirklich alle (${alle.offen} von ${bloeckeErwartet})`); ok(/Alle zuklappen/.test(alle.knopf), `und der Knopf heißt danach anders („${alle.knopf}“)`); @@ -474,12 +485,22 @@ melde("=== 8. Im Browser ==="); return { name, y: Math.round(window.scrollY), - kachel: kachel?.querySelector(".e-person__zahl")?.textContent.trim() || "", + /* DIE KACHEL ZAEHLT SEIT DEM 17.09.2026 IN KAESTEN. + + Vorher stand dort ein Satz ("2 von 68 angesehen"), und dieser + Selektor las ihn. Seit die Kachel dieselben Bilanz-Kaesten + traegt wie das Brett daneben, ist `.e-person__zahl` nur noch + die Zeile fuer veraltete Einschaetzungen -- die Pruefung las + seitdem einen leeren String, ohne dass etwas kaputt war. */ + kachel: kachel?.querySelector(".bilanz-zahl__n")?.textContent.trim() || "", + kopfZahl: (document.querySelector("#karte .e-karte__stand")?.textContent + .match(/(\d+) von/) || [])[1] || "", kopf: document.querySelector("#karte .e-karte__stand")?.textContent.trim() || "", }; }); ok(vorDemKlick.name === "Frida", `Fridas Karte ist offen (${vorDemKlick.name})`); - const zahlAus = (t) => Number((t.match(/^(\d+) von/) || [])[1] ?? -1); + /* Die Kachel nennt jetzt eine nackte Ziffer, keinen Satz. */ + const zahlAus = (x) => { const n = Number(x); return Number.isFinite(n) && x !== "" ? n : -1; }; ok(zahlAus(vorDemKlick.kachel) >= 0, `vorher steht auf ihrer Kachel „${vorDemKlick.kachel}“`); @@ -501,7 +522,16 @@ melde("=== 8. Im Browser ==="); .find((x) => x.querySelector(".e-person__name")?.textContent.trim() === name); return { y: Math.round(window.scrollY), - kachel: kachel?.querySelector(".e-person__zahl")?.textContent.trim() || "", + /* DIE KACHEL ZAEHLT SEIT DEM 17.09.2026 IN KAESTEN. + + Vorher stand dort ein Satz ("2 von 68 angesehen"), und dieser + Selektor las ihn. Seit die Kachel dieselben Bilanz-Kaesten + traegt wie das Brett daneben, ist `.e-person__zahl` nur noch + die Zeile fuer veraltete Einschaetzungen -- die Pruefung las + seitdem einen leeren String, ohne dass etwas kaputt war. */ + kachel: kachel?.querySelector(".bilanz-zahl__n")?.textContent.trim() || "", + kopfZahl: (document.querySelector("#karte .e-karte__stand")?.textContent + .match(/(\d+) von/) || [])[1] || "", kopf: document.querySelector("#karte .e-karte__stand")?.textContent.trim() || "", }; }); @@ -513,8 +543,8 @@ melde("=== 8. Im Browser ==="); /* (2) „wieso steht bei vanvan 0 von 28 obwohl ich alle durch habe." */ ok(zahlAus(nachDemKlick.kachel) === zahlAus(vorDemKlick.kachel) + 1, `die Kachel zählt sofort mit (${vorDemKlick.kachel} -> ${nachDemKlick.kachel})`); - ok(nachDemKlick.kachel === nachDemKlick.kopf, - `und nennt dieselbe Zahl wie die Karte („${nachDemKlick.kopf}“)`); + ok(nachDemKlick.kachel === nachDemKlick.kopfZahl, + `und nennt dieselbe Zahl wie die Karte (${nachDemKlick.kachel} / ${nachDemKlick.kopfZahl})`); ok(zahlAus(nachDemKlick.kachel) > 0, "es ist eine echte Zahl, keine Null — sonst wäre die Gleichheit wertlos"); diff --git a/server/workspace-entwicklung.js b/server/workspace-entwicklung.js index a89018ad..684244a6 100644 --- a/server/workspace-entwicklung.js +++ b/server/workspace-entwicklung.js @@ -70,9 +70,14 @@ import { } from "./workspace-entwicklungs-punkte.js"; import { TALENT_STUFEN, TALENT_GRUPPEN, TALENT_MERKMALE, TALENT_WARNZEICHEN, + // (Probeplan siehe unten -- eigene Datei, eigener Gegenstand) SCHICHT_WEIT_TAGE, SCHICHT_MAX_TAGE, ERSTER_TAG, TALENT_NACH_SCHLUESSEL, talentStufe, talentNaechste, ansprechtext, } from "./workspace-talent-punkte.js"; +import { + PROBE_PHASEN, PROBE_SCHRITTE, PROBE_WER, PROBE_ALLE, PROBE_FRAGEN, + PROBE_NACH_SCHLUESSEL, probeStand, +} from "./workspace-probeplan.js"; export const entwicklungRouter = express.Router(); @@ -177,6 +182,30 @@ export function tabellen() { und die Stufe braucht ausserdem ein SEIT -- ohne das gibt es keine Standzeit, und ohne Standzeit wird aus dem Trichter wieder eine Liste. */ + /* WAS IN DER PROBEZEIT SCHON PASSIERT IST (17.09.2026). + + Filipe: "wie talente ... aufgaben bekommen, analysiert werden von + vanvan und mir." + + EIGENE TABELLE UND NICHT DIE AUFGABEN: Ein Talent ist ein EINTRAG, + kein Zugang -- es gibt keine Person, der man eine Aufgabe geben + koennte. Und die Schritte gehoeren nicht alle ihm: drei davon tut + der Buddy, drei die Leitung. Eine Aufgabe auf dem Brett waere + damit entweder herrenlos oder an der falschen Person. + + MIT von_id UND am, anders als bei den Merkmalen mit Absicht + SICHTBAR: Bei einem Schritt zaehlt, WER ihn gesehen hat. "Das + Team wurde gefragt" von dir und von der rechten Hand sind zwei + verschiedene Auskuenfte. */ + CREATE TABLE IF NOT EXISTS talent_probe ( + eintrag_id INTEGER NOT NULL REFERENCES eintraege(id) ON DELETE CASCADE, + schluessel TEXT NOT NULL, + von_id INTEGER REFERENCES personen(id) ON DELETE SET NULL, + am TEXT NOT NULL, + notiz TEXT, + PRIMARY KEY (eintrag_id, schluessel) + ); + CREATE TABLE IF NOT EXISTS talent_stufe ( eintrag_id INTEGER PRIMARY KEY REFERENCES eintraege(id) ON DELETE CASCADE, stufe TEXT NOT NULL @@ -890,6 +919,53 @@ function talentKarte(e, merkmale) { eigenen Weg: Er aendert sich mit jedem Klick, und ein Text, den man extra abrufen muss, ist beim Abrufen schon veraltet. */ ansprechtext: ansprechtext(e.titel, gesetzt), + /* WER WAS GESEHEN HAT (17.09.2026). + + Filipe: "analysiert werden von vanvan und mir." + + `von_id` stand seit dem Bau in der Tabelle und wurde nie + ausgeliefert -- man sah, DASS ein Merkmal gesetzt ist, nicht von + wem. Bei zwei Leuten, die unabhaengig beobachten, ist genau das + die Auskunft: Was ihr BEIDE gesehen habt, ist mehr wert als was + einer von euch gesehen hat. */ + gesehen_von: Object.fromEntries(merkmale + .filter((m) => m.eintrag_id === e.id && m.von_name) + .map((m) => [m.schluessel, m.von_name])), + /* DIE PROBE. Sie steht an der Karte und nicht hinter einem eigenen + Abruf -- sonst holt man sie erst, wenn man schon entschieden + hat. */ + probe: probeVon(e.id, stufe.schluessel), + }; +} + +/* ===================================================================== + DIE PROBEZEIT EINES KANDIDATEN + + Sie wird nur fuer die Stufe "probe" (und danach) gefuellt. An einem + Kandidaten, der noch beobachtet wird, waere eine leere Zwoelferliste + nur Laerm -- und Laerm, den man dreimal wegliest, liest man auch + dann weg, wenn er gilt. + ===================================================================== */ +function probeVon(eintragId, stufe) { + if (stufe !== "probe" && stufe !== "imteam") return null; + const zeilen = db().prepare(` + SELECT p.schluessel, p.am, p.notiz, x.name AS von_name + FROM talent_probe p + LEFT JOIN personen x ON x.id = p.von_id + WHERE p.eintrag_id = ?`).all(eintragId); + const stand = probeStand(zeilen.map((z) => z.schluessel)); + const je = new Map(zeilen.map((z) => [z.schluessel, z])); + return { + ...stand, + schritte: PROBE_ALLE.map((s) => ({ + ...s, + wer_name: PROBE_WER[s.wer] || s.wer, + fertig: je.has(s.schluessel), + am: je.get(s.schluessel)?.am || null, + von: je.get(s.schluessel)?.von_name || null, + notiz: je.get(s.schluessel)?.notiz || null, + })), + fragen: PROBE_FRAGEN, }; } @@ -900,8 +976,10 @@ entwicklungRouter.get("/workspace/api/talente/lage", (req, res) => { WHERE bereich = 'talente' ORDER BY id DESC LIMIT 300`).all(); const merkmale = eintraege.length ? db().prepare( - `SELECT eintrag_id, schluessel FROM talent_merkmal - WHERE eintrag_id IN (${eintraege.map(() => "?").join(", ")})`) + `SELECT m.eintrag_id, m.schluessel, x.name AS von_name + FROM talent_merkmal m + LEFT JOIN personen x ON x.id = m.von_id + WHERE m.eintrag_id IN (${eintraege.map(() => "?").join(", ")})`) .all(...eintraege.map((e) => e.id)) : []; @@ -971,6 +1049,55 @@ entwicklungRouter.put("/workspace/api/talente/:id/merkmal/:schluessel", gleicheH } }); +/* ===================================================================== + EINEN SCHRITT DER PROBE ABHAKEN (17.09.2026) + + EIN KNOPF, ZWEI RICHTUNGEN -- wie bei den Merkmalen. Ein zweiter + Klick nimmt zurueck; ein eigener Weg zum Abwaehlen waere die Stelle, + an der die Rechtepruefung irgendwann fehlt. + + NUR AUF DER STUFE "PROBE". Wer einen Schritt abhakt, bevor die Probe + laeuft, haekchen einen Zeitraum ab, den es nicht gibt -- und am Tag + 30 stuende eine volle Liste da, die nichts bedeutet. + ===================================================================== */ +entwicklungRouter.put("/workspace/api/talente/:id/probe/:schluessel", gleicheHerkunft, + express.json({ limit: "8kb" }), (req, res) => { + try { + const id = nummer(req.params.id); + const schluessel = String(req.params.schluessel || ""); + const e = id && db().prepare( + "SELECT id, bereich FROM eintraege WHERE id = ?").get(id); + if (!e || e.bereich !== "talente") return res.status(404).json({ fehler: "nicht_gefunden" }); + if (!PROBE_NACH_SCHLUESSEL.has(schluessel)) { + return res.status(404).json({ fehler: "nicht_gefunden" }); + } + const stand = db().prepare("SELECT stufe FROM talent_stufe WHERE eintrag_id = ?").get(id); + if (stand?.stufe !== "probe") { + return res.status(409).json({ + fehler: "Das gehört in die Probezeit – die läuft für ihn gerade nicht.", + }); + } + + const schon = db().prepare( + "SELECT 1 AS x FROM talent_probe WHERE eintrag_id = ? AND schluessel = ?") + .get(id, schluessel); + if (schon) { + db().prepare("DELETE FROM talent_probe WHERE eintrag_id = ? AND schluessel = ?") + .run(id, schluessel); + } else { + const notiz = String(req.body?.notiz ?? "").trim().slice(0, 400) || null; + db().prepare( + "INSERT INTO talent_probe (eintrag_id, schluessel, von_id, am, notiz)" + + " VALUES (?,?,?,?,?)") + .run(id, schluessel, req.person.id, jetzt(), notiz); + } + res.json({ ok: true, fertig: !schon }); + } catch (f) { + console.error("[talente] Probe:", f?.message); + res.status(503).json({ fehler: "nicht_verfuegbar" }); + } + }); + /** Buddy und Datum prüfen. Gibt `{ fehlt: [...] }` oder `{ buddyId, schicht }`. * * EINE STELLE, ZWEI AUFRUFER: der Stufenwechsel auf „Probe" und das diff --git a/server/workspace-probeplan.js b/server/workspace-probeplan.js new file mode 100644 index 00000000..47769a2e --- /dev/null +++ b/server/workspace-probeplan.js @@ -0,0 +1,277 @@ +/* ===================================================================== + DIE PROBE HAT JETZT EINEN INHALT (17.09.2026) + + Filipe: „wie talente bewertet werden, aufgaben bekommen, analysiert + werden von vanvan und mir, alles mögliche." + + --------------------------------------------------------------------- + WAS GEMESSEN VORHER FEHLTE + + Die Stufe „Probe" hatte eine Frist von 30 Tagen und einen Satz: + „Nach 30 Tagen: übernehmen -- der Zugang entsteht dabei -- oder sauber + beenden." Dazu einen Buddy und ein Datum für die erste Schicht. + + Worauf diese Entscheidung sich gründen soll, stand nirgends. + + Eine Probezeit ohne Inhalt ist keine Probe, sondern eine Wartezeit -- + und am dreißigsten Tag entscheidet dann das Bauchgefühl dessen, der + zuletzt etwas gesehen hat. Genau das wollte Filipe nicht: „ich will + hoch profissionel arbeiten." + + --------------------------------------------------------------------- + DER AUFBAU: MITLAUFEN -> SELBST MACHEN -> ALLEIN + + Drei Phasen, und die Reihenfolge ist der ganze Punkt. Es ist die + Form, die Discord seinen Servern für neue Moderatoren empfiehlt und + die in der Ausbildung „see one, do one, teach one" heißt: erst + zusehen, dann unter Aufsicht handeln, dann allein. + + Wer die mittlere Stufe überspringt, bekommt am Tag 30 zwei Sorten + Kandidaten, die man nicht unterscheiden kann: die, die es können, + und die, die noch nie in der Lage waren. + + --------------------------------------------------------------------- + JEDER SCHRITT HAT EINEN ZUSTÄNDIGEN, UND DAS IST DIE HALBE MIETE + + `wer` sagt, WER ihn tut -- das Talent, der Buddy oder die Leitung. + Ohne diese Angabe wandern in jeder Probezeit dieselben drei Schritte + an niemanden: Der Buddy denkt, die Leitung macht es; die Leitung + denkt, der Buddy macht es. + + --------------------------------------------------------------------- + WAS DIESE LISTE NICHT IST + + Keine Prüfung mit Punkten. Es gibt keine Note und keine Mindestzahl. + Am Ende stehen drei Fragen, und die beantwortet ein Mensch -- die + Häkchen sagen nur, ob er überhaupt genug gesehen hat, um sie zu + beantworten. Eine Probe, bei der acht von zwölf Haken reichen, misst + Fleiß beim Abhaken. + ===================================================================== */ + +/** Die drei Phasen der Probezeit. */ +export const PROBE_PHASEN = [ + { + schluessel: "mitlaufen", + name: "Erst mitlaufen", + zeit: "Woche 1", + text: "Zusehen, fragen, nichts müssen. Wer in der ersten Woche schon " + + "moderieren soll, lernt nicht, sondern rät.", + }, + { + schluessel: "selbst", + name: "Selbst machen, jemand schaut zu", + zeit: "Woche 2 und 3", + text: "Handeln – aber mit jemandem daneben, den man fragen kann. " + + "Hier entsteht der Unterschied zwischen „weiß es“ und „kann es“.", + }, + { + schluessel: "allein", + name: "Allein", + zeit: "Woche 4", + text: "Eine Schicht ohne Aufsicht. Sie ist der eigentliche Test – " + + "und sie muss stattgefunden haben, bevor jemand entscheidet.", + }, +]; + +/** Wer einen Schritt tut. */ +export const PROBE_WER = { + talent: "Der Kandidat", + buddy: "Der Buddy", + leitung: "Du oder die rechte Hand", +}; + +/* ===================================================================== + DIE SCHRITTE + + ZWÖLF, NICHT DREISSIG. Eine Liste, die man in dreißig Tagen nicht + schafft, wird nach der ersten Woche nicht mehr aufgemacht -- und + dann ist sie schlechter als keine, weil man glaubt, es gäbe eine. + + Jeder Schritt beantwortet eine Frage, die am Tag 30 sonst offen + bleibt. Das steht bei jedem dabei: Ein Haken, dessen Zweck man nicht + kennt, wird gesetzt, weil er dasteht. + ===================================================================== */ +export const PROBE_SCHRITTE = { + mitlaufen: [ + { + schluessel: "pr-erste-schicht", + titel: "Erste Schicht – nur zusehen", + wer: "talent", + text: "Ein ganzer Stream, mit dem Buddy im Ohr. Keine Knöpfe, keine " + + "Verantwortung. Danach: Was ist dir aufgefallen?", + wofuer: "Ob er überhaupt kommt. Der häufigste Abbruch passiert vor " + + "der ersten Schicht, nicht danach.", + }, + { + schluessel: "pr-regeln-gelesen", + titel: "Regeln gelesen – und eine Frage dazu gestellt", + wer: "talent", + text: "Nicht „hast du gelesen?“ – sondern: Was war unklar? Wer nach " + + "sechs Regeln keine einzige Frage hat, hat sie überflogen.", + wofuer: "Ob er Regeln liest oder nickt. Das ist im Streitfall der " + + "ganze Unterschied.", + }, + { + schluessel: "pr-buddy-gesprochen", + titel: "Der Buddy hat sich einmal richtig Zeit genommen", + wer: "buddy", + text: "Ein Gespräch, kein Ablauf. Wer er ist, warum er das machen " + + "will, was ihm unangenehm wäre.", + wofuer: "Anschluss ans Team ist der stärkste Schutz davor, dass " + + "jemand nach drei Wochen wieder weg ist.", + }, + { + schluessel: "pr-wen-fragen", + titel: "Er weiß, wen er fragt, wenn es eng wird", + wer: "buddy", + text: "Ein Name, nicht „das Team“. Und: was er tut, wenn dieser " + + "Name gerade nicht da ist.", + wofuer: "Wer das nicht weiß, macht im Ernstfall gar nichts – " + + "und das ist die teuerste Variante.", + }, + ], + selbst: [ + { + schluessel: "pr-erste-loeschung", + titel: "Erste eigene Entscheidung im Chat", + wer: "talent", + text: "Etwas löschen oder jemanden stummschalten, mit dem Buddy " + + "daneben. Danach in einem Satz: Warum?", + wofuer: "Ob er sich traut – und ob er es begründen kann. Beides " + + "einzeln ist zu wenig.", + }, + { + schluessel: "pr-eigener-fehler", + titel: "Ein Fehler ist passiert – und er hat es gesagt", + wer: "talent", + text: "Jeder macht in der zweiten Woche etwas falsch. Die Frage ist " + + "nur, ob man es erfährt.", + wofuer: "Der wichtigste Schritt der ganzen Liste. Wer Fehler " + + "verschweigt, ist als Moderator ein Risiko, egal wie gut er " + + "sonst ist.", + }, + { + schluessel: "pr-nein-gesagt", + titel: "Einmal Nein gesagt, ohne verletzend zu werden", + wer: "talent", + text: "Zu einem Zuschauer, zu einem Wunsch, zu einer Bitte. " + + "Freundlich und trotzdem klar.", + wofuer: "Die Kernfähigkeit. Wer nur nett sein kann, moderiert nicht.", + }, + { + schluessel: "pr-rueckmeldung-1", + titel: "Rückmeldung nach zwei Wochen", + wer: "buddy", + text: "Zwei Sätze, die er behalten kann: Was läuft, was noch nicht. " + + "Nicht am Tag 30 zum ersten Mal.", + wofuer: "Wer erst am Ende erfährt, dass etwas nicht passt, hatte " + + "keine Chance, es zu ändern – und das ist unser Fehler, nicht " + + "seiner.", + }, + ], + allein: [ + { + schluessel: "pr-allein-schicht", + titel: "Eine Schicht ohne Aufsicht", + wer: "talent", + text: "Ein ganzer Stream, allein zuständig. Der Buddy erreichbar, " + + "aber nicht daneben.", + wofuer: "Der eigentliche Test. Ohne ihn entscheidet man am Tag 30 " + + "über jemanden, den man nie hat arbeiten sehen.", + }, + { + schluessel: "pr-eng-geworden", + titel: "Es ist einmal wirklich eng geworden", + wer: "leitung", + text: "Streit, eine Welle, ein Zuschauer, der nicht aufhört. Wenn " + + "es in vier Wochen nie eng wurde, sagt die Probe wenig.", + wofuer: "Ruhig bleiben kann man nur beurteilen, wenn es unruhig war.", + }, + { + schluessel: "pr-team-gefragt", + titel: "Das Team wurde gefragt, was es denkt", + wer: "leitung", + text: "Die Modis, die mit ihm gearbeitet haben – einzeln, nicht in " + + "der Runde. In der Runde sagt niemand etwas Kritisches.", + wofuer: "Sie sehen ihn im Alltag. Du siehst ihn, wenn du hinsiehst.", + }, + { + schluessel: "pr-er-will-noch", + titel: "Er will selbst noch", + wer: "leitung", + text: "Einfach fragen. Vier Wochen Moderation sind für manche " + + "etwas anderes, als sie erwartet haben.", + wofuer: "Die Hälfte aller Abbrüche ist keine Entscheidung gegen " + + "ihn, sondern seine gegen uns – und sie kommt nicht von " + + "selbst zur Sprache.", + }, + ], +}; + +/** Alle Schritte in einer Liste -- abgeleitet, nicht danebengeschrieben. */ +export const PROBE_ALLE = PROBE_PHASEN.flatMap( + (p) => (PROBE_SCHRITTE[p.schluessel] || []).map((s) => ({ ...s, phase: p.schluessel }))); + +/** Schnellzugriff. */ +export const PROBE_NACH_SCHLUESSEL = new Map(PROBE_ALLE.map((s) => [s.schluessel, s])); + +/* ===================================================================== + DIE ENTSCHEIDUNG AM ENDE + + DREI FRAGEN, KEINE PUNKTZAHL. Sie stehen hier und nicht im Kopf + dessen, der gerade entscheidet -- und sie sind so gestellt, dass + „weiß nicht" eine mögliche Antwort ist. Wer sie nicht beantworten + kann, hat nicht genug gesehen; das ist ein Ergebnis und kein Mangel + an der Person. + ===================================================================== */ +export const PROBE_FRAGEN = [ + { + schluessel: "pf-allein", + frage: "Würdest du ihn an einem großen Abend allein lassen?", + text: "Nicht „irgendwann“ – am nächsten großen Abend.", + }, + { + schluessel: "pf-team", + frage: "Arbeitet das Team gern mit ihm?", + text: "Nicht „kommt klar“. Gern.", + }, + { + schluessel: "pf-selbst", + frage: "Will er selbst noch?", + text: "Nach vier Wochen weiß er, was es ist. Vorher wusste er es nicht.", + }, +]; + +/** + * Wie weit ist eine Probe? + * + * Gibt Zahlen zurueck, KEIN Urteil -- „9 von 12" ist eine Auskunft + * darueber, wie viel man gesehen hat, und nicht darueber, wie gut + * jemand ist. Ein Prozentwert waere hier eine Note, und Noten ueber + * Menschen werden gelesen, verglichen und weitererzaehlt. + */ +export function probeStand(erledigt) { + const fertig = new Set(erledigt || []); + const phasen = PROBE_PHASEN.map((p) => { + const schritte = PROBE_SCHRITTE[p.schluessel] || []; + return { + ...p, + gesamt: schritte.length, + fertig: schritte.filter((s) => fertig.has(s.schluessel)).length, + }; + }); + const gesamt = PROBE_ALLE.length; + const getan = PROBE_ALLE.filter((s) => fertig.has(s.schluessel)).length; + /* DIE DREI SCHRITTE, OHNE DIE MAN NICHT ENTSCHEIDEN KANN. + + Nicht „die wichtigsten" -- die, deren Fehlen die Entscheidung + unmoeglich macht. Wer nie allein gearbeitet hat und wen das Team + nie beurteilt hat, ueber den weiss man am Tag 30 nichts, egal wie + viele andere Haken stehen. */ + const noetig = ["pr-allein-schicht", "pr-team-gefragt", "pr-er-will-noch"]; + return { + phasen, gesamt, getan, + fehlt_noetig: noetig.filter((s) => !fertig.has(s)), + entscheidbar: noetig.every((s) => fertig.has(s)), + }; +} diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 7b75d5fb..6e1117e4 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -22,25 +22,25 @@ }, "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609171558", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609171614", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609171558", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609171614", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609171558", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609171614", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609171558", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609171614", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/entwicklung.css b/workspace/assets/css/entwicklung.css index 4c7ad78e..8e4ae84c 100644 --- a/workspace/assets/css/entwicklung.css +++ b/workspace/assets/css/entwicklung.css @@ -997,3 +997,117 @@ } .b-analyse__quelle-hin { color: var(--text-leise); } .b-analyse__quelle-hin:hover { color: var(--text); } + +/* ===================================================================== + DIE PROBEZEIT (17.09.2026) + + Filipe: „wie talente bewertet werden, aufgaben bekommen, analysiert + werden von vanvan und mir." + + EIGENER KLASSENNAME `t-pschritt` UND NICHT `t-schritt`. + + `.t-schritt` gibt es auf dieser Seite schon -- es ist die Zeile mit + dem naechsten Schritt der Trichterstufe, gestaltet 550 Zeilen weiter + oben. Meine Regeln standen SPAETER in derselben Datei und haetten + sie schlicht ueberschrieben: aus einer Zeile mit Rahmen und + Hintergrund waere eine ohne geworden. + + Gefunden hat das keine Ueberlegung, sondern das Nachzaehlen im + Browser: 13 Elemente mit `.t-schritt`, obwohl der Probeplan zwoelf + hat. Die dreizehnte war die fremde. + + RUHIG, OBWOHL ES EINE CHECKLISTE IST. Hier steht jemand auf dem + Pruefstand; eine Wand aus roten und gruenen Haken macht aus einer + Beobachtung ein Zeugnis. Es gibt genau EINE Farbe, und die kommt + erst, wenn man entscheiden kann. + ===================================================================== */ + +.t-probe { + margin: 14px 0 0; + padding: 14px 16px; + border: 1px solid var(--rand); + border-radius: 12px; + /* Deckend, nicht durchsichtig -- hinter der Seite liegt ein Motiv. + `color-mix(..., transparent)` hat mich heute schon einmal + erwischt. */ + background: color-mix(in srgb, var(--tinte) 94%, #ffffff 6%); +} +.t-probe[data-reif="ja"] { border-color: color-mix(in srgb, var(--gut) 55%, var(--rand)); } + +.t-probe__kopf { + display: flex; align-items: baseline; justify-content: space-between; + gap: 10px; flex-wrap: wrap; + margin-bottom: 8px; +} +.t-probe__titel { margin: 0; font-size: .95rem; font-weight: 700; } +.t-probe__stand { font-size: .82rem; opacity: .7; } + +.t-probe__reif { + margin: 0 0 12px; padding: 8px 10px; + font-size: .88rem; + border-radius: 8px; + background: color-mix(in srgb, var(--gut) 12%, transparent); +} +/* „Noch nicht entscheidbar" ist KEINE Warnung -- es ist eine Auskunft + ueber uns. Deshalb grau und nicht bernstein. */ +.t-probe__offen { margin: 0 0 12px; font-size: .84rem; opacity: .7; line-height: 1.5; } + +.t-phase { margin: 0 0 14px; } +.t-phase:last-of-type { margin-bottom: 0; } +.t-phase__kopf { + display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; + border-top: 1px solid var(--rand); + padding-top: 10px; +} +.t-phase__titel { margin: 0; font-size: .84rem; font-weight: 700; } +.t-phase__zeit { font-size: .78rem; opacity: .6; } +.t-phase__satz { margin: 3px 0 8px; font-size: .82rem; opacity: .72; line-height: 1.5; } + +.t-pschritt { display: flex; gap: 10px; align-items: flex-start; margin: 0 0 10px; } + +.t-pschritt__haken { + flex: none; + width: 26px; height: 26px; margin-top: 2px; + font: inherit; font-size: .84rem; line-height: 1; + color: inherit; + border: 1px solid var(--rand); border-radius: 7px; + background: color-mix(in srgb, var(--tinte) 88%, #ffffff 12%); + cursor: pointer; +} +.t-pschritt__haken:hover { border-color: color-mix(in srgb, var(--akzent) 60%, var(--rand)); } +.t-pschritt__haken:focus-visible { outline: 2px solid var(--akzent); outline-offset: 2px; } +.t-pschritt[data-fertig="ja"] .t-pschritt__haken { + border-color: color-mix(in srgb, var(--gut) 60%, var(--rand)); + background: color-mix(in srgb, var(--gut) 18%, transparent); +} + +.t-pschritt__text { min-width: 0; } +.t-pschritt__titel { + margin: 0; font-size: .88rem; font-weight: 600; + display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; +} +/* WER IHN TUT. Klein, aber da -- ohne diese Angabe wandern in jeder + Probezeit dieselben drei Schritte an niemanden. */ +.t-pschritt__wer { + font-size: .72rem; font-weight: 600; + letter-spacing: .03em; + padding: 1px 7px; border-radius: 999px; + opacity: .8; + background: color-mix(in srgb, var(--rand) 45%, transparent); +} +.t-pschritt__satz { margin: 2px 0 0; font-size: .82rem; opacity: .78; line-height: 1.5; } +.t-pschritt__wofuer { margin: 2px 0 0; font-size: .78rem; opacity: .58; line-height: 1.45; } +.t-pschritt__von { margin: 3px 0 0; font-size: .76rem; opacity: .6; } + +.t-probe__fragen { + margin-top: 14px; padding-top: 12px; + border-top: 1px solid color-mix(in srgb, var(--gut) 40%, var(--rand)); +} +.t-frage { margin: 0 0 10px; } +.t-frage__frage { margin: 0; font-size: .9rem; font-weight: 600; } +.t-frage__satz { margin: 2px 0 0; font-size: .8rem; opacity: .68; } +.t-probe__schluss { margin: 10px 0 0; font-size: .82rem; opacity: .75; line-height: 1.55; } + +/* Wer ein Merkmal gesehen hat. Steht am Knopf, klein -- 27 Knoepfe + nebeneinander vertragen keine zweite Zeile in Normalgroesse. */ +.t-merkmal__wer { font-size: .72rem; opacity: .6; margin-inline-start: 6px; } diff --git a/workspace/assets/js/talente.js b/workspace/assets/js/talente.js index 818db769..b163ce44 100644 --- a/workspace/assets/js/talente.js +++ b/workspace/assets/js/talente.js @@ -247,6 +247,15 @@ a.append(u); } + /* ---- Die Probezeit -------------------------------------------------- + + Sie steht direkt unter dem Uebergang: waehrend einer Probe ist sie + das Wichtigste auf der Karte. Und sie erscheint NUR dann -- an + einem Kandidaten, der noch beobachtet wird, waere eine leere + Zwoelferliste Laerm, und Laerm liest man auch dann weg, wenn er + gilt. */ + if (k.probe) probeZeigen(a, k); + /* ---- Merkmale, aufklappbar ------------------------------------------ Zugeklappt steht dort eine Zahl, aufgeklappt die 21 Merkmale. Immer offen wären es bei zehn Kandidaten 270 Kästchen auf einer @@ -534,9 +543,19 @@ b.type = 'button'; if (istWarnung) b.dataset.warnung = 'ja'; b.setAttribute('aria-pressed', String(gesetzt)); - b.title = m.text; + /* WER ES GESEHEN HAT, steht im Titel -- nicht als weitere Zeile + auf einem Knopf, der in 27 Stueck nebeneinander steht. + + Filipe: „analysiert werden von vanvan und mir." Bei zwei Leuten, + die unabhaengig beobachten, ist genau das die Auskunft: Wer hat + es gesehen? Bis heute stand `von_id` in der Tabelle und wurde nie + ausgeliefert. */ + const wer = k.gesehen_von?.[m.schluessel]; + b.title = m.text + (gesetzt && wer ? ' — gesehen von ' + wer + '.' : ''); + if (gesetzt && wer) b.dataset.wer = wer; b.append(el('span', 't-merkmal__haken', gesetzt ? '✓' : '+')); b.append(el('span', 't-merkmal__titel', m.titel)); + if (gesetzt && wer) b.append(el('span', 't-merkmal__wer', wer)); b.addEventListener('click', async () => { b.disabled = true; const r = await hole(`/workspace/api/talente/${k.id}/merkmal/${m.schluessel}`, @@ -549,6 +568,122 @@ } /* ---- Zeichnen --------------------------------------------------------- */ + /* ===================================================================== + DIE PROBEZEIT (17.09.2026) + + Filipe: „wie talente bewertet werden, aufgaben bekommen, analysiert + werden von vanvan und mir." + + Zwoelf Schritte in drei Phasen -- mitlaufen, selbst machen, allein. + Jeder mit einem Zustaendigen, denn ohne den wandern in jeder + Probezeit dieselben drei Schritte an niemanden. + + KEINE PUNKTZAHL, KEIN PROZENT. Die Zahl sagt, wie viel man GESEHEN + hat -- nicht, wie gut jemand ist. Ein Prozentwert ueber einem + Menschen wird gelesen, verglichen und weitererzaehlt. + ===================================================================== */ + const kurzDatum = (iso) => { + const d = new Date(iso || ''); + return Number.isNaN(d.getTime()) ? '' + : d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); + }; + + function probeZeigen(ziel, k) { + const pr = k.probe; + const kasten = el('section', 't-probe'); + if (pr.entscheidbar) kasten.dataset.reif = 'ja'; + + const kopf = el('div', 't-probe__kopf'); + kopf.append(el('h3', 't-probe__titel', 'Die Probezeit')); + kopf.append(el('span', 't-probe__stand', pr.getan + ' von ' + pr.gesamt + ' gesehen')); + kasten.append(kopf); + + /* DER SATZ, DER DIE ENTSCHEIDUNG TRAEGT. Er steht oben und nicht + unter zwoelf Zeilen: Wer die Karte aufmacht, will wissen, ob man + schon entscheiden kann. */ + if (pr.entscheidbar) { + kasten.append(el('p', 't-probe__reif', + 'Genug gesehen, um zu entscheiden. Die drei Fragen stehen unten.')); + } else { + const fehlt = pr.fehlt_noetig + .map((s) => (pr.schritte.find((x) => x.schluessel === s) || {}).titel) + .filter(Boolean); + kasten.append(el('p', 't-probe__offen', + 'Noch nicht entscheidbar – dafür fehlt: ' + fehlt.join(' · '))); + } + + for (const ph of pr.phasen) { + const block = el('div', 't-phase'); + const pk = el('div', 't-phase__kopf'); + pk.append(el('h4', 't-phase__titel', ph.name)); + pk.append(el('span', 't-phase__zeit', ph.zeit + ' · ' + ph.fertig + '/' + ph.gesamt)); + block.append(pk); + block.append(el('p', 't-phase__satz', ph.text)); + + for (const sch of pr.schritte.filter((x) => x.phase === ph.schluessel)) { + const zeile = el('div', 't-pschritt'); + if (sch.fertig) zeile.dataset.fertig = 'ja'; + + const knopf = el('button', 't-pschritt__haken', sch.fertig ? '✓' : ''); + knopf.type = 'button'; + knopf.setAttribute('aria-pressed', String(!!sch.fertig)); + knopf.setAttribute('aria-label', + (sch.fertig ? 'Zurücknehmen: ' : 'Abhaken: ') + sch.titel); + knopf.addEventListener('click', async () => { + knopf.disabled = true; + const r = await hole('/workspace/api/talente/' + k.id + '/probe/' + sch.schluessel, + { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: '{}' }); + if (!r.ok) { + const d = await r.json().catch(() => ({})); + melde(d.fehler || absage(d)); + knopf.disabled = false; + return; + } + await laden(); + }); + zeile.append(knopf); + + const text = el('div', 't-pschritt__text'); + const oben = el('p', 't-pschritt__titel'); + oben.append(document.createTextNode(sch.titel)); + oben.append(el('span', 't-pschritt__wer', sch.wer_name)); + text.append(oben); + text.append(el('p', 't-pschritt__satz', sch.text)); + /* WOFUER der Schritt da ist. Ein Haken, dessen Zweck man nicht + kennt, wird gesetzt, weil er dasteht. */ + text.append(el('p', 't-pschritt__wofuer', sch.wofuer)); + if (sch.fertig && sch.von) { + text.append(el('p', 't-pschritt__von', + 'abgehakt von ' + sch.von + (sch.am ? ' · ' + kurzDatum(sch.am) : ''))); + } + zeile.append(text); + block.append(zeile); + } + kasten.append(block); + } + + /* DIE DREI FRAGEN. Sie erscheinen erst, wenn man sie beantworten + kann -- vorher waeren sie eine Einladung, aus dem Bauch zu + entscheiden. */ + if (pr.entscheidbar) { + const f = el('div', 't-probe__fragen'); + f.append(el('h4', 't-phase__titel', 'Und jetzt – drei Fragen')); + for (const q of pr.fragen) { + const z = el('div', 't-frage'); + z.append(el('p', 't-frage__frage', q.frage)); + z.append(el('p', 't-frage__satz', q.text)); + f.append(z); + } + f.append(el('p', 't-probe__schluss', + 'Drei Mal Ja: übernehmen. Ein Mal Nein: sauber beenden und sagen, ' + + 'warum. Ein „weiß nicht“ heißt, dass noch etwas fehlt – dann ' + + 'nicht raten, sondern nachholen.')); + kasten.append(f); + } + + ziel.append(kasten); + } + function zeichne() { trichter(); const ziel = $('liste'); diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 1981296f..c85b0b55 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -12,19 +12,19 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + - +
@@ -291,10 +291,10 @@ - - - - - + + + + +