diff --git a/server/pruef-entwicklung.mjs b/server/pruef-entwicklung.mjs
index 2120f33e..262ec0d2 100644
--- a/server/pruef-entwicklung.mjs
+++ b/server/pruef-entwicklung.mjs
@@ -384,6 +384,105 @@ console.log("=== Die Kacheln ===");
ok(new Set(toene).size === toene.length, `kein Farbton doppelt (${toene.length} Kacheln)`);
}
+/* =====================================================================
+ WELCHE PUNKTE GELTEN FUER DIESEN MENSCHEN (25.09.2026)
+ =====================================================================
+
+ Filipe: „ich will dass die rechte hand und ich diese aufgaben die es
+ da gibt, individuel aussuchen koennen wer welche aufgabe bekommt.
+ bis dahin sollen die keine aufgaben sehen. nur die, die die rechte
+ hand oder dogfather ihnen zutragen."
+
+ BIS HEUTE GALTEN ALLE 68 FUER JEDEN. Geprueft wird deshalb zuerst
+ das Wichtigste an seinem Satz -- „bis dahin sollen die keine
+ aufgaben sehen": Ohne Zuteilung ist die Karte LEER. Ohne diese
+ Zeile waere alles Folgende auch dann gruen, wenn weiterhin alles
+ fuer jeden gilt.
+ ===================================================================== */
+console.log("\n=== Punkte je Person aussuchen ===");
+{
+ const karte = async (keks) => (await hole(
+ `/workspace/api/entwicklung/person/${idModi}`, { host: CREW, keks })).daten;
+
+ const vorher = await karte(dogi.keks);
+ ok(vorher && vorher.gesamt === 0,
+ `ohne Zuteilung steht auf der Karte nichts (${vorher?.gesamt} Punkte)`);
+ ok((vorher?.gesamt_katalog || 0) >= 60,
+ `der Katalog hat trotzdem alle Punkte (${vorher?.gesamt_katalog})`);
+
+ /* DIE LISTE ZUM AUSSUCHEN zeigt ALLES, mit einem Haken an dem, was
+ gilt. Zeigte sie nur das Zugeteilte, koennte man nie etwas
+ hinzufuegen -- ein Auswahlfenster, in dem die Auswahl fehlt. */
+ const wahl = (await hole(`/workspace/api/entwicklung/person/${idModi}/punkte`,
+ { host: CREW, keks: dogi.keks })).daten;
+ const alle = (wahl?.bloecke || []).flatMap((b) => b.punkte);
+ ok(alle.length === vorher?.gesamt_katalog,
+ `die Auswahl zeigt den ganzen Katalog (${alle.length})`);
+ ok(alle.every((x) => x.dabei === false),
+ `und nichts davon ist angehakt (${alle.filter((x) => x.dabei).length} angehakt)`);
+
+ /* ---- Zwoelf aussuchen ------------------------------------------- */
+ const zwoelf = alle.slice(0, 12).map((x) => x.schluessel);
+ const gesetzt = await hole(`/workspace/api/entwicklung/person/${idModi}/punkte`,
+ { host: CREW, keks: dogi.keks, methode: "PUT",
+ rumpf: JSON.stringify({ punkte: zwoelf }) });
+ ok(gesetzt.status === 200 && gesetzt.daten?.zugeteilt === 12,
+ `DogFather sucht zwoelf aus (HTTP ${gesetzt.status}, ${gesetzt.daten?.zugeteilt})`);
+
+ const nachher = await karte(dogi.keks);
+ const drin = (nachher?.bloecke || []).flatMap((b) => b.punkte).map((x) => x.schluessel);
+ ok(nachher?.gesamt === 12 && drin.length === 12,
+ `und danach stehen genau die zwoelf auf der Karte (${nachher?.gesamt})`);
+ ok(zwoelf.every((s) => drin.includes(s)),
+ " und zwar dieselben, nicht irgendwelche zwoelf");
+
+ /* DIE RECHTE HAND SIEHT DASSELBE -- sie beurteilt denselben
+ Menschen, und zwei verschiedene Listen fuer dieselbe Person waeren
+ der Grund fuer jede Abweichung, die keine ist. */
+ const beiHand = await karte(hand.keks);
+ ok(beiHand?.gesamt === 12,
+ `die rechte Hand sieht dieselben zwoelf (${beiHand?.gesamt})`);
+
+ /* UND SIE DARF SELBST AUSSUCHEN -- Filipes Satz nennt beide. */
+ const vonHand = await hole(`/workspace/api/entwicklung/person/${idModi}/punkte`,
+ { host: CREW, keks: hand.keks, methode: "PUT",
+ rumpf: JSON.stringify({ punkte: alle.slice(0, 5).map((x) => x.schluessel) }) });
+ ok(vonHand.status === 200 && vonHand.daten?.zugeteilt === 5,
+ `die rechte Hand sucht ebenfalls aus (HTTP ${vonHand.status}, ${vonHand.daten?.zugeteilt})`);
+
+ /* ---- Gegenproben ------------------------------------------------- */
+ /* EIN ERFUNDENER SCHLUESSEL WIRD VERWORFEN. Ohne diesen Abgleich
+ liesse sich beliebiger Text in die Tabelle schreiben -- er stuende
+ danach in keiner Liste und waere auch nicht wegzubekommen. */
+ const mitMuell = await hole(`/workspace/api/entwicklung/person/${idModi}/punkte`,
+ { host: CREW, keks: dogi.keks, methode: "PUT",
+ rumpf: JSON.stringify({ punkte: [...zwoelf, "gibt_es_nicht", "auch_nicht"] }) });
+ ok(mitMuell.daten?.zugeteilt === 12,
+ `Gegenprobe: erfundene Schluessel fallen weg (${mitMuell.daten?.zugeteilt} statt 14)`);
+
+ /* EIN MODI DARF NICHT AUSSUCHEN -- weder fuer sich noch fuer
+ andere. Ohne diese Zeile hiesse „DogFather darf" nur, dass
+ ueberhaupt jemand darf. */
+ const vonModi = await hole(`/workspace/api/entwicklung/person/${idModi}/punkte`,
+ { host: CREW, keks: ayla.keks, methode: "PUT",
+ rumpf: JSON.stringify({ punkte: [] }) });
+ ok(vonModi.status >= 400,
+ `Gegenprobe: ein Modi sucht nicht aus (${vonModi.status})`);
+ const nochDa = await karte(dogi.keks);
+ ok(nochDa?.gesamt === 12,
+ ` und nach dem Versuch stehen die zwoelf noch (${nochDa?.gesamt})`);
+
+ /* ALLES WEGNEHMEN GEHT AUCH. Eine Auswahl, die man nur erweitern
+ kann, waere eine Falle: Wer sich vertut, bekaeme den Punkt nie
+ wieder los. */
+ const leer = await hole(`/workspace/api/entwicklung/person/${idModi}/punkte`,
+ { host: CREW, keks: dogi.keks, methode: "PUT",
+ rumpf: JSON.stringify({ punkte: [] }) });
+ const danach = await karte(dogi.keks);
+ ok(leer.status === 200 && danach?.gesamt === 0,
+ `und alles wieder wegnehmen geht ebenfalls (${danach?.gesamt})`);
+}
+
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 2ed87fa5..767f0e49 100644
--- a/server/workspace-entwicklung.js
+++ b/server/workspace-entwicklung.js
@@ -207,6 +207,76 @@ export function tabellen() {
entstand, wie ein Tag aus, an dem 43 Dinge gleichzeitig passiert
sind.
============================================================= */
+ /* =================================================================
+ WELCHE PUNKTE GELTEN FUER DIESEN MENSCHEN? (25.09.2026)
+ =================================================================
+
+ Filipe: „ich will dass die rechte hand und ich diese aufgaben die
+ es da gibt, individuel aussuchen koennen wer welche aufgabe
+ bekommt. bis dahin sollen die keine aufgaben sehen. nur die, die
+ die rechte hand oder dogfather ihnen zutragen."
+
+ BIS HEUTE GALTEN ALLE 68 FUER JEDEN. Das war bequem und in der
+ Sache falsch: „Clips, Schnitt und Kommentare" gehoert nicht zu
+ jemandem, der nur im Chat moderiert -- und ein Punkt, der nie
+ zutrifft, steht trotzdem in der Zaehlung („0 von 68"). Wer eine
+ Liste sieht, in der ein Drittel nie passt, liest die ganze Liste
+ weniger ernst.
+
+ EINE ZEILE JE PAAR, KEINE LISTE IN EINER SPALTE: Eine
+ kommagetrennte Liste in personen waere schneller gebaut und
+ liesse sich nicht abfragen („wer hat diesen Punkt?"), nicht
+ zaehlen und nicht mit einem Fremdschluessel absichern.
+
+ WER UND WANN stehen mit drin. Nicht fuer die Anzeige -- fuer die
+ Frage „seit wann gilt das eigentlich fuer ihn", die
+ erfahrungsgemaess in dem Moment kommt, in dem jemand sie nicht
+ mehr beantworten kann. */
+ CREATE TABLE IF NOT EXISTS entwicklung_zuteilung (
+ person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
+ schluessel TEXT NOT NULL,
+ seit TEXT NOT NULL,
+ von_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
+ PRIMARY KEY (person_id, schluessel)
+ );
+ CREATE INDEX IF NOT EXISTS idx_entw_zuteilung_person
+ ON entwicklung_zuteilung (person_id);
+
+ /* =================================================================
+ EINMALIG: WAS SCHON BEURTEILT WURDE, BLEIBT SICHTBAR
+ =================================================================
+
+ Ab heute zeigt die Karte nur noch zugeteilte Punkte. Fuer jemanden,
+ bei dem noch nichts ausgesucht ist, heisst das „leer" -- und genau
+ das hat Filipe gewollt („bis dahin sollen die keine aufgaben
+ sehen").
+
+ FUER BESTEHENDE BEURTEILUNGEN WAERE ES ABER DATENVERLUST AUF DEM
+ BILDSCHIRM: Wer zwanzig Punkte gesetzt hat, saehe am naechsten
+ Morgen eine leere Karte -- die Daten liegen noch da, man kommt nur
+ nicht mehr hin. Das sieht aus wie ein Fehler und ist einer.
+
+ DESHALB WIRD EINMALIG UEBERNOMMEN, wozu es schon eine
+ Einschaetzung gibt. Nicht mehr: Ein Punkt, den nie jemand gesetzt
+ hat, war auch vorher nur Papier.
+
+ Ein INSERT, das Vorhandenes stehen laesst, UND EIN FLAG: Das Flag verhindert, dass die
+ Uebernahme wiederkommt, nachdem jemand bewusst alles abgewaehlt
+ hat -- sonst waere „ich will diesen Punkt nicht mehr" nach dem
+ naechsten Neustart rueckgaengig gemacht, und niemand wuesste,
+ warum. */
+ INSERT INTO entwicklung_zuteilung (person_id, schluessel, seit, von_id)
+ SELECT DISTINCT s.person_id, s.schluessel, MIN(s.am), NULL
+ FROM entwicklung_stand s
+ WHERE NOT EXISTS (SELECT 1 FROM einstellungen
+ WHERE schluessel = 'entwicklung_zuteilung_uebernommen')
+ GROUP BY s.person_id, s.schluessel
+ ON CONFLICT(person_id, schluessel) DO NOTHING;
+
+ INSERT INTO einstellungen (schluessel, wert, geaendert, von)
+ VALUES ('entwicklung_zuteilung_uebernommen', 'ja', datetime('now'), NULL)
+ ON CONFLICT(schluessel) DO NOTHING;
+
CREATE TABLE IF NOT EXISTS entwicklung_lauf (
id INTEGER PRIMARY KEY AUTOINCREMENT,
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
@@ -488,6 +558,40 @@ function katalogFuer(person, ueberMichSelbst) {
.map((b) => ({ ...b, punkte: ENTWICKLUNG_PUNKTE[b.schluessel] || [] }));
}
+/* ===== WELCHE PUNKTE GELTEN FUER DIESEN MENSCHEN? (25.09.2026) ======
+
+ Filipe: „ich will dass die rechte hand und ich diese aufgaben die es
+ da gibt, individuel aussuchen koennen wer welche aufgabe bekommt.
+ bis dahin sollen die keine aufgaben sehen."
+
+ EINE STELLE FUER DIE ANTWORT, drei Aufrufer: die Karte der Leitung,
+ die Karte des Menschen selbst und die Uebersicht mit den Zahlen.
+ Drei Abschriften waeren drei Gelegenheiten, dass eine davon beim
+ naechsten Umbau nicht mitzieht -- und dann stuende in der Uebersicht
+ „12 von 68", waehrend in der Karte elf Punkte stehen.
+
+ EIN SET UND KEINE LISTE: Gefragt wird 68-mal je Person („gehoert
+ dieser Punkt dazu?"). In einer Liste ist das 68 x N Vergleiche, im
+ Set 68 Nachschlagevorgaenge. */
+function zugeteiltFuer(personId) {
+ return new Set(db().prepare(
+ "SELECT schluessel FROM entwicklung_zuteilung WHERE person_id = ?")
+ .all(personId).map((z) => z.schluessel));
+}
+
+/* Der Katalog, auf das eingegrenzt, was fuer DIESEN Menschen gilt.
+ `nurSelbst`-Bloecke bleiben unberuehrt: Das ist der Block, den die
+ Person selbst beantwortet (warum Moderatoren aufhoeren), er gehoert
+ niemandem sonst und ist nicht Teil der 68. */
+function katalogFuerPerson(person, personId, ueberMichSelbst) {
+ const dabei = zugeteiltFuer(personId);
+ return katalogFuer(person, ueberMichSelbst)
+ .map((b) => (b.nurSelbst
+ ? b
+ : { ...b, punkte: b.punkte.filter((pt) => dabei.has(pt.schluessel)) }))
+ .filter((b) => b.nurSelbst || b.punkte.length);
+}
+
entwicklungRouter.get("/workspace/api/entwicklung/lage", (req, res) => {
try {
const leitung = fuehrtTeamDogi(req.person);
@@ -514,6 +618,15 @@ entwicklungRouter.get("/workspace/api/entwicklung/lage", (req, res) => {
.all(req.person.id, new Date(Date.now() - VERBLASST_NACH_TAGEN * TAG).toISOString());
const alteZahl = new Map(alt.map((z) => [z.person_id, z.n]));
+ /* WIE VIELE PUNKTE GELTEN JE PERSON (25.09.2026). Ohne diese Zahl
+ stuende in der Uebersicht weiter „von 68" -- und in der Karte
+ daneben „von 12". Zwei Zahlen fuer dieselbe Sache, und die
+ Uebersicht ist die, die man zuerst sieht. */
+ const zugeteilt = db().prepare(
+ "SELECT person_id, COUNT(*) AS n FROM entwicklung_zuteilung GROUP BY person_id")
+ .all();
+ const zuteilZahl = new Map(zugeteilt.map((z) => [z.person_id, z.n]));
+
const anders = db().prepare(
`SELECT a.person_id, COUNT(*) AS n
FROM entwicklung_stand a JOIN entwicklung_stand b
@@ -544,6 +657,10 @@ entwicklungRouter.get("/workspace/api/entwicklung/lage", (req, res) => {
gesetzt: meineZahl.get(p.id) || 0,
veraltet: alteZahl.get(p.id) || 0,
abweichungen: andersZahl.get(p.id) || 0,
+ /* Wie viele Punkte fuer sie ausgesucht sind. `0` heisst
+ ausdruecklich „noch keine" und nicht „keine Angabe" -- die
+ Oberflaeche sagt dann „Punkte auswaehlen", nicht „0 von 68". */
+ zugeteilt: zuteilZahl.get(p.id) || 0,
})),
/* Die eigene Karte -- fuer jeden, der im Team ist. */
ich: { id: req.person.id, name: req.person.name },
@@ -576,7 +693,9 @@ entwicklungRouter.get("/workspace/api/entwicklung/person/:id", nurLeitung, (req,
const grenze = Date.now() - VERBLASST_NACH_TAGEN * TAG;
const schritte = schritteVon(p.id);
- const bloecke = katalogFuer(req.person, false).map((b) => ({
+ /* NUR, WAS FUER IHN GILT (25.09.2026). Vorher stand hier
+ `katalogFuer` -- alle 68 fuer jeden. */
+ const bloecke = katalogFuerPerson(req.person, id, false).map((b) => ({
...b,
punkte: b.punkte.map((pt) => {
const m = meine.get(pt.schluessel) || null;
@@ -662,8 +781,24 @@ entwicklungRouter.get("/workspace/api/entwicklung/person/:id", nurLeitung, (req,
erwartung: ENTWICKLUNG_ERWARTUNG,
bloecke,
abweichungen,
- gesetzt: meine.size,
- gesamt: ENTWICKLUNG_FREMD.length,
+ /* „X VON Y" ZAEHLT AB HEUTE DAS ZUGETEILTE (25.09.2026).
+
+ `ENTWICKLUNG_FREMD.length` sind alle 68 -- die Zahl war damit
+ fuer jeden gleich, egal wie viele Punkte ihn ueberhaupt
+ betreffen. „0 von 68" bei jemandem, fuer den zwoelf gelten,
+ ist keine Auskunft, sondern eine Entmutigung.
+
+ `meine.size` musste ebenfalls nachgezogen werden: Wer frueher
+ zu einem Punkt gesetzt hat, der ihm inzwischen nicht mehr
+ zugeteilt ist, haette sonst „13 von 12" bekommen. Gezaehlt
+ wird deshalb nur, was auch in der Liste steht. */
+ gesetzt: bloecke.reduce((n, b) => n
+ + b.punkte.filter((pt) => pt.meins).length, 0),
+ gesamt: bloecke.reduce((n, b) => n + b.punkte.length, 0),
+ /* Wie viele es insgesamt GAEBE -- die Oberflaeche sagt damit
+ „12 von 68 ausgewaehlt", ohne die Liste selbst zu kennen. */
+ gesamt_katalog: ENTWICKLUNG_FREMD.length,
+ darf_zuteilen: true,
});
} catch (f) {
console.error("[entwicklung] Person:", f?.message);
@@ -729,6 +864,93 @@ function schritteVon(personId) {
return nach;
}
+/* =====================================================================
+ WELCHE PUNKTE GELTEN FUER DIESEN MENSCHEN -- LESEN UND SETZEN
+ =====================================================================
+
+ Filipe: „ich will dass die rechte hand und ich diese aufgaben die es
+ da gibt, individuel aussuchen koennen wer welche aufgabe bekommt."
+
+ ZWEI WEGE UND NICHT EINER MIT SCHALTER: Lesen liefert den GANZEN
+ Katalog mit einem Haken an dem, was gilt -- man waehlt nur aus, was
+ man sieht. Die Karte selbst liefert weiterhin nur das Zugeteilte;
+ die zwei Fragen sind verschieden und bleiben getrennt.
+
+ GESETZT WIRD DIE GANZE LISTE AUF EINMAL, nicht Punkt fuer Punkt. Wer
+ zwoelf Haken setzt und dabei die Verbindung verliert, haette sonst
+ sieben gesetzte und fuenf verlorene -- und saehe nicht, welche. So
+ ist es eine Entscheidung, und sie gilt ganz oder gar nicht.
+ ===================================================================== */
+entwicklungRouter.get("/workspace/api/entwicklung/person/:id/punkte",
+ nurLeitung, (req, res) => {
+ try {
+ const id = nummer(req.params.id);
+ const p = id && db().prepare("SELECT id, name, rolle FROM personen WHERE id = ?").get(id);
+ if (!p || !KARTEN_ROLLEN.includes(p.rolle)) {
+ return res.status(404).json({ fehler: "nicht_gefunden" });
+ }
+ const dabei = zugeteiltFuer(id);
+ res.json({
+ person: { id: p.id, name: p.name, rolle_name: ROLLEN_NAME[p.rolle] ?? p.rolle },
+ bloecke: katalogFuer(req.person, false).map((b) => ({
+ schluessel: b.schluessel, name: b.name, text: b.text,
+ punkte: b.punkte.map((pt) => ({
+ schluessel: pt.schluessel, titel: pt.titel, text: pt.text || null,
+ stufe: pt.stufe, dabei: dabei.has(pt.schluessel),
+ })),
+ })),
+ });
+ } catch (f) {
+ console.error("[entwicklung] Punkte lesen:", f?.message);
+ res.status(503).json({ fehler: "nicht_verfuegbar" });
+ }
+ });
+
+entwicklungRouter.put("/workspace/api/entwicklung/person/:id/punkte",
+ gleicheHerkunft, nurLeitung, express.json({ limit: "32kb" }), (req, res) => {
+ try {
+ const id = nummer(req.params.id);
+ const p = id && db().prepare("SELECT id, name, rolle FROM personen WHERE id = ?").get(id);
+ if (!p || !KARTEN_ROLLEN.includes(p.rolle)) {
+ return res.status(404).json({ fehler: "nicht_gefunden" });
+ }
+ /* NUR SCHLUESSEL, DIE ES WIRKLICH GIBT. Ohne diesen Abgleich
+ liesse sich ueber diesen Weg beliebiger Text in die Tabelle
+ schreiben -- und er stuende danach in keiner Liste, waere aber
+ auch nicht wegzubekommen. */
+ const erlaubt = new Set(ENTWICKLUNG_FREMD.map((pt) => pt.schluessel));
+ const wunsch = Array.isArray(req.body?.punkte) ? req.body.punkte : null;
+ if (!wunsch) return res.status(400).json({ fehler: "ungueltig" });
+ const neu = [...new Set(wunsch.map(String).filter((s) => erlaubt.has(s)))];
+
+ const nun = jetzt();
+ /* EINE TRANSAKTION. Zwischen „alles weg" und „neu eintragen"
+ darf es keinen Moment geben, in dem die Person gar nichts hat
+ -- sonst saehe ein gleichzeitiger Abruf eine leere Karte, und
+ bei einem Fehler dazwischen bliebe sie leer. */
+ db().exec("BEGIN");
+ try {
+ db().prepare("DELETE FROM entwicklung_zuteilung WHERE person_id = ?").run(id);
+ const rein = db().prepare(`INSERT INTO entwicklung_zuteilung
+ (person_id, schluessel, seit, von_id) VALUES (?,?,?,?)`);
+ for (const s of neu) rein.run(id, s, nun, req.person.id);
+ db().exec("COMMIT");
+ } catch (f) {
+ db().exec("ROLLBACK");
+ throw f;
+ }
+
+ protokolliere("entwicklung_punkte_zugeteilt", {
+ personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
+ detail: `${p.name}: ${neu.length} Punkte`.slice(0, 120),
+ });
+ res.json({ ok: true, zugeteilt: neu.length });
+ } catch (f) {
+ console.error("[entwicklung] Punkte setzen:", f?.message);
+ res.status(503).json({ fehler: "nicht_verfuegbar" });
+ }
+ });
+
entwicklungRouter.post("/workspace/api/entwicklung/person/:id/:schluessel/schritt",
gleicheHerkunft, nurLeitung, express.json({ limit: "8kb" }), (req, res) => {
try {
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index e656858e..47d3f54d 100644
--- a/workspace/anruf-probe.html
+++ b/workspace/anruf-probe.html
@@ -16,15 +16,15 @@
verdeckt. Gefunden hat es pruef-struktur.
Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. -->
-
-
+
+
-
+