Aus der Probe wird ein Zugang - und die Seite sagt nicht mehr "Agentur"
Die drei offenen Punkte aus Abschnitt 9 des Plans, plus Filipes zweite
Ansage: "auf dieser seite soll nichts stehen von agentur, diese seite
hier ist nur fuer mich meine modis und meine community."
1. PROBE -> ZUGANG (ein Knopf statt eines Satzes)
Auf der Stufe "Probe" stand bisher "Zugang in Personen & Zugaenge
anlegen, Rolle Modi" - und wer das vergass, hatte eine Karte auf "Im
Team" und keinen Menschen darin. Jetzt haengt das Anlegen am Schritt
selbst.
Ueber DENSELBEN Weg wie in "Personen & Zugaenge", nicht ueber einen
zweiten: POST /workspace/api/verwaltung/personen, mit ihrer
Rechtepruefung, ihrem Protokolleintrag und ihrem genau einmal
angezeigten Code. Der Talente-Server legt selbst keinen einzigen Zugang
an, und die Pruefung zaehlt nach, dass es bei einem Weg bleibt.
Der Code steht im gleichen Kasten wie dort - die Gestaltung ist aus
personen.css nach start.css umgezogen, weil talente.html sie sonst nicht
laedt. Ein Geheimnis sieht im ganzen Haus gleich aus. Er steht
ausserhalb der Liste, sonst waere er in dem Moment weg, in dem er
entsteht: wenn die Karte auf "Im Team" springt.
Die rechte Hand bekommt an dieser Stelle KEINEN Knopf, sondern einen
Satz. Ein Knopf, der ihr jedes Mal "darfst du nicht" antwortet, waere
schlechter als gar keiner - er verspricht etwas.
2. DIE EIGENE KARTE (Abschnitt "Deine Karte")
Wer beschrieben wird, darf es lesen - aber erst, wenn ALLE gesetzt
haben (sonst waere die erste Einschaetzung eine Vorgabe fuer die
zweite), und ohne Namen und ohne Anlasstext. Gemessen in beide
Richtungen: vorher nicht sichtbar, nachher sichtbar.
3. DER VORLAGENTEXT
Gebaut aus den angeklickten Merkmalen, in einem Feld zum Aendern, nicht
zum Abschicken. Ohne Merkmale steht auch keines drin.
4. "GILT FUER" SAGT DER SERVER
In bereich.js stand woertlich "Agentur" - richtig auf der
Agenturadresse, falsch auf jeder anderen. Jetzt liefert der Server
`gehoert` ("Der Treff" / "Team Dogi" / "Agentur"); gemessen mit einem
einzigen Menschen auf zwei Adressen.
PRUEFUNGEN: treff 58 -> 66, nachwuchs 69 -> 109, neue-seiten 70 -> 93.
Die Katalogseiten werden jetzt auch mit den Augen der rechten Hand
angesehen - ohne das waere "Deine Karte" nie auf einem Bildschirm
gewesen. Und pruef-neue-seiten wartet nicht mehr 900 ms, sondern bis
sich der Text nicht mehr aendert: Die Karte stand da und wurde
trotzdem als fehlend gemeldet, weil zu frueh gelesen wurde.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+194
-1
@@ -20,7 +20,7 @@
|
||||
Aufrufen mit: node server/pruef-nachwuchs.mjs
|
||||
===================================================================== */
|
||||
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { request as httpAnfrage } from "node:http";
|
||||
@@ -454,6 +454,199 @@ melde("=== 8. Standzeit ===");
|
||||
`einen Tag vor der Frist noch nicht (${r2.kandidaten[0].tage} Tage)`);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
9. DER VORLAGENTEXT
|
||||
|
||||
Er ist der Unterschied zwischen "Ansprechen" als Aufgabe und
|
||||
"Ansprechen" als erledigter Sache. Geprueft wird deshalb nicht, DASS
|
||||
ein Text kommt, sondern dass er aus den angeklickten Merkmalen
|
||||
gebaut ist -- sonst waere es eine Floskel mit eingesetztem Namen.
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 9. Der Vorlagentext ===");
|
||||
{
|
||||
const zwei = [tal.TALENT_ALLE[0], tal.TALENT_ALLE[1]];
|
||||
const text = tal.ansprechtext("Kessi aus dem Chat", zwei.map((m) => m.schluessel));
|
||||
|
||||
ok(text.startsWith("Hey Kessi,"),
|
||||
"die Anrede nimmt nur den ersten Namensteil, nicht den ganzen Eintragstitel");
|
||||
ok(zwei.every((m) => text.includes(m.du)),
|
||||
"beide angeklickten Merkmale stehen woertlich im Text");
|
||||
ok(/kein Druck|nein/.test(text),
|
||||
"und der Ausstieg steht drin -- ein Angebot ohne Nein ist keines");
|
||||
|
||||
/* GEGENPROBE: ohne ein einziges Merkmal darf KEINES drinstehen --
|
||||
sonst baut die Funktion sich etwas zusammen, das niemand beobachtet
|
||||
hat. Das waere die schlimmste Sorte Vorlage. */
|
||||
const leer = tal.ansprechtext("Kessi", []);
|
||||
ok(!tal.TALENT_ALLE.some((m) => leer.includes(m.du)),
|
||||
"ohne angeklickte Merkmale steht auch keines im Text");
|
||||
ok(leer.length > 100, `aber ein vollstaendiger Text bleibt es (${leer.length} Zeichen)`);
|
||||
ok(leer !== text, "und er ist ein anderer als der mit Merkmalen");
|
||||
|
||||
/* HAUSREGEL: In Text, den ein Mensch liest, stehen echte Umlaute.
|
||||
"Haettest du Lust" war genau der Fehler, den diese Zeile findet. */
|
||||
const beide = text + " " + leer;
|
||||
ok(!/\b(fuer|koenn|haett|muess|ueber|waer|moecht)/i.test(beide),
|
||||
"keine ASCII-Ersatzumlaute in einem Text, den ein Mensch liest");
|
||||
ok(/\b(fuer)/i.test("das ist fuer dich"), "und der Sucher findet so einen auch wirklich");
|
||||
|
||||
/* UND ER KOMMT UEBER DIE LEITUNG AN, nicht nur aus dem Modul. */
|
||||
const lage = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi }));
|
||||
const k = (lage.kandidaten || [])[0];
|
||||
ok(typeof k?.ansprechtext === "string" && k.ansprechtext.length > 100,
|
||||
"die Karte bringt ihren Text mit");
|
||||
const gesetzt = (k?.merkmale || []).map((s) => tal.TALENT_NACH_SCHLUESSEL.get(s)?.du).filter(Boolean);
|
||||
ok(gesetzt.length > 0 && gesetzt.every((d) => k.ansprechtext.includes(d)),
|
||||
`und zwar aus ihren ${gesetzt.length} gesetzten Merkmalen`);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
10. DIE EIGENE KARTE
|
||||
|
||||
Sie ist die Gegenprobe zur ganzen Kategorie: Ueber Menschen wird hier
|
||||
aufgeschrieben, und wer beschrieben wird, darf es lesen. Aber erst,
|
||||
wenn ALLE gesetzt haben -- sonst waere die erste Einschaetzung eine
|
||||
Vorgabe fuer die zweite -- und ohne Namen und ohne Anlasstext.
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 10. Die eigene Karte ===");
|
||||
{
|
||||
const meine = json(await roh("/workspace/api/entwicklung/meine-karte", CREW, { cookie: kModi }));
|
||||
ok(meine.hat_karte === true, "der Modi hat eine eigene Karte");
|
||||
ok(meine.beurteiler === 2,
|
||||
`zwei Augenpaare urteilen ueber ihn (${meine.beurteiler}) -- er selbst zaehlt nicht mit`);
|
||||
|
||||
const punkte = (meine.bloecke || []).flatMap((b) => b.punkte || []);
|
||||
ok(punkte.length > 0, `${punkte.length} Punkte in seiner Karte`);
|
||||
|
||||
/* Aus Abschnitt 4: DIESEN Punkt haben beide gesetzt (laeuft/waechst). */
|
||||
const beide = punkte.find((p) => p.schluessel === kat.ENTWICKLUNG_FREMD[0].schluessel);
|
||||
ok(beide && beide.offen === false, "ein Punkt, den beide gesetzt haben, ist offen");
|
||||
ok(beide?.antworten?.length === 2,
|
||||
`mit beiden Antworten nebeneinander (${(beide?.antworten || []).map((a) => a.name).join(" / ")})`);
|
||||
ok(beide?.einig === false, "und dem ehrlichen Hinweis, dass sie sich uneinig sind");
|
||||
|
||||
/* GEGENPROBE 1 -- und ohne sie beweist die Zeile darueber nichts:
|
||||
Einen Punkt, den erst EINER gesetzt hat, bekommt er NICHT. */
|
||||
const einer = punkte.find((p) => p.schluessel === kat.ENTWICKLUNG_FREMD[1].schluessel);
|
||||
ok(einer && einer.offen === true,
|
||||
"einen Punkt, den erst einer gesetzt hat, sieht er noch nicht");
|
||||
ok(einer && einer.antworten === undefined,
|
||||
"und die Antwort des einen reist dabei auch nicht heimlich mit");
|
||||
|
||||
/* GEGENPROBE 2: sobald die zweite dazukommt, oeffnet er sich. */
|
||||
const nach = await roh(`/workspace/api/entwicklung/person/${idModi}/${kat.ENTWICKLUNG_FREMD[1].schluessel}`,
|
||||
CREW, { cookie: kHand }, { stufe: "hakt", anlass: "Zweimal zu spaet zur Schicht." }, "PUT");
|
||||
ok(nach.code === 200, `die rechte Hand setzt denselben Punkt (${nach.code})`);
|
||||
const meine2 = await roh("/workspace/api/entwicklung/meine-karte", CREW, { cookie: kModi });
|
||||
const jetztOffen = (json(meine2).bloecke || []).flatMap((b) => b.punkte || [])
|
||||
.find((p) => p.schluessel === kat.ENTWICKLUNG_FREMD[1].schluessel);
|
||||
ok(jetztOffen?.offen === false && jetztOffen?.antworten?.length >= 1,
|
||||
"danach steht er in seiner Karte");
|
||||
|
||||
/* KEINE NAMEN UND KEIN ANLASS. Beides steht in der Karte, die
|
||||
DogFather sieht -- und beides waere hier ein Vertrauensbruch:
|
||||
"wer hat das ueber mich gesagt" macht aus einer Entwicklung eine
|
||||
Abrechnung. */
|
||||
const roh2 = meine2.text;
|
||||
ok(!/Filipe|Rieke/.test(roh2), "kein Name eines Beurteilers in der ganzen Antwort");
|
||||
ok(!/zu spaet zur Schicht|Am Samstag/.test(roh2), "und kein einziger Anlasstext");
|
||||
ok(/hakt|laeuft|waechst/.test(roh2),
|
||||
"die Antworten selbst kommen aber an -- sonst waere die Zeile darueber trivial");
|
||||
|
||||
/* Block 5 gehoert der Person -- er steht in IHRER Selbstauskunft,
|
||||
nicht in der Fremdkarte. Waere er hier, stuenden Antworten
|
||||
anderer auf Fragen, die nur sie sich stellt. */
|
||||
const selbst = kat.ENTWICKLUNG_SELBST.map((p) => p.schluessel);
|
||||
ok(!punkte.some((p) => selbst.includes(p.schluessel)),
|
||||
"Block 5 taucht in der eigenen Karte nicht als Fremdurteil auf");
|
||||
|
||||
/* Wer gar nicht beurteilt wird, bekommt auch keine leere Karte
|
||||
vorgesetzt -- eine Karte ohne Inhalt liest sich wie ein Versaeumnis. */
|
||||
/* UND DIE RECHTE HAND HAT AUCH EINE. Ueber sie urteilt nur DogFather
|
||||
-- sie selbst zaehlt nicht mit, sonst waere ihre Karte erst dann
|
||||
vollstaendig, wenn sie sich selbst eingeschaetzt hat. */
|
||||
const ihre = json(await roh("/workspace/api/entwicklung/meine-karte", CREW, { cookie: kHand }));
|
||||
ok(ihre.hat_karte === true, `auch die rechte Hand hat eine eigene Karte (${JSON.stringify(ihre).slice(0, 90)})`);
|
||||
ok(ihre.beurteiler === 1, `ueber sie urteilt genau einer (${ihre.beurteiler})`);
|
||||
|
||||
const chef = json(await roh("/workspace/api/entwicklung/meine-karte", HAUS, { cookie: kDogi }));
|
||||
ok(chef.hat_karte === false, "DogFather selbst hat keine -- ueber ihn urteilt hier niemand");
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
11. AUS DER PROBE WIRD EIN ZUGANG
|
||||
|
||||
Der letzte Schritt des Trichters ist kein Schritt, sondern eine
|
||||
Entscheidung. Bis heute stand dort ein Satz ("Zugang in Personen &
|
||||
Zugaenge anlegen"), und wer ihn vergass, hatte eine Karte auf "Im
|
||||
Team" und keinen Menschen darin.
|
||||
|
||||
GEPRUEFT WIRD VOR ALLEM EINES: dass es dafuer keinen ZWEITEN Weg
|
||||
gibt. Ein Zugang entsteht im ganzen Haus an genau einer Stelle.
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 11. Probe → Zugang ===");
|
||||
{
|
||||
const letzte = tal.TALENT_STUFEN.at(-1);
|
||||
ok(!/Personen/.test(letzte.schritt || ""),
|
||||
"die letzte Stufe schickt niemanden mehr von Hand in die Personenverwaltung");
|
||||
|
||||
/* EIN EINZIGER WEG, und es ist der vorhandene. */
|
||||
const js = readFileSync(new URL("../workspace/assets/js/talente.js", import.meta.url), "utf8");
|
||||
/* OHNE DIE KOMMENTARE gezaehlt: Der Weg wird daneben auch erklaert,
|
||||
und eine Erklaerung ist kein zweiter Weg. */
|
||||
const jsCode = js.replace(/\/\*[\s\S]*?\*\//g, "");
|
||||
const wege = jsCode.split("/workspace/api/verwaltung/personen").length - 1;
|
||||
ok(wege === 1, `talente.js kennt genau einen Weg zum Anlegen (${wege})`);
|
||||
ok(js.split("/workspace/api/verwaltung/personen").length - 1 > wege,
|
||||
"und der Kommentar daneben nennt ihn beim Namen -- sonst zaehlt die Zeile darueber nichts weg");
|
||||
const eigen = readFileSync(new URL("../server/workspace-entwicklung.js", import.meta.url), "utf8");
|
||||
ok(!/personAnlegen|INSERT INTO personen/.test(eigen),
|
||||
"und der Talente-Server legt selbst keinen einzigen Zugang an");
|
||||
|
||||
/* DIE RECHTE HAND SIEHT DEN TRICHTER, VERGIBT ABER KEINE ZUGAENGE. */
|
||||
const hand = await roh("/workspace/api/verwaltung/personen", CREW, { cookie: kHand },
|
||||
{ name: "Heimlich", rolle: "modi" });
|
||||
ok(hand.code === 404, `die rechte Hand kommt am Anlegen nicht vorbei (${hand.code})`);
|
||||
const einModi = await roh("/workspace/api/verwaltung/personen", CREW, { cookie: kModi },
|
||||
{ name: "Heimlich", rolle: "modi" });
|
||||
ok(einModi.code === 404, `ein Modi erst recht nicht (${einModi.code})`);
|
||||
|
||||
/* GEGENPROBE: DogFather kann es -- sonst waere "404" oben auch dann
|
||||
gruen, wenn der Weg gar nicht existiert. */
|
||||
const neu = await roh("/workspace/api/verwaltung/personen", HAUS, { cookie: kDogi },
|
||||
{ name: "Kessi", rolle: "modi" });
|
||||
ok(neu.code === 201, `DogFather legt den Zugang an (${neu.code})`);
|
||||
const person = json(neu);
|
||||
ok(person.rolle === "modi", `und zwar als Modi (${person.rolle})`);
|
||||
ok(typeof person.code === "string" && person.code.length > 8,
|
||||
"der Zugangscode kommt genau einmal zurueck");
|
||||
|
||||
/* UND DANACH IST ER WEG. Nirgends abrufbar -- auch nicht fuer den,
|
||||
der ihn angelegt hat. */
|
||||
const liste = await roh("/workspace/api/verwaltung/personen", HAUS, { cookie: kDogi });
|
||||
ok(liste.code === 200 && !liste.text.includes(person.code),
|
||||
"danach steht er in keiner Liste mehr");
|
||||
|
||||
/* Erst jetzt wandert die Karte -- in derselben Reihenfolge wie in der
|
||||
Oberflaeche: erst der Mensch, dann der Haken. */
|
||||
const stufe = await roh(`/workspace/api/talente/${idTalent}/stufe`, HAUS,
|
||||
{ cookie: kDogi }, { stufe: "imteam" }, "PUT");
|
||||
ok(stufe.code === 200, `und die Karte geht auf "Im Team" (${stufe.code})`);
|
||||
const lage = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi }));
|
||||
const k = (lage.kandidaten || []).find((x) => x.id === idTalent);
|
||||
ok(k?.stufe === "imteam" && k?.knopf === null,
|
||||
"dahinter kommt kein weiterer Knopf -- der Trichter ist zu Ende");
|
||||
|
||||
/* Der neue Mensch ist wirklich da, mit allem, was daran haengt. */
|
||||
const dd = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
const z = dd.prepare("SELECT rolle, aktiv FROM personen WHERE name = 'Kessi'").get();
|
||||
dd.close();
|
||||
ok(z?.rolle === "modi" && z?.aktiv === 1, "und er steht als aktiver Modi in der Datenbank");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------- */
|
||||
melde("");
|
||||
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
||||
|
||||
+191
-21
@@ -121,19 +121,72 @@ await roh("/workspace/api/treff/massnahme", { cookie: kDogi },
|
||||
|
||||
/* ETWAS AUF DEN KATALOGSEITEN. Ohne eine Person im Team ist die
|
||||
Entwicklungsseite leer, ohne einen Kandidaten der Trichter -- und
|
||||
eine leere Seite erfuellt jede Bedingung, ohne etwas zu zeigen. */
|
||||
eine leere Seite erfuellt jede Bedingung, ohne etwas zu zeigen.
|
||||
|
||||
UND ZWAR AUF MEHREREN STUFEN. Der Trichter zeigt je Stufe einen
|
||||
ANDEREN naechsten Schritt; steht nur ein Kandidat auf der ersten,
|
||||
bleiben die Knoepfe der anderen vier ungesehen. Genau dort sitzen
|
||||
seit dem 11.09. die zwei neuen: der Vorlagentext auf "Beobachtet"
|
||||
und "Ins Team holen · Zugang anlegen" auf "Probe". */
|
||||
/* EINMAL ANKLOPFEN, DAMIT ES DIE TABELLEN GIBT. Sie entstehen beim
|
||||
ersten angemeldeten Aufruf des Bereichs, nicht beim Start -- ein
|
||||
INSERT davor liefe gegen eine Tabelle, die es noch nicht gibt. */
|
||||
await roh("/workspace/api/talente/lage", { cookie: kDogi });
|
||||
const kat = await import("./workspace-entwicklungs-punkte.js");
|
||||
const talKat = await import("./workspace-talent-punkte.js");
|
||||
const dd = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
let idHand = null;
|
||||
{
|
||||
const salt = randomBytes(16).toString("hex");
|
||||
const hash = scryptSync("CODE-MODI-0001", salt, 64,
|
||||
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
||||
dd.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
|
||||
+ " VALUES ('Frida','modi',?,?,?,?,1,?)")
|
||||
.run(hash, salt, 32768,
|
||||
createHmac("sha256", schluessel).update("CODE-MODI-0001").digest("hex"), jetzt);
|
||||
dd.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von, einsatz)"
|
||||
+ " VALUES ('talente','ruhig','Kessi aus dem Chat','Faellt seit Wochen auf.',?,?,?,?)")
|
||||
.run(jetzt.slice(0, 10), jetzt, idDogi, "Im Stream am Dienstag");
|
||||
const person = (name, rolle, code) => {
|
||||
const salt = randomBytes(16).toString("hex");
|
||||
const hash = scryptSync(code, salt, 64,
|
||||
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
||||
dd.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
|
||||
+ " VALUES (?,?,?,?,?,?,1,?)")
|
||||
.run(name, rolle, hash, salt, 32768,
|
||||
createHmac("sha256", schluessel).update(code).digest("hex"), jetzt);
|
||||
return dd.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||||
};
|
||||
person("Frida", "modi", "CODE-MODI-0001");
|
||||
idHand = person("Rieke", "hand", "CODE-HAND-0001");
|
||||
|
||||
const talent = (titel, text) => {
|
||||
dd.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von, einsatz)"
|
||||
+ " VALUES ('talente','ruhig',?,?,?,?,?,?)")
|
||||
.run(titel, text, jetzt.slice(0, 10), jetzt, idDogi, "Im Stream am Dienstag");
|
||||
return dd.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||||
};
|
||||
talent("Kessi aus dem Chat", "Faellt seit Wochen auf.");
|
||||
const idBeobachtet = talent("Malin aus dem Chat", "Haelt den Chat ruhig.");
|
||||
const idProbe = talent("Tino aus dem Chat", "Laeuft seit vier Wochen mit.");
|
||||
|
||||
const stufe = dd.prepare(
|
||||
"INSERT INTO talent_stufe (eintrag_id, stufe, seit, von_id) VALUES (?,?,?,?)");
|
||||
stufe.run(idBeobachtet, "beobachtet", jetzt, idDogi);
|
||||
stufe.run(idProbe, "probe", jetzt, idDogi);
|
||||
|
||||
/* MERKMALE fuer den Vorlagentext -- er wird daraus gebaut. Ohne sie
|
||||
stuende dort der Satz fuer "noch nichts beobachtet", und die neue
|
||||
Gestaltung waere zwar sichtbar, aber nicht in ihrer laengsten
|
||||
Fassung. Gemessen werden soll die laengste. */
|
||||
const merk = dd.prepare(
|
||||
"INSERT INTO talent_merkmal (eintrag_id, schluessel, von_id, am) VALUES (?,?,?,?)");
|
||||
for (const m of talKat.TALENT_ALLE.slice(0, 3)) {
|
||||
merk.run(idBeobachtet, m.schluessel, idDogi, jetzt);
|
||||
}
|
||||
|
||||
/* DIE EIGENE KARTE DER RECHTEN HAND. Sie oeffnet sich erst, wenn
|
||||
jeder Beurteiler gesetzt hat -- ueber sie urteilt nur DogFather,
|
||||
also reicht eine Zeile je Punkt. Sechs gesetzte und der Rest offen:
|
||||
So stehen beide Zustaende nebeneinander auf dem Bildschirm. */
|
||||
const stand = dd.prepare(
|
||||
"INSERT INTO entwicklung_stand (person_id, schluessel, von_id, stufe, anlass, am)"
|
||||
+ " VALUES (?,?,?,?,?,?)");
|
||||
const stufen = ["laeuft", "waechst", "laeuft", "hakt", "laeuft", "waechst"];
|
||||
kat.ENTWICKLUNG_FREMD.slice(0, 6).forEach((p, i) => {
|
||||
stand.run(idHand, p.schluessel, idDogi, stufen[i],
|
||||
stufen[i] === "hakt" ? "Zweimal zu spaet zur Schicht." : null, jetzt);
|
||||
});
|
||||
}
|
||||
dd.close();
|
||||
|
||||
@@ -299,7 +352,16 @@ const UEBERBREIT = `(() => {
|
||||
return { schiebt, raus };
|
||||
})()`;
|
||||
|
||||
async function ansehen(name, pfad, code, breite, hoehe) {
|
||||
/* MIT EINEM FERTIGEN KEKS STATT ueber die Anmeldemaske (11.09.2026).
|
||||
|
||||
Die rechte Hand meldet sich an der CREW-WAND an, und die haengt am
|
||||
Host-Kopf -- den kann ein Browser bei einer Navigation nicht setzen.
|
||||
Ueber die Maske auf 127.0.0.1 kommt sie also gar nicht herein.
|
||||
|
||||
Das ist kein Umgehen einer Sicherung: Angemeldet wird ueber denselben
|
||||
Weg wie sonst (/api/anmelden), nur die Maske bleibt aussen vor. Was
|
||||
die Seite danach zeigt, entscheidet weiterhin der Server. */
|
||||
async function ansehen(name, pfad, code, breite, hoehe, keks = null) {
|
||||
const kontext = await browser.newContext({ viewport: { width: breite, height: hoehe } });
|
||||
const seite = await kontext.newPage();
|
||||
const konsole = [];
|
||||
@@ -308,14 +370,39 @@ async function ansehen(name, pfad, code, breite, hoehe) {
|
||||
});
|
||||
seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message));
|
||||
|
||||
await seite.goto(`${BASIS}/workspace/`, { waitUntil: "domcontentloaded" });
|
||||
await seite.click(`.rolle[data-rolle="admin"]`);
|
||||
await seite.fill("#code", code);
|
||||
await seite.click("button[type=submit]");
|
||||
await seite.waitForURL("**/start.html", { timeout: 15000 });
|
||||
if (keks) {
|
||||
await kontext.addCookies(keks.split("; ").filter(Boolean).map((teil) => {
|
||||
const i = teil.indexOf("=");
|
||||
return { name: teil.slice(0, i), value: teil.slice(i + 1), domain: "127.0.0.1", path: "/" };
|
||||
}));
|
||||
} else {
|
||||
await seite.goto(`${BASIS}/workspace/`, { waitUntil: "domcontentloaded" });
|
||||
await seite.click(`.rolle[data-rolle="admin"]`);
|
||||
await seite.fill("#code", code);
|
||||
await seite.click("button[type=submit]");
|
||||
await seite.waitForURL("**/start.html", { timeout: 15000 });
|
||||
}
|
||||
|
||||
await seite.goto(`${BASIS}/workspace/${pfad}`, { waitUntil: "networkidle" });
|
||||
await seite.waitForTimeout(900);
|
||||
/* WARTEN, BIS SICH NICHTS MEHR AENDERT -- NICHT 900 MILLISEKUNDEN.
|
||||
|
||||
Gemessen am 11.09.2026: Die eigene Karte der rechten Hand stand
|
||||
nachweislich auf dem Bildschirm (versteckt: false, Antwort 200),
|
||||
aber der Text war schon vorher gelesen. Die Pruefung meldete
|
||||
"steht nicht da" fuer etwas, das dastand -- der schlimmste
|
||||
Fehlalarm, weil man danach am richtigen Code sucht.
|
||||
|
||||
Eine groessere Zahl waere derselbe Fehler mit einer anderen Zahl.
|
||||
Die Seite laedt ihre Abschnitte nacheinander; wie lange das
|
||||
dauert, haengt an ihrer Anzahl. Deshalb eine Regel statt einer
|
||||
Rechnung: gelesen wird, wenn der Text dreimal hintereinander
|
||||
derselbe war. Die Frist darueber ist nur die Notbremse. */
|
||||
await seite.waitForFunction(() => {
|
||||
const t = document.querySelector("main")?.innerText || "";
|
||||
if (window.__standStill === t) { window.__ruhig = (window.__ruhig || 0) + 1; }
|
||||
else { window.__standStill = t; window.__ruhig = 0; }
|
||||
return (window.__ruhig || 0) >= 3;
|
||||
}, null, { timeout: 12000, polling: 200 }).catch(() => {});
|
||||
|
||||
const text = await seite.evaluate(() => (document.querySelector("main")?.innerText || "").trim());
|
||||
const punkte = await seite.evaluate(() => (document.querySelector("main")?.innerText || "")
|
||||
@@ -323,12 +410,32 @@ async function ansehen(name, pfad, code, breite, hoehe) {
|
||||
const kontrast = await seite.evaluate(KONTRAST);
|
||||
const ueberlapp = await seite.evaluate(UEBERLAPP);
|
||||
const breit = await seite.evaluate(UEBERBREIT);
|
||||
/* DIE EIGENE KARTE IST DER EINZIGE ABSCHNITT, DER SICH SELBST
|
||||
AUFDECKT. Bleibt er zu, sieht die Seite vollkommen normal aus --
|
||||
deshalb wird hier nicht nur gelesen, was dasteht, sondern gefragt,
|
||||
was der Server dazu sagt. */
|
||||
const karte = await seite.evaluate(async () => {
|
||||
const e = document.getElementById("karte-block");
|
||||
if (!e) return { da: false };
|
||||
let antwort = null;
|
||||
try {
|
||||
const a = await fetch("/workspace/api/entwicklung/meine-karte", { credentials: "same-origin" });
|
||||
antwort = { status: a.status, hat: (await a.json()).hat_karte };
|
||||
} catch (f) { antwort = { status: -1, fehler: String(f) }; }
|
||||
return { da: true, versteckt: e.hidden, antwort };
|
||||
});
|
||||
await seite.screenshot({ path: join(BILDER, `${name}-${breite}.png`), fullPage: true });
|
||||
await kontext.close();
|
||||
return { text, punkte, kontrast, ueberlapp, breit, konsole };
|
||||
return { text, punkte, kontrast, ueberlapp, breit, konsole, karte };
|
||||
}
|
||||
|
||||
for (const [name, pfad] of [
|
||||
const gesehen = {}; // was auf jedem Bildschirm tatsaechlich stand
|
||||
|
||||
const anm3 = await roh("/workspace/api/anmelden", {}, { rolle: "hand", code: "CODE-HAND-0001" });
|
||||
const kHand = [].concat(anm3.kopf?.["set-cookie"] || []).map((c) => c.split(";")[0]).join("; ");
|
||||
ok(!!kHand, "die rechte Hand ist angemeldet -- ohne sie bliebe ihre eigene Karte ungesehen");
|
||||
|
||||
for (const [name, pfad, keks] of [
|
||||
["regeln", "treff-regeln.html"],
|
||||
["rechte", "rechte.html"],
|
||||
["moderation", "treff-moderation.html"],
|
||||
@@ -338,11 +445,17 @@ for (const [name, pfad] of [
|
||||
findet andere Fehler. */
|
||||
["entwicklung", "entwicklung.html"],
|
||||
["talente", "talente.html"],
|
||||
/* UND EINMAL DIESELBE SEITE MIT ANDEREN AUGEN. "Deine Karte" gibt es
|
||||
nur fuer die, ueber die geurteilt wird -- DogFather sieht sie nie.
|
||||
Ohne diese Zeile waere der ganze Abschnitt nie auf einem Bildschirm
|
||||
gewesen. */
|
||||
["entwicklung-hand", "entwicklung.html", kHand],
|
||||
]) {
|
||||
melde("");
|
||||
melde(`=== ${pfad} ===`);
|
||||
for (const [breite, hoehe] of [[1440, 1000], [390, 844]]) {
|
||||
const r = await ansehen(name, pfad, "CODE-DOGI-0001", breite, hoehe);
|
||||
const r = await ansehen(name, pfad, "CODE-DOGI-0001", breite, hoehe, keks);
|
||||
if (breite === 1440) { gesehen[name] = r.text; gesehen[name + ":karte"] = r.karte; }
|
||||
const wo = `${breite} px`;
|
||||
|
||||
ok(r.text.length > 400, `${wo}: die Seite ist gefüllt (${r.text.length} Zeichen)`);
|
||||
@@ -374,6 +487,63 @@ for (const [name, pfad] of [
|
||||
}
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
WAR DAS NEUE UEBERHAUPT AUF DEM BILDSCHIRM?
|
||||
|
||||
Die Messungen darueber sagen "nichts liegt uebereinander" und "alles
|
||||
lesbar" -- und beides ist auch dann wahr, wenn das Gesuchte gar nicht
|
||||
da war. Eine leere Seite erfuellt jede dieser Bedingungen.
|
||||
|
||||
Deshalb hier die Gegenfrage, und zwar in beide Richtungen: Das Neue
|
||||
MUSS dort stehen, wo es hingehoert, und darf NICHT dort stehen, wo
|
||||
es nicht hingehoert. Nur die zweite Haelfte beweist, dass die erste
|
||||
etwas heisst.
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== Stand das Neue auch wirklich da? ===");
|
||||
{
|
||||
const t = gesehen.talente || "";
|
||||
ok(/Vorlagentext/.test(t),
|
||||
"Talente: der Knopf fuer den Vorlagentext stand auf der Stufe „Beobachtet“");
|
||||
ok(/Ins Team holen/.test(t),
|
||||
"Talente: „Ins Team holen · Zugang anlegen“ stand auf der Stufe „Probe“");
|
||||
/* GEGENPROBE: Der Vorlagentext gehoert auf GENAU EINE Stufe. Stuende
|
||||
er auf jeder, waere er auf vier von fuenf eine Ablenkung -- und die
|
||||
Zeile darueber waere trotzdem gruen. */
|
||||
/* ZEILENWEISE gezaehlt und nicht als Wort: "Der Vorlagentext liegt
|
||||
bereit" ist der Satz des naechsten Schritts, der Knopf steht
|
||||
allein auf seiner Zeile. Als Wort gezaehlt waeren es zwei, und die
|
||||
Zeile haette einen Fehler gemeldet, den es nicht gibt. */
|
||||
const knoepfe = (wort) => t.split("\n").filter((z) => z.trim() === wort).length;
|
||||
ok(knoepfe("Vorlagentext") === 1,
|
||||
`und zwar genau einmal, nicht bei jedem Kandidaten (${knoepfe("Vorlagentext")})`);
|
||||
ok(t.split("Ins Team holen").length - 1 === 1,
|
||||
`der Zugangsknopf ebenso (${t.split("Ins Team holen").length - 1})`);
|
||||
|
||||
const hand = gesehen["entwicklung-hand"] || "";
|
||||
const dogi = gesehen.entwicklung || "";
|
||||
/* WENN ES SCHIEFGEHT, SOLL DIE MELDUNG SAGEN, WAS STATTDESSEN DA WAR.
|
||||
"nicht gefunden" allein laesst offen, ob die Karte fehlt oder die
|
||||
ganze Seite eine andere war. */
|
||||
const obenLinks = (hand.split("\n").map((z) => z.trim()).filter(Boolean)[0] || "(leer)");
|
||||
/* OHNE RUECKSICHT AUF GROSS UND KLEIN: innerText liefert, was der
|
||||
Browser ZEIGT -- und .e-abschnitt ist in Grossbuchstaben gesetzt.
|
||||
Im Quelltext steht "Deine Karte", auf dem Bildschirm "DEINE
|
||||
KARTE". Eine Suche mit Ruecksicht darauf meldet "steht nicht da"
|
||||
fuer etwas, das dasteht. */
|
||||
ok(/Deine Karte/i.test(hand),
|
||||
`Entwicklung: die rechte Hand sieht ihre eigene Karte (${JSON.stringify(gesehen["entwicklung-hand:karte"])} / oben: ${obenLinks})`);
|
||||
ok(!/Deine Karte/i.test(dogi),
|
||||
"und DogFather sieht sie nicht -- ueber ihn urteilt hier niemand");
|
||||
ok(hand !== dogi && hand.length > 400,
|
||||
`zwei verschiedene Bildschirme (${dogi.length} / ${hand.length} Zeichen) -- der Keks hat also gewirkt`);
|
||||
/* UND KEIN NAME EINES BEURTEILERS. Was der Server nicht ausliefert,
|
||||
kann die Seite nicht zeigen -- aber genau das ist die Behauptung,
|
||||
und Behauptungen gehoeren gemessen. */
|
||||
ok(!/Filipe/.test(hand),
|
||||
"und in ihrer Karte steht kein Name dessen, der geurteilt hat");
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
melde("");
|
||||
melde(`Bilder liegen in ${BILDER}`);
|
||||
|
||||
+61
-1
@@ -37,7 +37,7 @@
|
||||
dann an die falsche Wand und scheiterte, ohne dass man saehe warum.
|
||||
Dieselbe Falle wie am 10.09. bei den Kategorie-Kanaelen.
|
||||
===================================================================== */
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { request as httpAnfrage } from "node:http";
|
||||
@@ -618,6 +618,66 @@ melde("=== 6. Schreiben ===");
|
||||
ok(a.code !== 200 && a.code !== 201, `aber nicht in ein Brett des Hauses (${a.code})`);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
7. WEM EIN EINTRAG GILT, SAGT DER SERVER -- NICHT DIE OBERFLAECHE
|
||||
|
||||
Filipe, 11.09.2026: "auf dieser seite soll nichts stehen von agentur,
|
||||
diese seite hier ist nur fuer mich meine modis und meine community."
|
||||
|
||||
Er hatte recht, und der Fehler war ein fest eingetragenes Wort: In
|
||||
bereich.js stand woertlich "Agentur" -- richtig auf der
|
||||
Agenturadresse, falsch auf jeder anderen.
|
||||
|
||||
GEPRUEFT WIRD MIT EINEM EINZIGEN MENSCHEN AUF ZWEI ADRESSEN. Dieselbe
|
||||
Person, dasselbe Brett, zwei Waende: Wandert das Wort mit, kommt es
|
||||
vom Haus. Waere es an der Rolle festgemacht, stuende hier zweimal
|
||||
dasselbe -- und das ist genau der Fehler, den es zu finden gilt.
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 7. Das Wort für „Gilt für“ kommt vom Server ===");
|
||||
{
|
||||
const kCrew = (await anmelden("admin", "CODE-DOGI-0001", CREW)).keks;
|
||||
const kHaus = (await anmelden("admin", "CODE-DOGI-0001", HAUS)).keks;
|
||||
ok(!!kCrew && !!kHaus, "DogFather ist an beiden Waenden angemeldet");
|
||||
|
||||
const lies = async (brett, host, keks) => {
|
||||
const a = await roh(`/workspace/api/bereich/${brett}`, host, { cookie: keks });
|
||||
let g = null;
|
||||
try { g = JSON.parse(a.text).gehoert; } catch { /* bleibt null */ }
|
||||
return { code: a.code, gehoert: g };
|
||||
};
|
||||
|
||||
const treff = await lies("treff", CREW, kCrew);
|
||||
ok(treff.code === 200 && treff.gehoert === "Der Treff",
|
||||
`ein Treff-Brett gehoert dem Treff (${treff.gehoert})`);
|
||||
|
||||
const crew = await lies("ideen", CREW, kCrew);
|
||||
ok(crew.code === 200 && crew.gehoert === "Team Dogi",
|
||||
`dasselbe Haus-Brett auf crew. gehoert Team Dogi (${crew.gehoert})`);
|
||||
|
||||
/* GEGENPROBE, und ohne sie beweist die Zeile darueber nichts: Auf der
|
||||
Agenturadresse MUSS weiterhin "Agentur" stehen. Ein Wort, das
|
||||
ueberall dasselbe sagt, sagt nichts. */
|
||||
const haus = await lies("ideen", HAUS, kHaus);
|
||||
ok(haus.code === 200 && haus.gehoert === "Agentur",
|
||||
`und auf workspace. weiterhin Agentur (${haus.gehoert})`);
|
||||
ok(crew.gehoert !== haus.gehoert,
|
||||
"dieselbe Person, dasselbe Brett, zwei Woerter -- es haengt am Haus");
|
||||
|
||||
/* UND DIE OBERFLAECHE SCHREIBT ES NICHT MEHR SELBST HIN. */
|
||||
const js = readFileSync(new URL("../workspace/assets/js/bereich.js", import.meta.url), "utf8");
|
||||
const ohneKommentare = js.replace(/\/\*[\s\S]*?\*\//g, "");
|
||||
ok(!/['"`]Agentur/.test(ohneKommentare),
|
||||
"in bereich.js steht das Wort „Agentur“ in keinem Text mehr");
|
||||
ok(/gehoert/.test(ohneKommentare),
|
||||
"stattdessen fragt sie den Server danach");
|
||||
/* Gegenprobe zum Sucher selbst: Er MUSS ein hingeschriebenes
|
||||
"Agentur" auch wirklich finden, sonst ist die Zeile darueber nur
|
||||
gruen, weil sie blind ist. */
|
||||
ok(/['"`]Agentur/.test("const x = 'Agentur-Eintraege';"),
|
||||
"und der Sucher findet so ein Wort auch wirklich");
|
||||
}
|
||||
|
||||
melde("");
|
||||
console.log(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
||||
console.log(fehler === 0 ? "ALLES IN ORDNUNG" : "NICHT IN ORDNUNG");
|
||||
|
||||
@@ -1093,8 +1093,26 @@ bereicheRouter.get("/workspace/api/bereich/:bereich", (req, res) => {
|
||||
es gaebe eine zweite Wahrheit darueber, welche es sind. So gibt
|
||||
es weiterhin genau eine (TREFF_BRETTER), und der Browser fragt
|
||||
sie, statt sie zu kennen. */
|
||||
/* WEM GEHOERT EIN EINTRAG AUF DIESEM BRETT? (11.09.2026)
|
||||
|
||||
Filipe, mit Bildschirmfoto des Feldes "Gilt für: Agentur":
|
||||
"auf dieser seite soll nichts stehen von agentur, diese seite
|
||||
hier ist nur für mich meine modis und meine community."
|
||||
|
||||
Er hatte recht, und der Fehler war ein fest eingetragenes Wort:
|
||||
In bereich.js stand woertlich "Agentur" -- richtig auf der
|
||||
Agenturadresse, falsch auf jeder anderen. Ein Wort in der
|
||||
Oberflaeche, das eine Tatsache ueber den Server behauptet, ist
|
||||
genau die Sorte Zweitschrift, die auseinanderlaeuft.
|
||||
|
||||
Jetzt sagt der Server es. Er weiss beides: welches Brett es ist
|
||||
und in welchem Haus die Person gerade steht. */
|
||||
const gehoert = treffBrett ? "Der Treff"
|
||||
: (req.sicht || req.person)?.haus === "crew" ? "Team Dogi"
|
||||
: "Agentur";
|
||||
|
||||
res.json({
|
||||
bereich, einstellung, treff: treffBrett,
|
||||
bereich, einstellung, treff: treffBrett, gehoert,
|
||||
eintraege: treffBrett ? eintraege.map(mitRollenname) : eintraege,
|
||||
});
|
||||
} catch (fehler) {
|
||||
|
||||
@@ -69,7 +69,7 @@ import {
|
||||
} from "./workspace-entwicklungs-punkte.js";
|
||||
import {
|
||||
TALENT_STUFEN, TALENT_GRUPPEN, TALENT_MERKMALE, TALENT_WARNZEICHEN,
|
||||
TALENT_NACH_SCHLUESSEL, talentStufe, talentNaechste,
|
||||
TALENT_NACH_SCHLUESSEL, talentStufe, talentNaechste, ansprechtext,
|
||||
} from "./workspace-talent-punkte.js";
|
||||
|
||||
export const entwicklungRouter = express.Router();
|
||||
@@ -437,6 +437,96 @@ entwicklungRouter.put("/workspace/api/entwicklung/mich/:schluessel", gleicheHerk
|
||||
}
|
||||
});
|
||||
|
||||
/* ---------- Die eigene Karte ------------------------------------------
|
||||
|
||||
Filipes Frage aus dem Plan, Punkt 2: "Sieht die Person ihre eigene
|
||||
Entwicklungskarte?" -- und die Antwort war ja, aber unter zwei
|
||||
Bedingungen. Beide sind hier gebaut, und beide sind der Grund, warum
|
||||
es ueberhaupt geht:
|
||||
|
||||
ERST, WENN ALLE GESETZT HABEN. Eine halbe Karte ist schlimmer als
|
||||
keine: Wer sieht, dass einer "da hakt es" gesetzt hat und der andere
|
||||
noch nichts, liest daraus einen Streit, den es nicht gibt.
|
||||
|
||||
WER "ALLE" IST, wird gerechnet, nicht angenommen: alle aktiven
|
||||
Personen, die das Team fuehren -- OHNE die Person selbst. Sonst
|
||||
muesste die rechte Hand sich erst selbst beurteilen, um zu sehen,
|
||||
was DogFather ueber sie denkt.
|
||||
|
||||
OHNE NAMEN UND OHNE ANLASS. Sie sieht "2x Laeuft" oder "1x Laeuft,
|
||||
1x Waechst gerade" -- nicht, wer was gesetzt hat, und nicht, was
|
||||
danebengeschrieben steht. Der Anlass ist fuer ein Gespraech
|
||||
aufgeschrieben, nicht zum Lesen ohne den Menschen dazu. Genau
|
||||
deshalb steht auf der Seite der Satz, dass man danach fragen soll.
|
||||
|
||||
UND WER GAR KEINE KARTE HAT, bekommt das gesagt. DogFather steht
|
||||
nicht in KARTEN_ROLLEN -- eine leere Seite waere fuer ihn ein
|
||||
Fehler, ein Satz ist eine Auskunft.
|
||||
------------------------------------------------------------------- */
|
||||
entwicklungRouter.get("/workspace/api/entwicklung/meine-karte", (req, res) => {
|
||||
try {
|
||||
if (!KARTEN_ROLLEN.includes(req.person.rolle)) {
|
||||
return res.json({ hat_karte: false });
|
||||
}
|
||||
|
||||
/* Wer ueber diese Person urteilt -- alle ausser ihr selbst. */
|
||||
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);
|
||||
|
||||
const staende = db().prepare(
|
||||
"SELECT schluessel, von_id, stufe, am FROM entwicklung_stand WHERE person_id = ?")
|
||||
.all(req.person.id).filter((z) => beurteiler.includes(z.von_id));
|
||||
|
||||
const nachPunkt = new Map();
|
||||
for (const z of staende) {
|
||||
if (!nachPunkt.has(z.schluessel)) nachPunkt.set(z.schluessel, []);
|
||||
nachPunkt.get(z.schluessel).push(z);
|
||||
}
|
||||
|
||||
const bloecke = ENTWICKLUNG_BLOECKE
|
||||
.filter((b) => !b.nurSelbst)
|
||||
.map((b) => ({
|
||||
schluessel: b.schluessel, name: b.name, text: b.text,
|
||||
punkte: (ENTWICKLUNG_PUNKTE[b.schluessel] || []).map((pt) => {
|
||||
const liste = nachPunkt.get(pt.schluessel) || [];
|
||||
/* VOLLSTAENDIG heisst: jeder Beurteiler hat gesetzt. */
|
||||
const fertig = beurteiler.length > 0 && liste.length >= beurteiler.length;
|
||||
if (!fertig) {
|
||||
return { schluessel: pt.schluessel, titel: pt.titel, stufe: pt.stufe, offen: true };
|
||||
}
|
||||
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,
|
||||
/* 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. */
|
||||
antworten: ENTWICKLUNG_STUFEN
|
||||
.filter((x) => zaehler[x.schluessel])
|
||||
.map((x) => ({ stufe: x.schluessel, name: x.name, anzahl: zaehler[x.schluessel] })),
|
||||
einig: new Set(liste.map((z) => z.stufe)).size === 1,
|
||||
am: liste.map((z) => z.am).sort().at(-1) || null,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
const alle = bloecke.flatMap((b) => b.punkte);
|
||||
res.json({
|
||||
hat_karte: true,
|
||||
beurteiler: beurteiler.length,
|
||||
fertig: alle.filter((x) => !x.offen).length,
|
||||
gesamt: alle.length,
|
||||
stufen: ENTWICKLUNG_STUFEN,
|
||||
erwartung: ENTWICKLUNG_ERWARTUNG,
|
||||
bloecke,
|
||||
});
|
||||
} catch (f) {
|
||||
console.error("[entwicklung] Meine Karte:", f?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
/* ---------- Die Ampel ohne Namen -------------------------------------- */
|
||||
|
||||
/* WAS HIER HERAUSKOMMT UND WAS NICHT.
|
||||
@@ -541,6 +631,11 @@ function talentKarte(e, merkmale) {
|
||||
merkmale: gesetzt.filter((s) => !TALENT_NACH_SCHLUESSEL.get(s)?.warnung),
|
||||
warnzeichen: warn,
|
||||
notiz: stand?.notiz || null,
|
||||
/* DER VORLAGENTEXT -- gebaut aus den Merkmalen, die wirklich
|
||||
angeklickt wurden. Er kommt MIT der Karte und nicht ueber einen
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -73,13 +73,13 @@ export const TALENT_STUFEN = [
|
||||
{
|
||||
schluessel: "probe", name: "Probe", rang: 4, frist: 30,
|
||||
bedeutet: "Echte Schicht, kleiner Rahmen, fester Buddy – 30 Tage, beidseitig ausgesprochen.",
|
||||
schritt: "Nach 30 Tagen: übernehmen oder sauber beenden.",
|
||||
schritt: "Nach 30 Tagen: übernehmen – der Zugang entsteht dabei – oder sauber beenden.",
|
||||
knopf: "Ins Team",
|
||||
},
|
||||
{
|
||||
schluessel: "imteam", name: "Im Team", rang: 5, frist: null,
|
||||
bedeutet: "Zugang steht. Ab hier zählt die Entwicklungskarte, nicht mehr diese.",
|
||||
schritt: "Zugang in „Personen & Zugänge“ anlegen, Rolle Modi.",
|
||||
schritt: "Nichts mehr zu tun – der Zugang steht, ab hier zählt die Entwicklungskarte.",
|
||||
knopf: null,
|
||||
},
|
||||
];
|
||||
@@ -116,58 +116,79 @@ export const TALENT_GRUPPEN = [
|
||||
|
||||
export const TALENT_MERKMALE = {
|
||||
ruhe: [
|
||||
{ schluessel: "t-ruhe-streit", titel: "Bleibt ruhig, wenn im Chat gestritten wird",
|
||||
{ schluessel: "t-ruhe-streit", du: "auch dann ruhig bleibst, wenn es im Chat hitzig wird",
|
||||
titel: "Bleibt ruhig, wenn im Chat gestritten wird",
|
||||
text: "Der Normalfall, an dem sich alles zeigt." },
|
||||
{ schluessel: "t-ruhe-provokation", titel: "Geht auf Provokation nicht ein",
|
||||
{ schluessel: "t-ruhe-provokation", du: "auf Provokation nicht anspringst",
|
||||
titel: "Geht auf Provokation nicht ein",
|
||||
text: "Nicht ignorieren – nicht mitgehen. Zwei verschiedene Dinge." },
|
||||
{ schluessel: "t-ruhe-widerspricht", titel: "Widerspricht, ohne auszuteilen",
|
||||
{ schluessel: "t-ruhe-widerspricht", du: "widersprichst, ohne auszuteilen",
|
||||
titel: "Widerspricht, ohne auszuteilen",
|
||||
text: "Wer nie widerspricht, ist nicht ruhig, sondern still. Das ist etwas anderes." },
|
||||
{ schluessel: "t-ruhe-gegen-sich", titel: "Hat sich auch dann im Griff, wenn es gegen ihn selbst geht",
|
||||
{ schluessel: "t-ruhe-gegen-sich", du: "dich sogar dann im Griff hast, wenn es gegen dich selbst geht",
|
||||
titel: "Hat sich auch dann im Griff, wenn es gegen ihn selbst geht",
|
||||
text: "Der härteste der vier – und der aussagekräftigste." },
|
||||
],
|
||||
hilft: [
|
||||
{ schluessel: "t-hilft-vor-team", titel: "Beantwortet Fragen anderer, bevor jemand vom Team da ist",
|
||||
{ schluessel: "t-hilft-vor-team", du: "Fragen von anderen beantwortest, bevor jemand von uns da ist",
|
||||
titel: "Beantwortet Fragen anderer, bevor jemand vom Team da ist",
|
||||
text: "Tut die Arbeit schon, ohne den Titel zu haben." },
|
||||
{ schluessel: "t-hilft-erklaert-neuen", titel: "Erklärt Neuen, wie es hier läuft",
|
||||
{ schluessel: "t-hilft-erklaert-neuen", du: "Neuen erklärst, wie es hier läuft",
|
||||
titel: "Erklärt Neuen, wie es hier läuft",
|
||||
text: "Setzt voraus, dass er es selbst verstanden hat." },
|
||||
{ schluessel: "t-hilft-meldet-statt-moderiert", titel: "Meldet etwas, statt selbst zu moderieren",
|
||||
{ schluessel: "t-hilft-meldet-statt-moderiert", du: "Sachen meldest, statt selbst durchzugreifen",
|
||||
titel: "Meldet etwas, statt selbst zu moderieren",
|
||||
text: "Genau die richtige Grenze zu kennen, ist das eigentliche Signal – "
|
||||
+ "wer ohne Rolle durchgreift, wird es mit Rolle erst recht." },
|
||||
{ schluessel: "t-hilft-unbeobachtet", titel: "Hilft auch dann, wenn es niemand sieht",
|
||||
{ schluessel: "t-hilft-unbeobachtet", du: "auch dann hilfst, wenn gerade niemand hinsieht",
|
||||
titel: "Hilft auch dann, wenn es niemand sieht",
|
||||
text: "Schwer zu beobachten, deshalb selten angeklickt – und wenn, dann zählt es." },
|
||||
],
|
||||
haus: [
|
||||
{ schluessel: "t-haus-regeln", titel: "Kennt die Regeln, ohne sie nachzulesen",
|
||||
{ schluessel: "t-haus-regeln", du: "die Regeln hier kennst, ohne nachzulesen",
|
||||
titel: "Kennt die Regeln, ohne sie nachzulesen",
|
||||
text: "Und wendet sie im Gespräch an, nicht als Zitat." },
|
||||
{ schluessel: "t-haus-leute", titel: "Kennt die Leute – weiß, wer wer ist",
|
||||
{ schluessel: "t-haus-leute", du: "die Leute kennst und weißt, wer wer ist",
|
||||
titel: "Kennt die Leute – weiß, wer wer ist",
|
||||
text: "Ein halbes Jahr Einarbeitung, die schon passiert ist." },
|
||||
{ schluessel: "t-haus-witze", titel: "Versteht die Witze, die nur hier funktionieren",
|
||||
{ schluessel: "t-haus-witze", du: "die Witze verstehst, die nur hier funktionieren",
|
||||
titel: "Versteht die Witze, die nur hier funktionieren",
|
||||
text: "Klingt nebensächlich und ist der Unterschied zwischen Gast und Zuhause." },
|
||||
{ schluessel: "t-haus-frueh-dabei", titel: "War schon da, als es kleiner war",
|
||||
{ schluessel: "t-haus-frueh-dabei", du: "schon dabei warst, als es hier noch kleiner war",
|
||||
titel: "War schon da, als es kleiner war",
|
||||
text: "Nicht mehr wert – aber belastbarer, weil länger beobachtet." },
|
||||
],
|
||||
da: [
|
||||
{ schluessel: "t-da-regelmaessig", titel: "Über Wochen regelmäßig, nicht nur bei den großen Streams",
|
||||
{ schluessel: "t-da-regelmaessig", du: "über Wochen regelmäßig da bist, nicht nur bei den großen Streams",
|
||||
titel: "Über Wochen regelmäßig, nicht nur bei den großen Streams",
|
||||
text: "Regelmäßigkeit ist planbar, Begeisterung nicht." },
|
||||
{ schluessel: "t-da-auch-wenig-los", titel: "Auch dann, wenn wenig los ist",
|
||||
{ schluessel: "t-da-auch-wenig-los", du: "auch dann kommst, wenn wenig los ist",
|
||||
titel: "Auch dann, wenn wenig los ist",
|
||||
text: "Die kleinen Streams sind der ehrlichere Test." },
|
||||
{ schluessel: "t-da-luecke", titel: "Zu Zeiten, zu denen wir niemanden haben",
|
||||
{ schluessel: "t-da-luecke", du: "oft genau dann da bist, wenn wir sonst niemanden haben",
|
||||
titel: "Zu Zeiten, zu denen wir niemanden haben",
|
||||
text: "Operativ der entscheidendste Punkt der ganzen Liste: Ein guter Kandidat "
|
||||
+ "zur falschen Zeit löst kein einziges Problem." },
|
||||
{ schluessel: "t-da-kommt-wieder", titel: "Kommt nach einer Pause von selbst wieder",
|
||||
{ schluessel: "t-da-kommt-wieder", du: "nach einer Pause von selbst wieder auftauchst",
|
||||
titel: "Kommt nach einer Pause von selbst wieder",
|
||||
text: "Pausen sind normal. Wiederkommen ist das Signal." },
|
||||
],
|
||||
empfohlen: [
|
||||
{ schluessel: "t-emp-vom-team", titel: "Von jemandem aus dem Team vorgeschlagen",
|
||||
{ schluessel: "t-emp-vom-team", du: "jemandem aus dem Team aufgefallen bist",
|
||||
titel: "Von jemandem aus dem Team vorgeschlagen",
|
||||
text: "Ein Augenpaar, das den Alltag kennt." },
|
||||
{ schluessel: "t-emp-mehrfach", titel: "Mehr als eine Person hat unabhängig denselben Namen genannt",
|
||||
{ schluessel: "t-emp-mehrfach", du: "gleich mehreren aus dem Team unabhängig voneinander aufgefallen bist",
|
||||
titel: "Mehr als eine Person hat unabhängig denselben Namen genannt",
|
||||
text: "Der stärkste Einzelbefund im ganzen Katalog. Zwei unabhängige Nennungen "
|
||||
+ "schlagen jede einzelne Beobachtung." },
|
||||
{ schluessel: "t-emp-schicht-tauschen", titel: "Jemand aus dem Team würde eine Schicht mit ihm tauschen",
|
||||
{ schluessel: "t-emp-schicht-tauschen", du: "jemand aus dem Team gern eine Schicht mit dir tauschen würde",
|
||||
titel: "Jemand aus dem Team würde eine Schicht mit ihm tauschen",
|
||||
text: "Die ehrlichste Frage, die man dem Team stellen kann." },
|
||||
{ schluessel: "t-emp-schon-ausgeholfen", titel: "Hat schon mal ausgeholfen, ohne Rolle",
|
||||
{ schluessel: "t-emp-schon-ausgeholfen", du: "schon mal ausgeholfen hast, ohne dass du musstest",
|
||||
titel: "Hat schon mal ausgeholfen, ohne Rolle",
|
||||
text: "Der beste verfügbare Beweis – er hat es getan." },
|
||||
{ schluessel: "t-emp-von-zuschauer", titel: "Wurde von einem Zuschauer vorgeschlagen",
|
||||
{ schluessel: "t-emp-von-zuschauer", du: "auch anderen Zuschauern aufgefallen bist",
|
||||
titel: "Wurde von einem Zuschauer vorgeschlagen",
|
||||
text: "Schwächer als eine Nennung aus dem Team, aber es zählt." },
|
||||
],
|
||||
};
|
||||
@@ -217,6 +238,81 @@ export const TALENT_NACH_SCHLUESSEL = new Map([
|
||||
export const talentStufe = (s) =>
|
||||
TALENT_STUFEN.find((x) => x.schluessel === s) || null;
|
||||
|
||||
/* =====================================================================
|
||||
DER VORLAGENTEXT ZUM ANSPRECHEN (11.09.2026)
|
||||
=====================================================================
|
||||
|
||||
Auf Stufe 2 steht als naechster Schritt "Ansprechen". Bis heute stand
|
||||
daneben "der Vorlagentext liegt bereit" -- und es gab keinen. Ein
|
||||
Versprechen an eine Schublade.
|
||||
|
||||
ER WIRD GEBAUT, NICHT ABGESCHRIEBEN. Aus den Merkmalen, die wirklich
|
||||
angeklickt wurden. Ein allgemeiner Text ("wir fanden dich toll")
|
||||
klingt wie ein Serienbrief, weil er einer ist; drei konkrete
|
||||
Beobachtungen klingen nach jemandem, der hingesehen hat -- und das
|
||||
ist der Unterschied, ob jemand zusagt.
|
||||
|
||||
HOECHSTENS DREI. Wer fuenf Beobachtungen auflistet, klingt nach
|
||||
Personalakte. Die drei kommen in der Reihenfolge des Katalogs, und
|
||||
die faengt bei "Ruhe" an -- dem am besten belegten Merkmal.
|
||||
|
||||
DREI SAETZE, DIE IMMER DRINSTEHEN, und jeder hat einen Grund:
|
||||
* "kein Druck, sag ruhig nein" -- wer sich gedraengt fuehlt, sagt
|
||||
ja und hoert nach drei Wochen auf.
|
||||
* "30 Tage, danach schauen wir beide" -- die Probezeit wird
|
||||
BEIDSEITIG ausgesprochen. Wer weiss, dass er ohne Gesichtsverlust
|
||||
wieder gehen kann, sagt eher ehrlich, wie es ihm damit geht.
|
||||
* "du musst nichts koennen" -- die haeufigste Absage kommt nicht
|
||||
aus Desinteresse, sondern aus der Annahme, man muesse etwas
|
||||
mitbringen.
|
||||
|
||||
ER WIRD NICHT VERSCHICKT. Er steht da zum Kopieren. Eine Nachricht,
|
||||
die ein Programm verschickt, liest sich wie eine Nachricht, die ein
|
||||
Programm verschickt hat. */
|
||||
export function ansprechtext(name, gesetzteMerkmale) {
|
||||
/* NUR DAS ERSTE WORT. Im Brett steht selten ein Name allein --
|
||||
"Kessi aus dem Chat" ist eine Notiz, keine Anrede. "Hey Kessi aus
|
||||
dem Chat," liest sich wie ein Serienbrief, und genau das soll der
|
||||
Text nicht sein.
|
||||
|
||||
Es ist eine VORLAGE zum Kopieren, keine Nachricht, die abgeschickt
|
||||
wird -- wer den Namen anders schreiben will, tut das im Text. */
|
||||
const sauber = String(name || "").trim().split(/[\s,(–-]+/)[0].trim() || "du";
|
||||
const beobachtet = (gesetzteMerkmale || [])
|
||||
.map((sch) => TALENT_ALLE.find((m) => m.schluessel === sch)?.du)
|
||||
.filter(Boolean)
|
||||
.slice(0, 3);
|
||||
|
||||
const mitte = beobachtet.length
|
||||
? "Mir ist aufgefallen, dass du " + (beobachtet.length === 1
|
||||
? beobachtet[0]
|
||||
: beobachtet.slice(0, -1).join(", ") + " und " + beobachtet[beobachtet.length - 1]) + "."
|
||||
: "Du bist mir hier schon öfter positiv aufgefallen.";
|
||||
|
||||
/* ECHTE UMLAUTE UND ECHTE STRICHE.
|
||||
|
||||
In Kommentaren schreibt dieses Haus ASCII; in TEXT, den jemand
|
||||
liest, nicht. Der erste Entwurf stand voller "Haettest" und
|
||||
"koennen" -- und dieser Text hier ist der einzige im ganzen
|
||||
Bereich, den ein Fremder zu sehen bekommt. "Haettest du Lust" sieht
|
||||
aus wie ein Fehler, weil es einer ist. Derselbe Befund stand schon
|
||||
am 10.09. in workspace-personen.js. */
|
||||
return [
|
||||
`Hey ${sauber},`,
|
||||
"",
|
||||
mitte,
|
||||
"",
|
||||
"Deshalb die Frage: Hättest du Lust, bei uns im Mod-Team mitzumachen?",
|
||||
"",
|
||||
"Du musst dafür nichts können – alles, was es zu wissen gibt, zeigen wir dir. "
|
||||
+ "Wir würden mit 30 Tagen anfangen, und danach schauen wir beide, ob es passt. "
|
||||
+ "Wenn nicht, ist das völlig in Ordnung und ändert nichts.",
|
||||
"",
|
||||
"Und ganz wichtig: kein Druck. Sag ruhig nein oder „gerade nicht“ – "
|
||||
+ "das nehme ich dir null übel.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
/** Die naechste Stufe -- oder null am Ende des Trichters. */
|
||||
export function talentNaechste(s) {
|
||||
const i = TALENT_STUFEN.findIndex((x) => x.schluessel === s);
|
||||
|
||||
Reference in New Issue
Block a user