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`. -->
-
-
+
+
-
+