diff --git a/server/mess-vanvan-karte.mjs b/server/mess-vanvan-karte.mjs new file mode 100644 index 00000000..68f175ad --- /dev/null +++ b/server/mess-vanvan-karte.mjs @@ -0,0 +1,334 @@ +/* ===================================================================== + VANVANS MELDUNG AUS DEM SUPPORT (30.09.2026) + + „Wenn ich im Bereich eure Aufgaben runterscrolle und dort bei den 68 + Aufgaben … Aufgaben an einen Modi zutrage und auch bewerte, dann + sieht der Modi zwar die Auswertung im Bereich Entwicklung, aber bei + ihm taucht nichts im Bereich eure Aufgaben unten in der Karte auf. + Bei mir zum Beispiel steht in der Karte im Bereich eure Aufgaben + bei jedem Punkt läuft obwohl auch wenn die Aufgaben darüber gar + nicht zugetragen wurde." + + --------------------------------------------------------------------- + ZWEI FRAGEN, UND BEIDE LASSEN SICH NUR AM BILDSCHIRM BEANTWORTEN + + 1. Was sieht der MODI unten in seiner eigenen Karte, nachdem ihm + etwas zugetragen und bewertet wurde? + 2. Was steht bei der LEITUNG an einem Punkt, der gar nicht + zugetragen ist? + + Der Quelltext allein sagt das nicht. Er sagt, was gerendert wird -- + nicht, was ein Mensch daraus liest. Und genau darum geht es in ihrer + Meldung. + + --------------------------------------------------------------------- + DER AUFBAU IST IHR FALL, NICHT EIN AEHNLICHER + + Filipe (admin) und VanVan (hand) sind die beiden Beurteiler -- + genauso wie im Haus. Diene ist ein Modi. VanVan traegt ihm ZWEI von + 68 Punkten zu und bewertet sie; einen DRITTEN bewertet sie, ohne ihn + zuzutragen. Filipe bewertet ABSICHTLICH NICHTS -- denn genau so war + es bei ihr, und wenn das der Grund ist, muss er hier sichtbar + werden. + + Sie prueft nichts und meldet keinen Fehlschlag. Sie sagt, was auf + dem Bildschirm steht. + + Aufrufen mit: node server/mess-vanvan-karte.mjs + ===================================================================== */ + +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { portMussFreiSein } from "./helfer-port.mjs"; + +const PORT = await portMussFreiSein(5493, "mess-vanvan-karte"); +const ordner = mkdtempSync(join(tmpdir(), "ws-vanvan-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = String(PORT); +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; + +const express = (await import("express")).default; +const ec = express.response.cookie; +express.response.cookie = function (n, w, o) { + return ec.call(this, n, w, { ...(o || {}), secure: false }); +}; +import { notbremse } from "./helfer-notbremse.mjs"; +await import("./index.js"); +notbremse(180_000, "mess-vanvan-karte"); +await new Promise((r) => setTimeout(r, 800)); +const BASIS = `http://127.0.0.1:${PORT}`; + +process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f?.stack || f); process.exit(7); }); +process.on("unhandledRejection", (f) => { console.error("ABSTURZ:", f?.stack || f); process.exit(7); }); + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); +const { ENTWICKLUNG_FREMD } = await import("./workspace-entwicklungs-punkte.js"); + +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); +const geheim = randomBytes(32).toString("hex"); +d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") + .run("code_kennung_schluessel", geheim, jetzt); +function anlegen(name, rolle, code) { + const salz = randomBytes(16).toString("hex"); + const hash = scryptSync(code, salz, 64, + { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); + d.prepare("INSERT INTO personen" + + " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)" + + " VALUES (?,?,?,?,?,?,1,?)") + .run(name, rolle, hash, salz, 32768, + createHmac("sha256", geheim).update(code).digest("hex"), jetzt); + return d.prepare("SELECT last_insert_rowid() AS id").get().id; +} +const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001"); +const idVan = anlegen("VanVan", "hand", "CODE-VANVAN-01"); +const idModi = anlegen("Diene", "modi", "CODE-DIENE-001"); +d.close(); + +/* ---- Anmelden ---------------------------------------------------- */ +async function anmelden(rolle, code) { + const a = await fetch(BASIS + "/workspace/api/anmelden", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rolle, code, alter_ok: true }), + }); + return (a.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; "); +} +const kVan = await anmelden("hand", "CODE-VANVAN-01"); +const kModi = await anmelden("modi", "CODE-DIENE-001"); +const kDogi = await anmelden("admin", "CODE-DOGI-0001"); +if (!kVan || !kModi || !kDogi) { + console.error("KONNTE NICHT NACHSEHEN: die Anmeldung hat nicht geklappt."); + process.exit(1); +} +const ruf = (keks, weg, rumpf, art = "GET") => fetch(BASIS + weg, { + method: art, + headers: { "Content-Type": "application/json", Cookie: keks }, + body: rumpf === undefined ? undefined : JSON.stringify(rumpf), +}); + +/* ---- VanVans Handgriffe ------------------------------------------ */ +const P1 = ENTWICKLUNG_FREMD[0].schluessel; +const P2 = ENTWICKLUNG_FREMD[1].schluessel; +const P3 = ENTWICKLUNG_FREMD[2].schluessel; // bewertet, NICHT zugetragen + +console.log(""); +console.log("=== Was VanVan getan hat ==="); +for (const [p, wort] of [[P1, "zugetragen"], [P2, "zugetragen"]]) { + const a = await ruf(kVan, `/workspace/api/entwicklung/person/${idModi}/punkt/${p}`, + { an: true }, "PUT"); + console.log(` ${wort}: ${p} (HTTP ${a.status})`); +} +for (const p of [P1, P2, P3]) { + const a = await ruf(kVan, `/workspace/api/entwicklung/person/${idModi}/${p}`, + { stufe: "laeuft", anlass: "Im Stream gesehen, passt." }, "PUT"); + console.log(` bewertet „Läuft": ${p} (HTTP ${a.status})`); +} +console.log(` Filipe bewertet NICHTS -- genau wie in ihrem Fall.`); +console.log(` Von ${ENTWICKLUNG_FREMD.length} Punkten sind 2 zugetragen und 3 bewertet.`); + +/* ===================================================================== + FRAGE 1: WAS SIEHT DER MODI UNTEN IN SEINER KARTE? + ===================================================================== */ +console.log(""); +console.log("=== 1. Die eigene Karte des Modi (API) ==="); +{ + const k = await (await ruf(kModi, "/workspace/api/entwicklung/meine-karte")).json(); + console.log(` hat_karte=${k.hat_karte} Beurteiler=${k.beurteiler} ` + + `fertig=${k.fertig} von ${k.gesamt}`); + const alle = (k.bloecke || []).flatMap((b) => b.punkte); + const offen = alle.filter((p) => p.offen).length; + console.log(` ${offen} von ${alle.length} Punkten stehen auf „offen"`); + for (const s of [P1, P2, P3]) { + const p = alle.find((x) => x.schluessel === s); + console.log(` ${s.padEnd(26)} offen=${p?.offen} ` + + `zugetragen=${"zugetragen" in (p || {}) ? p.zugetragen : "(gibt es nicht)"} ` + + `antworten=${JSON.stringify(p?.antworten ?? null)}`); + } + /* DIE KERNFRAGE: Steht in seiner Karte ueberhaupt irgendwo, WELCHE + Punkte ihm als Aufgabe zugetragen wurden? */ + const kennt = alle.some((p) => "zugetragen" in p); + console.log(` Kennt seine Karte das Wort „zugetragen"? ${kennt ? "JA" : "NEIN"}`); +} + +/* ===================================================================== + FRAGE 2: WAS STEHT BEI VANVAN AN EINEM PUNKT? + ===================================================================== */ +console.log(""); +console.log("=== 2. VanVans Karte ueber Diene (API) ==="); +{ + const k = await (await ruf(kVan, `/workspace/api/entwicklung/person/${idModi}`)).json(); + const alle = (k.bloecke || []).flatMap((b) => b.punkte); + console.log(` ${alle.length} Punkte insgesamt, ` + + `${alle.filter((p) => p.zugetragen).length} davon zugetragen, ` + + `${alle.filter((p) => p.meins).length} bewertet, ` + + `${alle.filter((p) => p.schritt).length} mit einem Schritt`); + for (const s of [P1, P3]) { + const p = alle.find((x) => x.schluessel === s); + console.log(` ${s.padEnd(26)} zugetragen=${p?.zugetragen} ` + + `meins=${p?.meins?.stufe ?? "-"} schritt=${p?.schritt ? p.schritt.status : "-"}`); + } +} + +/* ===================================================================== + UND JETZT DER BILDSCHIRM + ===================================================================== */ +const { chromium } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); +const browser = await chromium.launch(); + +async function alsWer(keks, breite = 1280, hoehe = 1000) { + const kontext = await browser.newContext({ viewport: { width: breite, height: hoehe } }); + await kontext.addCookies(keks.split("; ").filter(Boolean).map((z) => { + const [n, ...r] = z.split("="); + return { name: n, value: r.join("="), domain: "127.0.0.1", path: "/" }; + })); + const seite = await kontext.newPage(); + const konsole = []; + seite.on("console", (e) => { if (e.type() === "error") konsole.push(e.text()); }); + seite.on("pageerror", (f) => konsole.push(String(f?.message || f))); + return { kontext, seite, konsole }; +} + +console.log(""); +console.log("=== 3. Was der Modi auf der Seite Eure Aufgaben WIRKLICH sieht ==="); +{ + const { kontext, seite, konsole } = await alsWer(kModi); + await seite.goto(`${BASIS}/workspace/entwicklung.html`, { waitUntil: "networkidle" }); + await seite.waitForTimeout(1500); + const w = await seite.evaluate(() => { + /* GEMESSEN WIRD IN SEINER KARTE, nicht auf der ganzen Seite. Das + Wort „Aufgabe" steht oben in der Kopfleiste und in der + Vorlagen-Kachel -- eine Suche ueber die ganze Seite haette + „ja" gesagt, ohne dass in der Karte irgendetwas steht. Genau + die Sorte gruener Haken, die nichts geprueft hat. */ + const karte = document.getElementById("karte-block"); + const auf = karte?.querySelector(".e-block--auftrag") || null; + return { + ueberschriften: [...document.querySelectorAll("h1, h2")] + .map((x) => x.textContent.trim()).filter(Boolean).slice(0, 12), + karteDa: !!karte && !karte.hidden, + auftragsblock: auf ? auf.querySelector(".e-block__titel")?.textContent.trim() : null, + aufgabenTitel: auf + ? [...auf.querySelectorAll(".e-punkt__titel")].map((x) => x.textContent.trim()) + : [], + aufgabenLage: auf + ? [...auf.querySelectorAll(".e-punkt__wann")].map((x) => x.textContent.trim()) + : [], + satz: (document.getElementById("karte-satz")?.textContent || "").trim().slice(0, 160), + punkte: karte ? karte.querySelectorAll(".e-punkt").length : 0, + }; + }); + console.log(` Ueberschriften: ${w.ueberschriften.join(" | ")}`); + console.log(` Karte sichtbar: ${w.karteDa}, Punktzeilen darin: ${w.punkte}`); + console.log(` Abschnitt: ${JSON.stringify(w.auftragsblock)}`); + console.log(` Seine Aufgaben: ${JSON.stringify(w.aufgabenTitel)}`); + console.log(` Lage dazu: ${JSON.stringify(w.aufgabenLage)}`); + console.log(` Satz: ${w.satz}`); + if (!w.aufgabenTitel.length) { + console.log(" ACHTUNG: In seiner Karte steht nicht, welche Punkte seine " + + "Aufgaben sind -- genau VanVans Meldung."); + } + await seite.screenshot({ path: "server/mess-vanvan-karte-modi.png", fullPage: true }); + console.log(" Bild: server/mess-vanvan-karte-modi.png"); + if (konsole.length) console.log(" KONSOLE:", konsole.slice(0, 3).join(" | ")); + await kontext.close(); +} + +console.log(""); +console.log("=== 4. Was VanVan an einem Punkt sieht ==="); +{ + const { kontext, seite, konsole } = await alsWer(kVan); + await seite.goto(`${BASIS}/workspace/entwicklung.html`, { waitUntil: "networkidle" }); + await seite.waitForTimeout(1200); + /* Dienes Kachel aufmachen. */ + const auf = await seite.evaluate((name) => { + const k = [...document.querySelectorAll("#personen .e-person")] + .find((x) => (x.querySelector(".e-person__name")?.textContent || "").includes(name)); + if (!k) return false; + (k.querySelector("button, a") || k).click(); + return true; + }, "Diene"); + console.log(` Dienes Kachel geoeffnet: ${auf}`); + await seite.waitForTimeout(2000); + + const z = await seite.evaluate(() => { + const zeilen = [...document.querySelectorAll(".e-punkt")].slice(0, 6).map((p) => ({ + titel: (p.querySelector(".e-punkt__titel")?.textContent || "").trim().slice(0, 34), + zugetragen: p.dataset.zugetragen, + auftrag: (p.querySelector(".e-punkt__auftrag")?.textContent || "").trim(), + /* WAS WIRKLICH AN DER ZEILE STEHT -- ohne die Knopfbeschriftungen + vorher zu kennen. Genau das ist die Frage: Was liest ein + Mensch dort? */ + woerter: (p.innerText || "").replace(/\s+/g, " ").trim().slice(0, 150), + gedrueckt: [...p.querySelectorAll("[aria-pressed='true']")] + .map((b) => b.textContent.trim()), + })); + return { + anzahl: document.querySelectorAll(".e-punkt").length, + zeilen, + /* Sieht SIE, dass es bei ihm noch nicht ankommt? */ + wartet: [...document.querySelectorAll(".e-punkt__wartet")] + .map((x) => x.textContent.trim()), + /* Und gibt es den Knopf, mit dem sie auf das Zugetragene + eingrenzen kann? */ + knoepfe: [...document.querySelectorAll(".e-stufen__knopf")] + .map((k) => `${k.querySelector(".e-stufen__name")?.textContent.trim()}` + + `=${k.querySelector(".e-stufen__zahl")?.textContent.trim()}`), + }; + }); + console.log(` ${z.anzahl} Punktzeilen sichtbar. Die ersten sechs:`); + for (const r of z.zeilen) { + console.log(` · ${r.titel.padEnd(36)} zugetragen=${r.zugetragen} ` + + `Knopf=„${r.auftrag}" gedrueckt=${JSON.stringify(r.gedrueckt)}`); + console.log(` „${r.woerter}"`); + } + console.log(` „Er sieht das noch nicht" steht an ${z.wartet.length} Punkten`); + if (z.wartet[0]) console.log(` „${z.wartet[0]}"`); + console.log(` Filterknoepfe: ${z.knoepfe.join(" ")}`); + if (!z.wartet.length) { + console.log(" ACHTUNG: Sie hat bewertet, es kommt beim Modi nicht an, und " + + "nichts auf ihrem Bildschirm sagt ihr das."); + } + await seite.screenshot({ path: "server/mess-vanvan-karte-leitung.png", fullPage: true }); + console.log(" Bild: server/mess-vanvan-karte-leitung.png"); + + /* ---- Und der Filter: grenzt er wirklich ein? ------------------ + Ohne diesen Griff hiesse „der Knopf ist da" nur, dass ein Knopf + da ist. */ + const nachFilter = await seite.evaluate(() => { + const k = [...document.querySelectorAll(".e-stufen__knopf")] + .find((x) => x.querySelector(".e-stufen__name")?.textContent.trim() === "Zugetragen"); + if (!k) return null; + k.click(); + return true; + }); + if (nachFilter) { + await seite.waitForTimeout(1200); + const n = await seite.evaluate(() => ({ + punkte: document.querySelectorAll(".e-punkt").length, + alleZugetragen: [...document.querySelectorAll(".e-punkt")] + .every((p) => p.dataset.zugetragen === "ja"), + })); + console.log(` Nach „Zugetragen": ${n.punkte} Punkte, ` + + `alle zugetragen=${n.alleZugetragen}`); + if (n.punkte !== 2 || !n.alleZugetragen) { + console.log(" ACHTUNG: Der Filter grenzt nicht auf das Zugetragene ein."); + } + await seite.screenshot({ path: "server/mess-vanvan-karte-filter.png", fullPage: true }); + console.log(" Bild: server/mess-vanvan-karte-filter.png"); + } else { + console.log(` ACHTUNG: Den Knopf „Zugetragen“ gibt es nicht.`); + } + if (konsole.length) console.log(" KONSOLE:", konsole.slice(0, 3).join(" | ")); + await kontext.close(); +} + +await browser.close(); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Rest im Temp */ } +console.log(""); +console.log("Fertig."); +process.exit(0); diff --git a/server/pruef-entwicklung.mjs b/server/pruef-entwicklung.mjs index c438c92d..f611ae5b 100644 --- a/server/pruef-entwicklung.mjs +++ b/server/pruef-entwicklung.mjs @@ -551,6 +551,102 @@ console.log("\n=== Punkte je Person aussuchen ==="); `und alles wieder wegnehmen geht ebenfalls (${danach?.gesamt})`); } +/* ======================================================================= + VANVANS MELDUNG AUS DEM SUPPORT (30.09.2026) + + „Wenn ich … Aufgaben an einen Modi zutrage und auch bewerte, dann + sieht der Modi zwar die Auswertung im Bereich Entwicklung, aber bei + ihm taucht nichts im Bereich eure Aufgaben unten in der Karte auf." + + Nachgestellt im Browser (mess-vanvan-karte) war es genau so: Seine + Karte lieferte alle 68 Punkte des Katalogs und kein Wort darueber, + welche davon ihm zugetragen wurden -- und weil noch nicht alle + Beurteiler gesetzt hatten, stieg die Anzeige ganz aus. + + ZWEI GETRENNTE DINGE, die vorher eins waren: + * WAS ER TUN SOLL -- die Zuteilung. Die gibt es, bevor jemand + etwas bewertet, und sie gehoert ihm. + * WAS MAN SIEHT -- die Bewertung. Die erscheint erst, wenn + alle hingesehen haben, und das bleibt so. + ======================================================================= */ +console.log(""); +console.log("=== VanVans Meldung: die eigene Karte kennt die Aufgaben ==="); +{ + const PA = "e-chat-neue-begruesst"; + const PB = "e-chat-fragen-beantwortet"; + + /* Sauberer Ausgangspunkt -- die Abschnitte davor haben gesetzt. */ + await hole(`/workspace/api/entwicklung/person/${idModi}/punkte`, + { host: CREW, keks: dogi.keks, methode: "PUT", + rumpf: JSON.stringify({ punkte: [PA, PB] }) }); + + const meine = async () => (await hole("/workspace/api/entwicklung/meine-karte", + { host: CREW, keks: ayla.keks })).daten; + + const m0 = await meine(); + const alle0 = (m0?.bloecke || []).flatMap((b) => b.punkte); + ok(alle0.some((p) => p.schluessel === PA && p.zugetragen === true), + "die eigene Karte weiss, welche Punkte zugetragen sind"); + ok(m0?.meine_aufgaben === 2, + `und sagt, wie viele es sind (${m0?.meine_aufgaben})`); + + /* DER KERN DER MELDUNG: Er sieht seine Aufgabe, BEVOR irgendjemand + sie bewertet hat. Vorher war das umgekehrt -- ohne vollstaendige + Bewertung sah er gar nichts. */ + ok(alle0.find((p) => p.schluessel === PA)?.offen === true, + "sie steht da, obwohl noch niemand etwas dazu gesetzt hat"); + + /* GEGENPROBE: Ein Punkt, der NICHT zugetragen ist, traegt die Marke + auch nicht. Ohne sie hiesse „zugetragen=true" womoeglich nur, + dass dort immer `true` steht. */ + const fremd = alle0.find((p) => p.schluessel !== PA && p.schluessel !== PB); + ok(fremd && fremd.zugetragen === false, + `ein nicht zugetragener Punkt traegt die Marke nicht (${fremd?.schluessel})`); + + /* ---- Und die andere Haelfte: sieht die Leitung, dass es bei ihm + noch nicht ankommt? ------------------------------------- */ + await hole(`/workspace/api/entwicklung/person/${idModi}/${PA}`, + { host: CREW, keks: hand.keks, methode: "PUT", + rumpf: JSON.stringify({ stufe: "laeuft", anlass: "Im Stream gesehen." }) }); + + const sicht = async (keks) => { + const k = (await hole(`/workspace/api/entwicklung/person/${idModi}`, + { host: CREW, keks })).daten; + return (k?.bloecke || []).flatMap((b) => b.punkte).find((p) => p.schluessel === PA); + }; + + const beiVan = await sicht(hand.keks); + ok(beiVan?.sieht_er && beiVan.sieht_er.da === 1 && beiVan.sieht_er.noetig === 2, + `VanVan sieht: eine von zwei Einschaetzungen liegt vor ` + + `(${JSON.stringify(beiVan?.sieht_er)})`); + + const m1 = await meine(); + ok((m1?.bloecke || []).flatMap((b) => b.punkte) + .find((p) => p.schluessel === PA)?.offen === true, + "und bei ihm ist der Punkt weiterhin offen -- eine halbe Karte waere schlimmer"); + + /* GEGENPROBE, OHNE DIE DIE ZWEI ZEILEN OBEN NICHTS BEWEISEN: + Setzt der zweite Beurteiler auch, geht es durch -- und die + Leitung sieht das ebenfalls. */ + await hole(`/workspace/api/entwicklung/person/${idModi}/${PA}`, + { host: CREW, keks: dogi.keks, methode: "PUT", + rumpf: JSON.stringify({ stufe: "laeuft", anlass: "Sehe ich auch so." }) }); + + const beiVan2 = await sicht(hand.keks); + ok(beiVan2?.sieht_er?.da === 2, + `nach der zweiten Einschaetzung steht es auf 2 von 2 ` + + `(${JSON.stringify(beiVan2?.sieht_er)})`); + + const m2 = await meine(); + const pa2 = (m2?.bloecke || []).flatMap((b) => b.punkte) + .find((p) => p.schluessel === PA); + ok(pa2?.offen === false && pa2?.zugetragen === true, + `jetzt sieht er die Einschaetzung -- und es ist weiterhin seine Aufgabe ` + + `(offen=${pa2?.offen}, zugetragen=${pa2?.zugetragen})`); + ok(Array.isArray(pa2?.antworten) && pa2.antworten[0]?.anzahl === 2, + `mit beiden Antworten (${JSON.stringify(pa2?.antworten)})`); +} + d.close(); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } console.log(""); diff --git a/server/workspace-entwicklung.js b/server/workspace-entwicklung.js index 29ba859e..61f9deb7 100644 --- a/server/workspace-entwicklung.js +++ b/server/workspace-entwicklung.js @@ -731,6 +731,10 @@ entwicklungRouter.get("/workspace/api/entwicklung/person/:id", nurLeitung, (req, `gesetzt`/`gesamt`): „0 von 68" war die Zahl, die ihn gestoert hat, und sie sagt bei jemandem mit drei Aufgaben nichts. */ const zugeteilt = zugeteiltFuer(id); + /* EINMAL, NICHT JE PUNKT. Beim ersten Anlauf stand der Aufruf + drinnen in der Schleife -- 68 Datenbankfragen fuer eine Zahl, + die sich waehrend eines Kartenaufrufs nicht aendert. */ + const noetigeStimmen = beurteilerZahl(p.id); const bloecke = katalogFuer(req.person, false).map((b) => ({ ...b, punkte: b.punkte.map((pt) => { @@ -749,6 +753,27 @@ entwicklungRouter.get("/workspace/api/entwicklung/person/:id", nurLeitung, (req, nicht" dasselbe Bild, und das erste ist eine Auskunft, das zweite eine Taeuschung. */ andere_offen: m ? 0 : fremde.filter((f) => f.schluessel === pt.schluessel).length, + /* ==== SIEHT ER ES SCHON? (30.09.2026) ===================== + + VanVan aus dem Support: Sie hatte bewertet, und beim Modi + kam nichts an. Der Grund ist die Regel unten in + `meine-karte`: Ein Punkt erscheint bei ihm erst, wenn + ALLE Beurteiler gesetzt haben. + + Die Regel bleibt -- eine einzelne Meinung soll bei einem + Menschen nicht als Urteil des Teams ankommen. Was fehlte, + war die Rueckmeldung an die, die schon gesetzt haben: + VanVan hat getan, was sie tun sollte, und nichts auf + ihrem Bildschirm sagte ihr, dass es liegen bleibt. + + GEZAEHLT WIRD GEGEN DIESELBE LISTE wie dort, nicht gegen + eine eigene -- sonst sagen die beiden Seiten irgendwann + Verschiedenes ueber denselben Punkt. */ + sieht_er: (() => { + const wieViele = (m ? 1 : 0) + + fremde.filter((f) => f.schluessel === pt.schluessel).length; + return { da: wieViele, noetig: noetigeStimmen }; + })(), /* DER SCHRITT ZU DIESEM PUNKT. Er steht an der Karte und nicht auf einer eigenen Liste: Wer die Beobachtung sieht, soll im selben Blick sehen, ob daraus etwas geworden ist. */ @@ -893,6 +918,22 @@ entwicklungRouter.get("/workspace/api/entwicklung/person/:id", nurLeitung, (req, * erinnert, und weit genug, dass sich etwas ändern kann. */ export const SCHRITT_TAGE = 14; +/** Wie viele Menschen urteilen ueber diese Person? + * + * AN EINER STELLE (30.09.2026). Dieselbe Frage wird an zwei Enden + * gestellt: `meine-karte` entscheidet damit, ob ein Punkt bei dem + * Menschen ueberhaupt erscheint, und die Karte der Leitung sagt + * damit, ob er ihn schon sieht. Zwei Abschriften waeren die, die + * irgendwann Verschiedenes ueber denselben Punkt sagen. + * + * ALLE AUSSER IHM SELBST: Wer sich selbst einschaetzt, ist keine + * zweite Meinung. */ +function beurteilerZahl(personId) { + return db().prepare( + "SELECT COUNT(*) AS n FROM personen WHERE rolle IN ('admin','hand') AND aktiv = 1 AND id <> ?") + .get(personId)?.n || 0; +} + /** Die Schritte zu den Punkten einer Person — einer je Punkt. * * ES IST IMMER DER JÜNGSTE. Wer denselben Punkt ein zweites Mal @@ -1278,7 +1319,10 @@ entwicklungRouter.get("/workspace/api/entwicklung/meine-karte", (req, res) => { return res.json({ hat_karte: false }); } - /* Wer ueber diese Person urteilt -- alle ausser ihr selbst. */ + /* Wer ueber diese Person urteilt -- alle ausser ihr selbst. + DIE ZAHL KOMMT AUS `beurteilerZahl()`, die Liste von hier: Die + Karte der Leitung stellt dieselbe Frage, und zwei Abschriften + waeren die, die irgendwann Verschiedenes sagen. */ const beurteiler = db().prepare( "SELECT id FROM personen WHERE rolle IN ('admin','hand') AND aktiv = 1 AND id <> ?") .all(req.person.id).map((z) => z.id); @@ -1293,6 +1337,20 @@ entwicklungRouter.get("/workspace/api/entwicklung/meine-karte", (req, res) => { nachPunkt.get(z.schluessel).push(z); } + /* ==== WELCHE PUNKTE SIND SEINE AUFGABE? (30.09.2026) ========= + + VanVan aus dem Support: „dann sieht der Modi zwar die + Auswertung im Bereich Entwicklung, aber bei ihm taucht nichts + im Bereich eure Aufgaben unten in der Karte auf." + + Nachgemessen war es genau so: Seine Karte lieferte alle 68 + Punkte des Katalogs und kein Wort darueber, welche davon ihm + zugetragen wurden. Dabei ist das die eigentliche Auskunft -- + die Bewertung ist die Folge, nicht der Gegenstand. Wer nicht + weiss, was seine Aufgaben sind, kann sie auch nicht + angehen. */ + const meineAufgaben = zugeteiltFuer(req.person.id); + const bloecke = ENTWICKLUNG_BLOECKE .filter((b) => !b.nurSelbst) .map((b) => ({ @@ -1301,13 +1359,27 @@ entwicklungRouter.get("/workspace/api/entwicklung/meine-karte", (req, res) => { const liste = nachPunkt.get(pt.schluessel) || []; /* VOLLSTAENDIG heisst: jeder Beurteiler hat gesetzt. */ const fertig = beurteiler.length > 0 && liste.length >= beurteiler.length; + /* DIE ZUTEILUNG HAENGT NICHT AN DER BEWERTUNG. Sie geht + auch dann mit, wenn noch niemand etwas gesetzt hat -- + sonst saehe er seine Aufgabe erst, wenn sie schon + beurteilt ist, und das ist die falsche Reihenfolge. */ + const zugetragen = meineAufgaben.has(pt.schluessel); if (!fertig) { - return { schluessel: pt.schluessel, titel: pt.titel, stufe: pt.stufe, offen: true }; + return { + schluessel: pt.schluessel, titel: pt.titel, stufe: pt.stufe, + offen: true, zugetragen, + /* WIE VIELE NOCH FEHLEN. „Noch nichts fertig" allein + sieht aus wie ein Fehler; „einer von zwei hat + gesetzt" ist eine Auskunft. */ + davon_da: liste.length, + }; } const zaehler = {}; for (const z of liste) zaehler[z.stufe] = (zaehler[z.stufe] || 0) + 1; return { schluessel: pt.schluessel, titel: pt.titel, stufe: pt.stufe, offen: false, + zugetragen, + davon_da: liste.length, /* Nach der Reihenfolge des Katalogs, nicht nach Haeufigkeit -- sonst stuende bei einer Abweichung mal das eine, mal das andere zuerst, und das liest sich wie eine Wertung. */ @@ -1326,6 +1398,10 @@ entwicklungRouter.get("/workspace/api/entwicklung/meine-karte", (req, res) => { beurteiler: beurteiler.length, fertig: alle.filter((x) => !x.offen).length, gesamt: alle.length, + /* WIE VIELE PUNKTE SEINE AUFGABE SIND. Die Zahl steht oben an + der Karte -- „2 Aufgaben" ist etwas anderes als „0 von 68", + und das zweite hat Filipe schon einmal zu Recht gestoert. */ + meine_aufgaben: alle.filter((x) => x.zugetragen).length, stufen: ENTWICKLUNG_STUFEN, erwartung: ENTWICKLUNG_ERWARTUNG, bloecke, diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index f29b4de0..d0668401 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,8 +16,8 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +