From bba36426644283c5790e4ab4c7669d945fd1ef4f Mon Sep 17 00:00:00 2001 From: Dogfather Date: Fri, 11 Sep 2026 23:13:50 +0200 Subject: [PATCH] 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 --- server/pruef-nachwuchs.mjs | 195 +++++++++++++++++++++++- server/pruef-neue-seiten.mjs | 212 ++++++++++++++++++++++++--- server/pruef-treff.mjs | 62 +++++++- server/workspace-bereiche.js | 20 ++- server/workspace-entwicklung.js | 97 +++++++++++- server/workspace-talent-punkte.js | 142 +++++++++++++++--- workspace/app.webmanifest | 8 +- workspace/assets/css/entwicklung.css | 64 ++++++++ workspace/assets/css/personen.css | 26 +--- workspace/assets/css/start.css | 33 +++++ workspace/assets/js/bereich.js | 20 ++- workspace/assets/js/entwicklung.js | 67 +++++++++ workspace/assets/js/talente.js | 202 ++++++++++++++++++++++++- workspace/aufgaben.html | 24 +-- workspace/automation.html | 26 ++-- workspace/bereich.html | 34 ++--- workspace/calls.html | 28 ++-- workspace/chat.html | 26 ++-- workspace/content.html | 30 ++-- workspace/crew-index.html | 10 +- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 26 ++-- workspace/entwicklung.html | 39 +++-- workspace/index.html | 8 +- workspace/kalender.html | 28 ++-- workspace/leistung.html | 26 ++-- workspace/personen.html | 26 ++-- workspace/profil.html | 28 ++-- workspace/rechte.html | 24 +-- workspace/report.html | 28 ++-- workspace/scouting.html | 26 ++-- workspace/start.html | 24 +-- workspace/startcheck.html | 28 ++-- workspace/steckbrief.html | 26 ++-- workspace/talente.html | 46 ++++-- workspace/team.html | 26 ++-- workspace/teamlage.html | 24 +-- workspace/treff-moderation.html | 24 +-- workspace/treff-regeln.html | 24 +-- workspace/uebersicht.html | 26 ++-- workspace/wissen.html | 26 ++-- 41 files changed, 1447 insertions(+), 420 deletions(-) diff --git a/server/pruef-nachwuchs.mjs b/server/pruef-nachwuchs.mjs index 606cba9b..075ce039 100644 --- a/server/pruef-nachwuchs.mjs +++ b/server/pruef-nachwuchs.mjs @@ -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`); diff --git a/server/pruef-neue-seiten.mjs b/server/pruef-neue-seiten.mjs index c19ad9da..15d665a9 100644 --- a/server/pruef-neue-seiten.mjs +++ b/server/pruef-neue-seiten.mjs @@ -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}`); diff --git a/server/pruef-treff.mjs b/server/pruef-treff.mjs index 9493e83d..607c124e 100644 --- a/server/pruef-treff.mjs +++ b/server/pruef-treff.mjs @@ -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"); diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js index 288551d7..2fce6c3a 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -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) { diff --git a/server/workspace-entwicklung.js b/server/workspace-entwicklung.js index 4e3cfe3e..e45fb860 100644 --- a/server/workspace-entwicklung.js +++ b/server/workspace-entwicklung.js @@ -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), }; } diff --git a/server/workspace-talent-punkte.js b/server/workspace-talent-punkte.js index e201e81c..69aa4547 100644 --- a/server/workspace-talent-punkte.js +++ b/server/workspace-talent-punkte.js @@ -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); diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index ed7f69ea..19191867 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609112230", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609112313", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609112230", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609112313", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609112230", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609112313", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609112230", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609112313", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/entwicklung.css b/workspace/assets/css/entwicklung.css index 3287c414..d819d34e 100644 --- a/workspace/assets/css/entwicklung.css +++ b/workspace/assets/css/entwicklung.css @@ -363,3 +363,67 @@ .t-karte__warn { margin-left: 0; } .e-punkt__text, .e-block__satz { font-size: .88rem; } } + +/* ===================================================================== + DER VORLAGENTEXT UND DIE EIGENE KARTE (11.09.2026) + ===================================================================== */ + +.t-text { + margin: 14px 0 0; padding: 15px 17px; + background: rgba(0, 0, 0, .3); + border: 1px solid var(--rand-hell); +} +.t-text__titel { margin: 0 0 5px; font-size: .95rem; font-weight: 640; color: var(--text); } +.t-text__satz { + margin: 0 0 11px; max-width: 62ch; + font-size: .82rem; line-height: 1.6; color: var(--text-leise); +} +.t-text__feld { + display: block; width: 100%; + font: inherit; font-size: .86rem; line-height: 1.65; + padding: 11px 13px; resize: vertical; + color: var(--text); + background: rgba(0, 0, 0, .35); + border: 1px solid var(--rand); +} +.t-text__knoepfe { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 11px; } + +/* Zwei, die seit dem Bau der Seite ohne eigene Regel dastanden: der + Knopf am naechsten Schritt und die Beschriftung eines Merkmals. Sie + sahen richtig aus, weil .schritt und .t-merkmal sie getragen haben -- + aber wer eines davon umbaut, merkt es hier nicht. */ +.t-schritt__knopf { margin-top: 2px; } +.t-schritt__text { margin-top: 2px; } +.t-merkmal__titel { text-align: left; line-height: 1.35; } + +/* DIE EIGENE KARTE HAT KEINE KNOEPFE. Hier wird nichts gesetzt, hier + wird gelesen -- deshalb ein anderer Grund als auf der Karte, auf der + man klickt, und kein einziges anklickbares Feld darin. */ +.e-meine { + margin: 30px 0 0; padding: 20px 22px; + border: 1px solid var(--rand); + background: color-mix(in srgb, var(--tinte) 94%, #ffffff 6%); +} +.e-meine__satz { + margin: 0 0 16px; max-width: 64ch; + font-size: .88rem; line-height: 1.65; color: var(--text-leise); +} +.e-punkt--meine { background: rgba(0, 0, 0, .2); } +.e-meine__antworten { display: flex; flex-wrap: wrap; gap: 7px; align-items: center; } +.e-meine__antwort { + font-size: .8rem; font-weight: 600; padding: 4px 11px; border-radius: 999px; + color: var(--text-leise); + border: 1px solid var(--rand-hell); +} +.e-meine__antwort[data-stufe="laeuft"] { + color: var(--text); border-color: color-mix(in srgb, #668e6e 55%, transparent); +} +.e-meine__antwort[data-stufe="waechst"] { + color: var(--text); border-color: color-mix(in srgb, #8ec9ff 50%, transparent); +} +.e-meine__antwort[data-stufe="hakt"] { + color: var(--text); border-color: color-mix(in srgb, #cc9451 55%, transparent); +} +/* „Darüber seid ihr euch nicht einig“ ist keine Warnung, sondern eine + Einladung zu fragen – deshalb leise und ohne Farbe. */ +.e-meine__uneinig { font-size: .78rem; color: var(--text-still); font-style: italic; } diff --git a/workspace/assets/css/personen.css b/workspace/assets/css/personen.css index 86472a72..45bc8d62 100644 --- a/workspace/assets/css/personen.css +++ b/workspace/assets/css/personen.css @@ -41,27 +41,13 @@ .person__knoepfe { display: flex; gap: 7px; flex-wrap: wrap; justify-content: flex-end; } -/* ---------- Code-Anzeige ---------------------------------------------- */ +/* ---------- Code-Anzeige ---------------------------------------------- -.code-kasten { - margin: 0 0 26px; padding: 20px 22px; - background: linear-gradient(100deg, rgba(63, 189, 245, .12), rgba(138, 118, 255, .09)); - border: 1px solid rgba(63, 189, 245, .5); - border-radius: var(--radius); -} -.code-kasten__schild { - margin: 0 0 8px; font-size: .78rem; font-weight: 600; - letter-spacing: .06em; text-transform: uppercase; color: var(--text-leise); -} -.code-kasten__code { - margin: 0 0 12px; - font-family: "Cascadia Mono", Consolas, "SF Mono", Menlo, monospace; - font-size: clamp(1.3rem, 3.4vw, 1.9rem); font-weight: 700; - letter-spacing: .12em; color: #eaf4fd; - user-select: all; /* ein Klick markiert den ganzen Code */ - word-break: break-all; -} -.code-kasten__hinweis { margin: 0 0 14px; font-size: .85rem; color: var(--text-leise); } + STEHT SEIT DEM 11.09.2026 IN start.css. Nicht aufgeraeumt, sondern + umgezogen: Seit dem Talente-Trichter entsteht ein Zugang an zwei + Stellen, und talente.html laedt diese Datei nicht. Der Kasten waere + dort unsichtbar gewesen -- ein Code in weisser Grundschrift, ohne + Rahmen, mitten im Text. Gesucht unter ".code-kasten" in start.css. */ /* ---------- Protokoll --------------------------------------------------- */ diff --git a/workspace/assets/css/start.css b/workspace/assets/css/start.css index c240fee6..83d9376f 100644 --- a/workspace/assets/css/start.css +++ b/workspace/assets/css/start.css @@ -7845,3 +7845,36 @@ body.start .k-leiste .feldschild::after { fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; } + +/* ===================================================================== + DIE CODE-ANZEIGE (umgezogen aus personen.css, 11.09.2026) + + Ein Zugangscode wird im ganzen Haus nur an zwei Stellen sichtbar: + in "Personen & Zugaenge" und am Ende des Talente-Trichters. Beide + zeigen ihn gleich -- gleiche Kante, gleiche Schrift, gleicher Satz + darunter. Zwei Gestaltungen fuer dasselbe Geheimnis waeren zwei + Gelegenheiten, eine davon zu vergessen. + + Er steht hier und nicht in module.css: module.css beschreibt die + Form ALLER Karten, das hier ist ein einzelner Baustein. + ===================================================================== */ + +.code-kasten { + margin: 0 0 26px; padding: 20px 22px; + background: linear-gradient(100deg, rgba(63, 189, 245, .12), rgba(138, 118, 255, .09)); + border: 1px solid rgba(63, 189, 245, .5); + border-radius: var(--radius); +} +.code-kasten__schild { + margin: 0 0 8px; font-size: .78rem; font-weight: 600; + letter-spacing: .06em; text-transform: uppercase; color: var(--text-leise); +} +.code-kasten__code { + margin: 0 0 12px; + font-family: "Cascadia Mono", Consolas, "SF Mono", Menlo, monospace; + font-size: clamp(1.3rem, 3.4vw, 1.9rem); font-weight: 700; + letter-spacing: .12em; color: #eaf4fd; + user-select: all; /* ein Klick markiert den ganzen Code */ + word-break: break-all; +} +.code-kasten__hinweis { margin: 0 0 14px; font-size: .85rem; color: var(--text-leise); } diff --git a/workspace/assets/js/bereich.js b/workspace/assets/js/bereich.js index fea8e520..8d3251b1 100644 --- a/workspace/assets/js/bereich.js +++ b/workspace/assets/js/bereich.js @@ -861,14 +861,28 @@ NICHT aufgesetzt -- sie erlaubt getippte Namen, und genau das soll es hier nicht geben. */ if (einstellung.ohneCreatorBezug) { + /* DAS WORT KOMMT VOM SERVER (11.09.2026). + + Hier stand fest "Agentur" -- richtig auf der Agenturadresse, + falsch auf jeder anderen. Filipe, mit Bildschirmfoto: "auf + dieser seite soll nichts stehen von agentur, diese seite hier + ist nur für mich meine modis und meine community." + + Der Server weiss, welches Brett es ist und in welchem Haus + die Person steht; er schickt das Wort mit (daten.gehoert). + Faellt die Angabe aus, bleibt das Feld LEER statt falsch -- + ein fehlendes Wort sieht man, ein falsches nicht. */ + const gehoert = daten?.gehoert || ''; $('schild-creator').textContent = 'Gilt für'; $('f-creator').textContent = ''; const o = document.createElement('option'); - o.value = ''; o.textContent = 'Agentur'; + o.value = ''; o.textContent = gehoert || '—'; $('f-creator').append(o); const hinweis = $('hinweis-creator'); - hinweis.textContent = 'Agentur-Einträge gehören allen – jeder im Workspace sieht sie.'; - hinweis.hidden = false; + hinweis.textContent = gehoert + ? `Gehört allen hier – wer ${gehoert} sieht, sieht auch diesen Eintrag.` + : ''; + hinweis.hidden = !gehoert; } else { try { const { personen } = await (await hole('/workspace/api/personen')).json(); diff --git a/workspace/assets/js/entwicklung.js b/workspace/assets/js/entwicklung.js index 3c49edcf..2b44f690 100644 --- a/workspace/assets/js/entwicklung.js +++ b/workspace/assets/js/entwicklung.js @@ -244,6 +244,72 @@ return z; } + /* ---- Deine eigene Karte ----------------------------------------------- + + WAS HIER STEHT UND WAS NICHT: die Antworten, nicht die Namen, und + nicht der Anlass. „2 × Läuft" oder „1 × Läuft, 1 × Wächst gerade". + + Der Anlass ist für ein Gespräch aufgeschrieben, nicht zum Lesen + ohne den Menschen dazu — deshalb steht hier stattdessen der Satz, + dass man danach fragen soll. Ein Satz aus einer Notiz, aus dem + Zusammenhang gerissen, richtet mehr an, als er erklärt. */ + async function meineKarte() { + let a; + try { a = await hole('/workspace/api/entwicklung/meine-karte'); } catch { return; } + if (!a.ok) return; + const r = await a.json(); + if (!r.hat_karte) return; // DogFather hat keine + + $('karte-block').hidden = false; + const satz = $('karte-satz'); + const ziel = $('meine-karte'); + ziel.textContent = ''; + ziel.setAttribute('aria-busy', 'false'); + + if (!r.beurteiler) { + satz.textContent = 'Es gibt gerade niemanden, der sie führt. Sobald jemand ' + + 'da ist, steht hier etwas.'; + return; + } + if (!r.fertig) { + satz.textContent = `Noch nichts fertig. Eine Beobachtung erscheint hier erst, ` + + `wenn alle ${r.beurteiler} sie gesetzt haben – eine halbe Karte ist ` + + `schlimmer als keine.`; + return; + } + + satz.textContent = `${r.fertig} von ${r.gesamt} Beobachtungen sind durch. ` + + 'Du siehst die Antworten, nicht wer was gesetzt hat – und nicht, was ' + + 'danebengeschrieben steht. Das gehört ins Gespräch: frag danach.'; + + for (const b of r.bloecke) { + const fertige = b.punkte.filter((pt) => !pt.offen); + if (!fertige.length) continue; + const abschnitt = el('section', 'e-block'); + abschnitt.append(el('h3', 'e-block__titel', b.name)); + for (const pt of fertige) { + const zeile = el('article', 'e-punkt e-punkt--meine'); + if (pt.einig) zeile.dataset.stufe = pt.antworten[0].stufe; + zeile.append(el('h4', 'e-punkt__titel', pt.titel)); + const antw = el('div', 'e-meine__antworten'); + for (const x of pt.antworten) { + const chip = el('span', 'e-meine__antwort', `${x.anzahl} × ${x.name}`); + chip.dataset.stufe = x.stufe; + antw.append(chip); + } + if (!pt.einig) { + const hin = el('span', 'e-meine__uneinig', 'darüber seid ihr euch nicht einig'); + hin.title = 'Zwei Leute haben dasselbe verschieden gesehen. Das ist normal – ' + + 'und ein guter Anlass, danach zu fragen.'; + antw.append(hin); + } + zeile.append(antw); + abschnitt.append(zeile); + } + ziel.append(abschnitt); + } + } + /* ---- Die eigenen sechs Fragen ---------------------------------------- */ async function meins() { const a = await hole('/workspace/api/entwicklung/mich'); @@ -305,6 +371,7 @@ 'Sechs Fragen über dich. Niemand aus dem Team sieht deine Antworten.'; } + await meineKarte(); await meins(); } catch { /* umgeleitet */ } })(); diff --git a/workspace/assets/js/talente.js b/workspace/assets/js/talente.js index 30869478..508c39fd 100644 --- a/workspace/assets/js/talente.js +++ b/workspace/assets/js/talente.js @@ -37,6 +37,7 @@ } let lage = null; + let darfAnlegen = false; // darf ich hier einen Modi-Zugang anlegen? const offen = new Set(); // welche Karten aufgeklappt sind /* ---- Der Trichter oben ---------------------------------------------- */ @@ -86,23 +87,64 @@ ist, und nicht erst eine Entscheidung treffen müssen. */ const schritt = el('div', 't-schritt'); schritt.append(el('p', 't-schritt__satz', k.schritt)); - if (k.knopf && k.naechste) { - const b = el('button', 'schritt t-schritt__knopf', k.knopf); + /* DER LETZTE SCHRITT IST KEIN SCHRITT, SONDERN EINE ENTSCHEIDUNG. + Aus einem Namen wird ein Mensch mit einem Zugang. Deshalb haengt + genau daran das Anlegen selbst -- und nicht ein Satz, den man + danach von Hand nachbaut. Bis heute stand hier "Zugang in + Personen & Zugaenge anlegen, Rolle Modi", und wer das vergass, + hatte eine Karte auf "Im Team" und keinen Menschen darin. */ + const zugangNoetig = k.naechste === 'imteam'; + if (k.knopf && k.naechste && (!zugangNoetig || darfAnlegen)) { + const b = el('button', 'schritt t-schritt__knopf', + zugangNoetig ? 'Ins Team holen · Zugang anlegen' : k.knopf); b.type = 'button'; b.addEventListener('click', async () => { b.disabled = true; + let zugang = null; + if (zugangNoetig) { + zugang = await zugangAnlegen(k); + if (!zugang) { b.disabled = false; return; } + } const r = await hole(`/workspace/api/talente/${k.id}/stufe`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ stufe: k.naechste }), }); b.disabled = false; - if (!r.ok) { melde('Ging nicht.'); return; } + if (!r.ok) { + /* ZWEI DINGE SIND PASSIERT, EINES DAVON HAT GEKLAPPT. Ein + blosses "ging nicht" waere hier die gefaehrlichste + Auskunft von allen -- der Zugang steht ja, und niemand + wuesste es. */ + melde(zugang + ? `Der Zugang für ${zugang.name} steht – der Code steht oben. Nur die Karte ` + + 'blieb auf „Probe“. Bitte den Schritt gleich noch einmal setzen.' + : 'Ging nicht.'); + return; + } offen.add(k.id); await laden(); }); schritt.append(b); + } else if (zugangNoetig && k.knopf) { + /* KEIN KNOPF INS LEERE. Die rechte Hand sieht den Trichter + vollstaendig, aber Zugaenge vergibt nur DogFather. Ein Knopf, + der ihr jedes Mal "darfst du nicht" antwortet, waere schlechter + als gar keiner -- er verspricht etwas. */ + schritt.append(el('p', 't-schritt__liegt', + 'Den Zugang legt DogFather an – sag ihm Bescheid. Die Karte bleibt so ' + + 'lange auf „Probe“.')); } + /* DER VORLAGENTEXT -- genau auf der Stufe, auf der er gebraucht + wird. Eine Schaltfläche „Text anzeigen" auf jeder Stufe wäre auf + vier von fünf davon eine Ablenkung. */ + if (k.stufe === 'beobachtet' && k.ansprechtext) { + const b = el('button', 'schritt t-schritt__text', 'Vorlagentext'); + b.type = 'button'; + b.addEventListener('click', () => textZeigen(k)); + schritt.append(b); + } + if (k.liegt) { schritt.append(el('p', 't-schritt__liegt', 'Das liegt länger, als es sollte. Entweder weiter – oder bewusst beenden.')); @@ -150,6 +192,139 @@ return a; } + /* ---- Aus der Probe wird ein Zugang --------------------------------- + + DERSELBE WEG WIE IN "PERSONEN & ZUGAENGE", nicht ein zweiter. + /workspace/api/verwaltung/personen ist die einzige Stelle im ganzen + Haus, an der ein Zugang entsteht -- mit ihrer Rechtepruefung, ihrer + Rollenpruefung, ihrem Protokolleintrag und ihrem genau einmal + angezeigten Code. Ein eigener Weg von hier waere eine zweite Tuer + zu genau dem, was am wenigsten zwei Tueren haben darf. + + NICHT ueber hole(): Diese Adresse antwortet Nicht-DogFather mit + 404, und hole() versteht ein 404 als "Seite gibt es nicht" und + schickt zur Startseite. Ein Rechtefehler wuerde die rechte Hand + also aus dem Trichter werfen. Hier wird deshalb selbst gelesen. + + @returns {object|null} die angelegte Person, oder null bei Abbruch. */ + async function zugangAnlegen(k) { + if (!darfAnlegen) { melde('Zugänge legt DogFather an.'); return null; } + + const vorschlag = String(k.titel || '').split(/[\s,(–-]+/)[0] || ''; + const name = prompt( + 'Wie soll die Person im Workspace heißen?\n\n' + + 'Das ist der Name, den euer Team sieht. Gleich danach bekommst du ' + + 'einmalig ihren Zugangscode – danach nie wieder.', + vorschlag); + if (name === null) return null; // abgebrochen + if (String(name).trim().length < 2) { melde('Der Name ist zu kurz.'); return null; } + + let a; + try { + a = await fetch('/workspace/api/verwaltung/personen', { + credentials: 'same-origin', + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: String(name).trim(), rolle: 'modi' }), + }); + } catch { melde('Keine Verbindung – der Zugang wurde nicht angelegt.'); return null; } + + if (a.status === 404 || a.status === 403) { + melde('Zugänge legt DogFather an – sag ihm Bescheid.'); + return null; + } + if (!a.ok) { + const grund = (await a.json().catch(() => ({}))).fehler; + melde(grund || 'Der Zugang ließ sich nicht anlegen.'); + return null; + } + const neu = await a.json(); + codeZeigen(neu); + return neu; + } + + /* Der Code steht in einem Kasten, den man SCHLIESSEN muss -- nicht in + einer Meldung, die von selbst verschwindet. Er ist das Einzige auf + dieser Seite, das es nur ein einziges Mal gibt. */ + function codeZeigen(neu) { + $('code-name').textContent = `${neu.name} · Modi`; + $('code-wert').textContent = neu.code; + $('code-kasten').hidden = false; + $('code-kasten').scrollIntoView({ behavior: 'smooth', block: 'center' }); + $('code-kopieren').focus(); + } + + $('code-kopieren')?.addEventListener('click', async () => { + const code = $('code-wert').textContent; + try { await navigator.clipboard.writeText(code); melde('Code kopiert.'); } + catch { + /* Ohne Zwischenablage-Recht: markieren, dann geht Strg+C. Der + Code steht ja da -- "ging nicht" waere hier unbrauchbar. */ + getSelection()?.removeAllRanges(); + const r = document.createRange(); + r.selectNodeContents($('code-wert')); + getSelection()?.addRange(r); + melde('Markiert – mit Strg+C kopieren.'); + } + }); + + $('code-schliessen')?.addEventListener('click', () => { + $('code-kasten').hidden = true; + $('code-wert').textContent = ''; // auch aus der Seite raus + melde(''); + }); + + /* DER TEXT WIRD GEZEIGT, NICHT VERSCHICKT. + + Eine Nachricht, die ein Programm abschickt, liest sich wie eine + Nachricht, die ein Programm abgeschickt hat — und genau das soll + dieser Text nicht sein. Er steht da zum Lesen, Ändern und + Kopieren, in einem Feld, das man bearbeiten kann. */ + function textZeigen(k) { + const alt = document.getElementById('t-textfeld'); + if (alt) alt.closest('.t-text')?.remove(); + + const kasten = el('div', 't-text'); + kasten.append(el('h3', 't-text__titel', `Vorschlag für ${k.titel}`)); + kasten.append(el('p', 't-text__satz', + 'Gebaut aus den Merkmalen, die du angeklickt hast. Ändere ihn ruhig – ' + + 'er soll nach dir klingen, nicht nach einer Vorlage. Abgeschickt wird ' + + 'hier nichts.')); + const feld = el('textarea', 't-text__feld'); + feld.id = 't-textfeld'; + feld.rows = 14; + feld.value = k.ansprechtext; + kasten.append(feld); + + const reihe = el('div', 't-text__knoepfe'); + const kopieren = el('button', 'schritt', 'Kopieren'); + kopieren.type = 'button'; + kopieren.addEventListener('click', async () => { + try { + await navigator.clipboard.writeText(feld.value); + melde('Kopiert. Jetzt einfügen, wo du ihn ansprichst.'); + } catch { + /* Ohne Zwischenablage-Recht: markieren, dann geht Strg+C. + Eine Fehlermeldung „ging nicht" wäre hier unbrauchbar — der + Text ist ja da. */ + feld.select(); + melde('Markiert – mit Strg+C kopieren.'); + } + }); + reihe.append(kopieren); + + const zu = el('button', 'schritt', 'Schließen'); + zu.type = 'button'; + zu.addEventListener('click', () => kasten.remove()); + reihe.append(zu); + kasten.append(reihe); + + const karte = [...document.querySelectorAll('.t-karte')] + .find((x) => x.querySelector('.t-karte__titel')?.textContent === k.titel); + (karte || $('liste')).append(kasten); + feld.focus(); + } + function merkmal(k, m, istWarnung) { const gesetzt = (istWarnung ? k.warnzeichen : k.merkmale).includes(m.schluessel); const b = el('button', 't-merkmal'); @@ -210,5 +385,24 @@ zeichne(); } - (async () => { try { await laden(); } catch { /* umgeleitet */ } })(); + /* WER ZUGAENGE ANLEGEN DARF, SAGT DER SERVER -- dieselbe Auskunft, + aus der auch "Personen & Zugaenge" ihre Rollenknoepfe baut. Eine + eigene Rollenabfrage hier waere eine zweite Meinung darueber, und + die zweite ist die, die irgendwann mehr durchlaesst. + + VOR dem ersten Zeichnen, nicht daneben: Sonst steht der Knopf + einen Wimpernschlag lang da und verschwindet wieder. */ + async function ichHolen() { + try { + const a = await hole('/workspace/api/ich'); + if (!a.ok) return; + const ich = await a.json(); + window.werZeigen?.(ich); + darfAnlegen = Array.isArray(ich?.darf_anlegen) && ich.darf_anlegen.includes('modi'); + } catch { /* umgeleitet */ } + } + + (async () => { + try { await ichHolen(); await laden(); } catch { /* umgeleitet */ } + })(); })(); diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index dd262ade..2a176a0b 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -12,19 +12,19 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + - + @@ -252,10 +252,10 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index e0a2b01b..87a9f78d 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -147,10 +147,10 @@ - - - - - + + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index dab23a84..1d5534e9 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -181,14 +181,14 @@ - - - - - - - - - + + + + + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index 5ade6bbf..f815d9cc 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -89,11 +89,11 @@ - - - - - - + + + + + + diff --git a/workspace/chat.html b/workspace/chat.html index 240cb8f6..d8786675 100644 --- a/workspace/chat.html +++ b/workspace/chat.html @@ -12,26 +12,26 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + - + @@ -341,10 +341,10 @@ - - - - - + + + + + diff --git a/workspace/content.html b/workspace/content.html index 458f1789..f31cad90 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -179,12 +179,12 @@ - - - - - - - + + + + + + + diff --git a/workspace/crew-index.html b/workspace/crew-index.html index 6810d6f0..98962ed9 100644 --- a/workspace/crew-index.html +++ b/workspace/crew-index.html @@ -36,15 +36,15 @@ - - - + + + - + @@ -319,6 +319,6 @@ - + diff --git a/workspace/crew.webmanifest b/workspace/crew.webmanifest index c223ae6b..8a2cfb6c 100644 --- a/workspace/crew.webmanifest +++ b/workspace/crew.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/crew-192.png?v=202609112230", + "src": "/assets/img/app-symbole/crew-192.png?v=202609112313", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-512.png?v=202609112230", + "src": "/assets/img/app-symbole/crew-512.png?v=202609112313", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609112230", + "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609112313", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609112230", + "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609112313", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/dateien.html b/workspace/dateien.html index b8d516d0..1a286640 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -108,10 +108,10 @@ - - - - - + + + + + diff --git a/workspace/entwicklung.html b/workspace/entwicklung.html index 4457f223..47eca8f5 100644 --- a/workspace/entwicklung.html +++ b/workspace/entwicklung.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -71,6 +71,21 @@ + + +