Entwicklung & Nachwuchs: zwei Kataloge zum Anklicken statt zwei leerer Formulare

Filipe: "ich will dass diese zwei kategorien perfektionniert werden, ich
will dass fertige aufgaben da stehen und so ... so dass dogfather und
rechte hand es so einfach wie möglich haben aber so professionell wie
auch nur möglich."

Bis heute waren beide Kacheln ein leeres Formular: Titel tippen, Art
wählen, Text schreiben. Ein leeres Feld beantwortet keine Frage — es
stellt sie noch einmal.

ENTWICKLUNG: 34 Beobachtungen in fünf Blöcken, vier Antworten je Zeile
(Läuft · Wächst gerade · Da hakt es · Kann ich nicht sagen). Es sind
BEOBACHTUNGEN, keine Eigenschaften — "Absagen kommen rechtzeitig" statt
"ist zuverlässig". Eine Eigenschaft kann man nur bestätigen oder
bestreiten; ein Verhalten kann man gesehen haben oder nicht.

TALENTE: 21 Merkmale in fünf Gruppen, 6 Warnzeichen getrennt geführt,
fünf Trichterstufen mit je GENAU EINEM nächsten Schritt. Eine Liste
wächst und wird nie wieder angesehen; ein Trichter fragt bei jeder Karte
"und jetzt?". Die Standzeit steht dabei — "seit 41 Tagen beobachtet"
ist eine Auskunft über uns, nicht über den Kandidaten.

VIER RIEGEL, JEDER MIT GEGENPROBE:
 · Block 5 ("Wie geht es dir damit?") gehört der Person — lesend UND
   schreibend, auch vor DogFather. Er kommt gar nicht erst aus dem
   Server; Schreiben gibt dasselbe 404 wie ein erfundener Schlüssel.
   Nach außen dringt eine Ampel OHNE Namen, und die schweigt, solange
   das Team so klein ist, dass "jemandem geht es zu viel" dasselbe wäre
   wie ein Name.
 · Der Stand des anderen kommt erst nach dem eigenen Klick — sonst
   klickt man dasselbe. Sichtbar ist nur, DASS es einen gibt: sonst
   sähen "noch niemand" und "du darfst es nicht sehen" gleich aus.
 · "Da hakt es" ohne Anlass wird abgelehnt. "Läuft" nicht — 28
   Pflichtfelder je Person wären das Gegenteil von "so einfach wie
   möglich".
 · Talente gehört DogFather und der rechten Hand. Ein Modi bekommt 404
   auf Seite UND Schnittstelle.

WAS NICHT GEBAUT IST, UND WARUM: keine Punktzahl, kein Durchschnitt,
keine Rangliste, keine Gesamtnote. Eine Zahl über einem Menschen wird
gelesen, verglichen und weitererzählt. Und kein Rot — "da hakt es" ist
eine Beobachtung, kein Alarm.

DER PLAN LAG AN EINER STELLE FALSCH, gemessen statt vermutet: Er sagte
"keine neue Tabelle, der Stand liegt in punkt_stand". Dort ist der
Schlüssel (bereich, schluessel, creator_id) — genau EIN Stand je Punkt
und Person. Zwei unabhängige Augenpaare passen da nicht hinein, ohne
eine lebende Tabelle neu zu bauen. Also eine eigene.

EIN RÜCKSCHLAG, DEN NUR DIE PRÜFUNG GEFUNDEN HAT: Die Liste der
Bretter, die ein Modi oder die rechte Hand öffnen darf, wird aus den
KACHELZIELEN abgeleitet. Als die Kacheln auf die neuen Seiten zeigten,
fielen beide Bretter heraus — und die Seite verlinkte auf ein 404. Kein
Fehler im Riegel, sondern in seiner Quelle.

Dazu zwei kleinere Funde vom Hinsehen statt vom Messen: Die Ampel sagte
"Solange ihr zu 1 seid" (jetzt drei Sätze für drei Lagen), und die
Fußzeile der Talente-Seite lag auf der Bühne — die Kontrastprüfung hatte
sie übersprungen, weil ein Link darin steht. Beide behoben, und die
Prüfung hat den stillen Aussetzer gleich mit verloren.

Geprüft: rollen 314 · start-ansicht 147 · crew-adresse 132 · neue-seiten
70 · nachwuchs 69 · entwicklung 40 · css-klassen 30 · zwischenspeicher
21 · alle-wege 19 · rechtetafel 19 — alles grün.

Was noch fehlt, steht in der Notiz, Abschnitt 9: die eigene Karte für
die Person, der Übergang "Probe" → Zugang, der Vorlagentext zum
Ansprechen.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-11 22:20:09 +02:00
co-authored by Claude Opus 5
parent 93ba881b18
commit dfd951861a
42 changed files with 3168 additions and 335 deletions
+6
View File
@@ -24,6 +24,7 @@ import { serienRouter, serienStarten } from "./workspace-serien.js";
import { dateienRouter } from "./workspace-dateien.js";
import { bereicheRouter } from "./workspace-bereiche.js";
import { treffRouter } from "./workspace-treff.js";
import { entwicklungRouter } from "./workspace-entwicklung.js";
import { reportRouter } from "./workspace-reports.js";
import { scoutRouter } from "./workspace-scouts.js";
import { callRouter } from "./workspace-calls.js";
@@ -243,6 +244,11 @@ app.use(bereicheRouter);
Weg zu denselben Zeilen waere ein zweiter Ort fuer dieselbe
Rechtepruefung. */
app.use(treffRouter);
/* Entwicklung & Nachwuchs (11.09.2026): die zwei Kataloge zum Anklicken.
Die Bretter mit den freien Notizen laufen weiter ueber bereicheRouter
-- beides nebeneinander ist Absicht: Der Katalog ist das, was man
regelmaessig durchgeht, die Notiz das, was dazwischen auffaellt. */
app.use(entwicklungRouter);
app.use(reportRouter);
app.use(scoutRouter);
app.use(callRouter);
+54 -2
View File
@@ -137,9 +137,23 @@ console.log("=== Die beiden Bereiche ===");
ok(arten.includes("last"),
"„Zu viel gerade“ ist dabei -- der haeufigste Grund aufzuhoeren");
/* DAS NOTIZBRETT MUSS ERREICHBAR BLEIBEN -- gerade WEIL die Kachel
nicht mehr darauf zeigt.
Genau hier ist am 11.09.2026 etwas kaputtgegangen: Die Liste der
Bretter, die ein Modi oder die rechte Hand oeffnen darf, wird aus
den KACHELZIELEN abgeleitet. Als die Kacheln auf die Katalogseiten
umgestellt wurden, fielen beide Bretter aus der Liste -- und die
Seite verlinkte auf ein 404. Kein Fehler im Riegel, sondern in
seiner Quelle.
Diese Zeile ist die einzige, die das gemerkt hat. */
const b = await hole(TAL, { host: CREW, keks: hand.keks });
ok(b.daten?.einstellung?.name === "Talente",
`die rechte Hand kann Talente oeffnen (${b.daten?.einstellung?.name})`);
const bE = await hole(ENTW, { host: CREW, keks: hand.keks });
ok(bE.daten?.einstellung?.name === "Entwicklung",
`und das Notizbrett Entwicklung auch (${bE.daten?.einstellung?.name})`);
const tArten = Object.keys(b.daten?.einstellung?.arten || {});
ok(tArten.includes("empfohlen"),
`die Empfehlung aus dem Team ist dabei (${tArten.join(", ")})`);
@@ -240,17 +254,55 @@ console.log("=== An die Person schicken ===");
console.log("");
console.log("=== Die Kacheln ===");
{
/* DIE KACHELN ZEIGEN SEIT DEM 11.09.2026 AUF DIE KATALOGSEITEN.
Vorher stand hier `b=(entwicklung|talente)` -- das Ziel war ein
Brett. Filipe: "ich will dass fertige aufgaben da stehen und so".
Seither fuehrt die Kachel auf entwicklung.html bzw. talente.html;
das Notizbrett gibt es weiter und ist von dort verlinkt.
GEPRUEFT WIRD DER NAME DER SEITE UND NICHT MEHR DAS BRETT -- eine
Kachel, die auf ein Brett zeigt, waere ab jetzt der Rueckschritt,
den diese Zeile melden soll. */
for (const [wer, keks] of [["DogFather", dogi.keks], ["die rechte Hand", hand.keks]]) {
const a = await hole("/workspace/api/ich", { host: CREW, keks });
const alle = [...(a.daten?.bereiche || []), ...(a.daten?.bereiche_zusatz || [])];
const beide = alle.filter((k) => /b=(entwicklung|talente)/.test(k.ziel || ""));
/* NACH DEM NAMEN UND NICHT NACH DEM ZIEL.
Seit dem 11.09.2026 zeigen DREI Kacheln auf entwicklung.html: die
beiden Katalogkacheln und „Wie geht's dir?" -- dieselbe Seite,
drei verschiedene Zwecke, weil der Server entscheidet, was ein
Modi dort sieht. Ein Filter nach dem Ziel zaehlt sie alle und
meldet einen Fehler, wo keiner ist.
Der Name ist hier die ehrlichere Frage: Es geht um DIESE zwei
Kacheln, nicht um alles, was zufaellig dieselbe Adresse hat. */
const beide = alle.filter((k) => ["Entwicklung", "Talente"].includes(k.name));
ok(beide.length === 2, `${wer}: beide Kacheln (${beide.map((k) => k.name).join(", ") || "keine"})`);
ok(beide.every((k) => k.gruppe === "Entwicklung & Nachwuchs"),
`${wer}: in einer eigenen Gruppe (${[...new Set(beide.map((k) => k.gruppe))].join(", ")})`);
}
/* DIE DRITTE KACHEL -- und sie gehoert ausdruecklich ALLEN im Team.
„Wie geht's dir?" ist die einzige Kachel des Hauses, deren Inhalt
niemand ausser der Person selbst je sieht. Sie steht auch bei
DogFather und der rechten Hand: Wenn es jemandem zu viel wird,
dann ihnen genauso. Eine Kachel, die nur die anderen ausfuellen,
waere genau die Schieflage, die dieses Haus nicht haben soll. */
for (const [wer, keks] of [["DogFather", dogi.keks], ["die rechte Hand", hand.keks],
["ein Modi", ayla.keks]]) {
const x = await hole("/workspace/api/ich", { host: CREW, keks });
const alleX = [...(x.daten?.bereiche || []), ...(x.daten?.bereiche_zusatz || [])];
const befinden = alleX.find((k) => k.ziel === "entwicklung.html"
&& !["Entwicklung", "Talente"].includes(k.name));
ok(!!befinden, `${wer} hat „${befinden?.name || "—"}"`);
ok(befinden?.gruppe === "Für dich",
`und sie steht bei „Für dich" (${befinden?.gruppe})`);
}
const a = await hole("/workspace/api/ich", { host: CREW, keks: ayla.keks });
const alle = [...(a.daten?.bereiche || []), ...(a.daten?.bereiche_zusatz || [])];
ok(!alle.some((k) => /b=(entwicklung|talente)/.test(k.ziel || "")),
ok(!alle.some((k) => ["Entwicklung", "Talente"].includes(k.name)),
`ein Teammitglied bekommt sie nicht angeboten (${alle.length} Kacheln)`);
/* Kein Farbton doppelt -- zwei Kacheln in derselben Farbe sind ein
+462
View File
@@ -0,0 +1,462 @@
/* =====================================================================
Entwicklung & Nachwuchs — die Riegel (11.09.2026)
Der Bereich hat vier Regeln, und jede einzelne wäre ohne Prüfung
eine Zusage statt einer Eigenschaft:
1. Block 5 gehört der Person. DogFather sieht ihn nicht — lesend
UND schreibend. Mit Gegenprobe: die Person selbst sieht ihn.
2. Der Stand des anderen kommt erst nach dem eigenen Klick. Mit
Gegenprobe: danach kommt er auch wirklich.
3. „Da hakt es" ohne Anlass wird abgelehnt. Mit Gegenprobe: mit
Anlass geht es durch.
4. Talente gehört DogFather und der rechten Hand. Ein Modi bekommt
404 — und zwar auf Seite UND Schnittstelle.
Dazu der Katalog selbst (Schlüssel, Stufen, keine Dopplungen) und
das Verblassen nach 90 Tagen — von beiden Seiten der Grenze, sonst
beweist es nichts.
Aufrufen mit: node server/pruef-nachwuchs.mjs
===================================================================== */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { request as httpAnfrage } from "node:http";
import { portMussFreiSein } from "./helfer-port.mjs";
const PORT = await portMussFreiSein(4404, "pruef-nachwuchs");
const ordner = mkdtempSync(join(tmpdir(), "ws-nachwuchs-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = String(PORT);
process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
const express = (await import("express")).default;
const ec = express.response.cookie;
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
import { notbremse } from "./helfer-notbremse.mjs";
await import("./index.js");
notbremse(240_000, "pruef-nachwuchs");
await new Promise((r) => setTimeout(r, 700));
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
let fehler = 0, geprueft = 0;
const melde = (t) => console.log(t);
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
const CREW = "crew.dogfather-universe.com";
const HAUS = "workspace.dogfather-universe.com";
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
const kat = await import("./workspace-entwicklungs-punkte.js");
const tal = await import("./workspace-talent-punkte.js");
const rechte = await import("./rechte.js");
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const jetzt = new Date().toISOString();
const schluessel = randomBytes(32).toString("hex");
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
.run("code_kennung_schluessel", schluessel, jetzt);
function anlegen(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");
d.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 d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
const idHand = anlegen("Rieke", "hand", "CODE-HAND-0001");
const idModi = anlegen("Frida", "modi", "CODE-MODI-0001");
anlegen("Nina", "modi", "CODE-MODI-0002");
anlegen("Mara", "manager", "CODE-MANA-0001");
/* Ein Kandidat im Talente-Brett -- ohne ihn waere jede Zeile ueber den
Trichter ein gruener Haken ueber der Null. */
d.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 idTalent = d.prepare("SELECT last_insert_rowid() AS id").get().id;
d.close();
function roh(pfad, host, kopf = {}, koerper = null, methode = null) {
if (methode === "DELETE") koerper = null;
return new Promise((fertig, schief) => {
const a = httpAnfrage({
host: "127.0.0.1", port: PORT, path: pfad,
method: methode || (koerper ? "POST" : "GET"),
headers: { Host: host, ...(koerper ? { "Content-Type": "application/json" } : {}), ...kopf },
}, (antwort) => {
let text = "";
antwort.on("data", (s) => { text += s; });
antwort.on("end", () => fertig({ code: antwort.statusCode, text, kopf: antwort.headers }));
});
a.on("error", schief);
if (koerper) a.write(JSON.stringify(koerper));
a.end();
});
}
const json = (a) => { try { return JSON.parse(a.text); } catch { return {}; } };
async function anmelden(rolle, code, host) {
const r = await roh("/workspace/api/anmelden", host, {}, { rolle, code });
return [].concat(r.kopf?.["set-cookie"] || []).map((c) => c.split(";")[0]).join("; ");
}
const kDogi = await anmelden("admin", "CODE-DOGI-0001", HAUS);
const kHand = await anmelden("hand", "CODE-HAND-0001", CREW);
const kModi = await anmelden("modi", "CODE-MODI-0001", CREW);
ok(!!kDogi && !!kHand && !!kModi, "DogFather, rechte Hand und ein Modi sind angemeldet");
/* =======================================================================
1. DER KATALOG SELBST
======================================================================= */
melde("");
melde("=== 1. Die zwei Kataloge ===");
{
const alle = kat.ENTWICKLUNG_ALLE;
ok(alle.length > 20, `${alle.length} Beobachtungen im Katalog`);
ok(new Set(alle.map((p) => p.schluessel)).size === alle.length,
"kein Schlüssel doppelt");
ok(alle.every((p) => p.schluessel.startsWith("e-")),
"jeder Schlüssel beginnt mit e- (keine Kollision mit der Checkliste)");
ok(alle.every((p) => p.titel && p.text && p.stufe),
"jede Beobachtung hat Titel, Erklärung und Erwartungsstufe");
const stufen = new Set(alle.map((p) => p.stufe));
ok(kat.ENTWICKLUNG_ERWARTUNG.every((e) => stufen.has(e.schluessel)),
`alle drei Erwartungsstufen kommen vor (${[...stufen].join(", ")})`);
/* DER RIEGEL IST EINE EIGENSCHAFT DES KATALOGS, nicht nur des
Servers: Genau ein Block ist `nurSelbst`, und die abgeleiteten
Listen passen dazu. Verrutscht das hier, greift der Server
weiterhin -- aber die Oberflaeche zeigte etwas anderes an. */
const selbstBloecke = kat.ENTWICKLUNG_BLOECKE.filter((b) => b.nurSelbst);
ok(selbstBloecke.length === 1, `genau ein Block gehört der Person selbst (${selbstBloecke.length})`);
ok(kat.ENTWICKLUNG_FREMD.length + kat.ENTWICKLUNG_SELBST.length === alle.length,
`${kat.ENTWICKLUNG_FREMD.length} fremd + ${kat.ENTWICKLUNG_SELBST.length} selbst = ${alle.length}`);
ok(kat.ENTWICKLUNG_SELBST.length > 0 && kat.ENTWICKLUNG_FREMD.length > 0,
"beide Listen haben Inhalt — sonst prüft die Zeile darüber nichts");
const merk = tal.TALENT_ALLE;
ok(merk.length > 15, `${merk.length} Merkmale im Talente-Katalog`);
ok(tal.TALENT_WARNZEICHEN.length > 0, `${tal.TALENT_WARNZEICHEN.length} Warnzeichen, getrennt geführt`);
ok(tal.TALENT_NACH_SCHLUESSEL.size === merk.length + tal.TALENT_WARNZEICHEN.length,
"kein Schlüssel doppelt zwischen Merkmalen und Warnzeichen");
ok([...merk, ...tal.TALENT_WARNZEICHEN].every((m) => m.schluessel.startsWith("t-")),
"jeder Schlüssel beginnt mit t-");
const raenge = tal.TALENT_STUFEN.map((s) => s.rang);
ok(JSON.stringify(raenge) === JSON.stringify([...raenge].sort((a, b) => a - b)),
`die fünf Stufen stehen in der richtigen Reihenfolge (${tal.TALENT_STUFEN.map((s) => s.name).join(" → ")})`);
ok(tal.TALENT_STUFEN.slice(0, -1).every((s) => s.knopf && s.schritt),
"jede Stufe außer der letzten hat GENAU EINEN nächsten Schritt mit Knopf");
ok(tal.talentNaechste("imteam") === null, "und hinter der letzten kommt nichts mehr");
ok(tal.talentStufe("gibt-es-nicht") === null,
"eine erfundene Stufe ergibt null statt der ersten — sie soll auffallen");
}
/* =======================================================================
2. BLOCK 5 GEHÖRT DER PERSON
======================================================================= */
melde("");
melde("=== 2. Block 5 ===");
{
const selbst = kat.ENTWICKLUNG_SELBST[0].schluessel;
const karte = await roh(`/workspace/api/entwicklung/person/${idModi}`, HAUS, { cookie: kDogi });
const bloecke = json(karte).bloecke || [];
const alleSchluessel = bloecke.flatMap((b) => (b.punkte || []).map((p) => p.schluessel));
ok(karte.code === 200 && alleSchluessel.length > 0,
`DogFather bekommt die Karte des Modis (${alleSchluessel.length} Punkte)`);
const durch = kat.ENTWICKLUNG_SELBST.filter((p) => alleSchluessel.includes(p.schluessel));
ok(durch.length === 0,
`und KEINEN Punkt aus Block 5${durch.length ? " -- DURCH: " + durch.map((p) => p.schluessel).join(", ") : ""}`);
ok(!/Wie geht es dir/.test(karte.text), "auch der Blockname steht nicht in der Antwort");
/* Und schreiben geht auch nicht -- mit derselben Antwort wie ein
erfundener Schluessel, damit man an der Antwort nichts ablesen kann. */
const schreib = await roh(`/workspace/api/entwicklung/person/${idModi}/${selbst}`, HAUS,
{ cookie: kDogi }, { stufe: "laeuft" }, "PUT");
const erfunden = await roh(`/workspace/api/entwicklung/person/${idModi}/e-gibt-es-nicht`, HAUS,
{ cookie: kDogi }, { stufe: "laeuft" }, "PUT");
ok(schreib.code === 404, `DogFather kann dort auch nichts setzen (${schreib.code})`);
ok(schreib.code === erfunden.code,
`dieselbe Antwort wie bei einem erfundenen Punkt (${schreib.code}/${erfunden.code})`);
/* GEGENPROBE: Die Person selbst sieht ihn sehr wohl. Ohne diese
Zeile waeren die drei darueber auch dann gruen, wenn Block 5 fuer
NIEMANDEN existierte. */
const meins = await roh("/workspace/api/entwicklung/mich", CREW, { cookie: kModi });
const meine = (json(meins).punkte || []).map((p) => p.schluessel);
ok(meins.code === 200 && meine.length === kat.ENTWICKLUNG_SELBST.length,
`der Modi selbst sieht alle ${meine.length} Fragen`);
const setzen = await roh(`/workspace/api/entwicklung/mich/${selbst}`, CREW,
{ cookie: kModi }, { stufe: "hakt" }, "PUT");
ok(setzen.code === 200, `und kann sie beantworten (${setzen.code})`);
/* Und danach steht sie immer noch nicht in DogFathers Karte. */
const nachher = await roh(`/workspace/api/entwicklung/person/${idModi}`, HAUS, { cookie: kDogi });
ok(!nachher.text.includes(selbst),
"auch nach der Antwort steht sie nicht in DogFathers Karte");
/* Und in die andere Richtung: ueber jemand ANDEREN kann man dort
nichts eintragen -- es gibt gar keine Nummer in der Adresse. */
const fremdPunkt = await roh(`/workspace/api/entwicklung/mich/${kat.ENTWICKLUNG_FREMD[0].schluessel}`,
CREW, { cookie: kModi }, { stufe: "laeuft" }, "PUT");
ok(fremdPunkt.code === 404,
`über „mich" lässt sich kein fremder Punkt setzen (${fremdPunkt.code})`);
}
/* =======================================================================
3. DIE AMPEL VERRÄT KEINEN NAMEN
======================================================================= */
melde("");
melde("=== 3. Die Ampel ===");
{
/* BEIDE SEITEN DER GRENZE, sonst beweist es nichts.
Die Ampel schweigt, solange das Team so klein ist, dass „jemandem
geht es gerade zu viel" dasselbe waere wie ein Name. Eine Pruefung,
die nur eine Seite ansieht, waere auch dann gruen, wenn die Ampel
IMMER schwiege -- oder immer redete. Beides waere ein Fehler, und
keiner davon faellt von allein auf.
Die Schwelle wird nicht abgeschrieben, sondern aus dem Verhalten
abgeleitet: Erst mit vollem Team messen, dann eine Person
abschalten und noch einmal. Was dazwischen kippt, IST die Grenze. */
const voll = json(await roh("/workspace/api/entwicklung/ampel", HAUS, { cookie: kDogi }));
ok(voll.team >= 3 && voll.sagbar === true,
`bei ${voll.team} Personen sagt die Ampel etwas (${voll.lage})`);
ok(typeof voll.geantwortet === "number" && voll.geantwortet >= 1,
`und zaehlt, wie viele geantwortet haben (${voll.geantwortet} von ${voll.team})`);
/* Ein Modi hat in Abschnitt 2 „hakt" gesetzt -- die Ampel muss das
sehen, ohne zu sagen, wer es war. */
ok(voll.lage === "jemand",
`sie meldet die Lage aus Abschnitt 2 (${voll.lage})`);
ok(!/Frida|Nina|Rieke|Filipe/.test(JSON.stringify(voll)),
"und in der ganzen Antwort steht kein einziger Name");
/* Jetzt eine Person abschalten -- das Team faellt unter die Schwelle. */
const dd = new DatabaseSync(process.env.WORKSPACE_DB);
dd.prepare("UPDATE personen SET aktiv = 0 WHERE name = 'Nina'").run();
dd.close();
const klein = json(await roh("/workspace/api/entwicklung/ampel", HAUS, { cookie: kDogi }));
ok(klein.team === voll.team - 1 && klein.sagbar === false,
`bei ${klein.team} Personen schweigt sie`);
ok(/Name/.test(klein.grund || ""),
"und sagt, warum – ein stilles Nichts sieht aus wie ein Fehler");
ok(!/Frida|Nina|Rieke|Filipe/.test(JSON.stringify(klein)),
"auch im schweigenden Fall steht kein Name da");
const dd2 = new DatabaseSync(process.env.WORKSPACE_DB);
dd2.prepare("UPDATE personen SET aktiv = 1 WHERE name = 'Nina'").run();
dd2.close();
const modi = await roh("/workspace/api/entwicklung/ampel", CREW, { cookie: kModi });
ok(modi.code === 404, `ein Modi bekommt sie gar nicht (${modi.code})`);
}
/* =======================================================================
4. DER STAND DES ANDEREN KOMMT ERST NACH DEM EIGENEN
======================================================================= */
melde("");
melde("=== 4. Zwei Augenpaare ===");
{
const punkt = kat.ENTWICKLUNG_FREMD[0].schluessel;
const gesetzt = await roh(`/workspace/api/entwicklung/person/${idModi}/${punkt}`, CREW,
{ cookie: kHand }, { stufe: "laeuft", anlass: "Gestern im Chat gesehen." }, "PUT");
ok(gesetzt.code === 200, `die rechte Hand setzt einen Punkt (${gesetzt.code})`);
const vorher = await roh(`/workspace/api/entwicklung/person/${idModi}`, HAUS, { cookie: kDogi });
const p1 = (json(vorher).bloecke || []).flatMap((b) => b.punkte || [])
.find((p) => p.schluessel === punkt);
ok(p1 && p1.andere === null,
"DogFather sieht ihren Stand NICHT, bevor er selbst gesetzt hat");
ok(p1 && p1.andere_offen === 1,
`aber er sieht, DASS es einen gibt (${p1?.andere_offen}) – sonst wäre „noch niemand" dasselbe Bild`);
ok(!vorher.text.includes("Gestern im Chat gesehen"),
"und ihr Anlasstext wird gar nicht erst ausgeliefert");
const eigen = await roh(`/workspace/api/entwicklung/person/${idModi}/${punkt}`, HAUS,
{ cookie: kDogi }, { stufe: "waechst" }, "PUT");
ok(eigen.code === 200, `DogFather setzt selbst (${eigen.code})`);
/* GEGENPROBE: jetzt kommt er. */
const nachher = await roh(`/workspace/api/entwicklung/person/${idModi}`, HAUS, { cookie: kDogi });
const p2 = (json(nachher).bloecke || []).flatMap((b) => b.punkte || [])
.find((p) => p.schluessel === punkt);
ok(p2 && p2.andere?.length === 1 && p2.andere[0].stufe === "laeuft",
`danach sieht er ihn (${p2?.andere?.[0]?.stufe})`);
/* UND DIE ABWEICHUNG IST DAS ERGEBNIS. */
const ab = json(nachher).abweichungen || [];
ok(ab.length === 1 && ab[0].schluessel === punkt,
`die Abweichung steht oben in der Karte (${ab.length})`);
ok(ab[0].meins === "waechst" && ab[0].andere === "laeuft",
`mit beiden Ständen nebeneinander (${ab[0]?.meins} / ${ab[0]?.andere})`);
}
/* =======================================================================
5. „DA HAKT ES" BRAUCHT EINEN ANLASS
======================================================================= */
melde("");
melde("=== 5. Kein Klick ohne Anlass ===");
{
const punkt = kat.ENTWICKLUNG_FREMD[1].schluessel;
const ohne = await roh(`/workspace/api/entwicklung/person/${idModi}/${punkt}`, HAUS,
{ cookie: kDogi }, { stufe: "hakt", anlass: "" }, "PUT");
ok(ohne.code === 400, `„Da hakt es" ohne Anlass wird abgelehnt (${ohne.code})`);
ok(/gemerkt|Satz/.test(json(ohne).fehler || ""),
"und die Absage sagt, was fehlt, statt nur nein");
const mit = await roh(`/workspace/api/entwicklung/person/${idModi}/${punkt}`, HAUS,
{ cookie: kDogi }, { stufe: "hakt", anlass: "Am Samstag zweimal nicht reagiert." }, "PUT");
ok(mit.code === 200, `mit Anlass geht es durch (${mit.code})`);
/* GEGENPROBE: „Läuft" braucht keinen — sonst wären es 28 Pflichtfelder
je Person, und „so einfach wie möglich" war die halbe Aufgabe. */
const laeuft = await roh(`/workspace/api/entwicklung/person/${idModi}/${kat.ENTWICKLUNG_FREMD[2].schluessel}`,
HAUS, { cookie: kDogi }, { stufe: "laeuft" }, "PUT");
ok(laeuft.code === 200, `„Läuft" geht ohne Anlass (${laeuft.code})`);
}
/* =======================================================================
6. NACH 90 TAGEN VERBLASST EINE EINSCHÄTZUNG
======================================================================= */
melde("");
melde("=== 6. Verblassen ===");
{
const modul = await import("./workspace-entwicklung.js");
const alt = kat.ENTWICKLUNG_FREMD[3].schluessel;
const dd = new DatabaseSync(process.env.WORKSPACE_DB);
dd.prepare(`INSERT INTO entwicklung_stand (person_id, schluessel, von_id, stufe, anlass, am)
VALUES (?,?,?,'laeuft',NULL,?)`)
.run(idModi, alt, idDogi,
new Date(Date.now() - (modul.VERBLASST_NACH_TAGEN + 10) * 86400000).toISOString());
dd.close();
const r = await roh(`/workspace/api/entwicklung/person/${idModi}`, HAUS, { cookie: kDogi });
const punkte = (json(r).bloecke || []).flatMap((b) => b.punkte || []);
const alterPunkt = punkte.find((p) => p.schluessel === alt);
ok(alterPunkt?.meins?.verblasst === true,
`ein Stand von vor ${modul.VERBLASST_NACH_TAGEN + 10} Tagen gilt als verblasst`);
/* GEGENPROBE, und ohne sie beweist die Zeile darüber nichts: Einer
von heute gilt NICHT als verblasst. */
const frisch = punkte.find((p) => p.schluessel === kat.ENTWICKLUNG_FREMD[0].schluessel);
ok(frisch?.meins?.verblasst === false,
"einer von heute nicht");
const lage = json(await roh("/workspace/api/entwicklung/lage", HAUS, { cookie: kDogi }));
const zeile = (lage.personen || []).find((p) => p.id === idModi);
ok(zeile?.veraltet === 1, `und die Übersicht zählt ihn (${zeile?.veraltet})`);
ok(zeile?.abweichungen === 1, `samt der Abweichung (${zeile?.abweichungen})`);
}
/* =======================================================================
7. TALENTE GEHÖRT DOGFATHER UND DER RECHTEN HAND
======================================================================= */
melde("");
melde("=== 7. Talente ===");
{
const lage = await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi });
const r = json(lage);
ok(lage.code === 200 && (r.kandidaten || []).length === 1,
`DogFather sieht den Trichter (${(r.kandidaten || []).length} Kandidat)`);
ok((r.trichter || []).length === tal.TALENT_STUFEN.length,
`alle ${r.trichter?.length} Stufen kommen mit`);
const k = r.kandidaten[0];
ok(k.stufe === "aufgefallen",
`ein neuer Kandidat startet auf „${k.stufe_name}" (${k.stufe})`);
ok(!!k.schritt && !!k.knopf, `und hat genau einen nächsten Schritt: „${k.knopf}"`);
/* DER RIEGEL. Auf Seite UND Schnittstelle -- eine Kachel, die es
nicht gibt, ist keine Sicherung. */
const modiApi = await roh("/workspace/api/talente/lage", CREW, { cookie: kModi });
ok(modiApi.code === 404, `ein Modi bekommt die Schnittstelle nicht (${modiApi.code})`);
const modiSeite = await roh("/workspace/talente.html", CREW, { cookie: kModi });
ok(modiSeite.code !== 200, `und die Seite auch nicht (${modiSeite.code})`);
const wer = rechte.SEITEN["/workspace/talente.html"] || [];
ok(wer.length === 2 && wer.includes("admin") && wer.includes("hand"),
`die Tafel gibt sie genau an ${wer.join(" und ")}`);
/* GEGENPROBE: die rechte Hand kommt hinein. */
const handApi = await roh("/workspace/api/talente/lage", CREW, { cookie: kHand });
ok(handApi.code === 200, `die rechte Hand schon (${handApi.code})`);
/* Merkmale: ein Klick setzt, der zweite nimmt zurück. */
const m = tal.TALENT_ALLE[0].schluessel;
const w = tal.TALENT_WARNZEICHEN[0].schluessel;
const eins = await roh(`/workspace/api/talente/${idTalent}/merkmal/${m}`, HAUS,
{ cookie: kDogi }, {}, "PUT");
ok(eins.code === 200 && json(eins).gesetzt === true, "ein Merkmal wird gesetzt");
const zwei = await roh(`/workspace/api/talente/${idTalent}/merkmal/${m}`, HAUS,
{ cookie: kDogi }, {}, "PUT");
ok(zwei.code === 200 && json(zwei).gesetzt === false, "und der zweite Klick nimmt es zurück");
await roh(`/workspace/api/talente/${idTalent}/merkmal/${m}`, HAUS, { cookie: kDogi }, {}, "PUT");
await roh(`/workspace/api/talente/${idTalent}/merkmal/${w}`, HAUS, { cookie: kDogi }, {}, "PUT");
const nach = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi })).kandidaten[0];
ok(nach.merkmale.length === 1 && nach.warnzeichen.length === 1,
`Merkmale und Warnzeichen stehen getrennt (${nach.merkmale.length} / ${nach.warnzeichen.length})`);
ok(!nach.merkmale.includes(w),
"ein Warnzeichen landet nicht zwischen den Merkmalen");
/* Die Stufe wandert -- und die Standzeit beginnt von vorn. */
const weiter = await roh(`/workspace/api/talente/${idTalent}/stufe`, HAUS,
{ cookie: kDogi }, { stufe: "beobachtet" }, "PUT");
ok(weiter.code === 200, `ein Schritt weiter im Trichter (${weiter.code})`);
const nach2 = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi })).kandidaten[0];
ok(nach2.stufe === "beobachtet" && nach2.tage === 0,
`Stufe „${nach2.stufe_name}", Standzeit beginnt bei ${nach2.tage}`);
ok(nach2.liegt === false, "und liegt damit auch nicht");
const erfunden = await roh(`/workspace/api/talente/${idTalent}/stufe`, HAUS,
{ cookie: kDogi }, { stufe: "chef" }, "PUT");
ok(erfunden.code === 400, `eine erfundene Stufe wird abgelehnt (${erfunden.code})`);
}
/* =======================================================================
8. LIEGENGEBLIEBEN IST EINE AUSKUNFT ÜBER UNS
======================================================================= */
melde("");
melde("=== 8. Standzeit ===");
{
const stufe = tal.talentStufe("beobachtet");
const dd = new DatabaseSync(process.env.WORKSPACE_DB);
dd.prepare("UPDATE talent_stufe SET seit = ? WHERE eintrag_id = ?")
.run(new Date(Date.now() - (stufe.frist + 11) * 86400000).toISOString(), idTalent);
dd.close();
const r = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi }));
const k = r.kandidaten[0];
ok(k.liegt === true && k.tage === stufe.frist + 11,
`nach ${k.tage} Tagen auf einer Stufe mit Frist ${stufe.frist} gilt die Karte als liegengeblieben`);
const t = (r.trichter || []).find((x) => x.schluessel === "beobachtet");
ok(t?.liegt === 1, `und der Trichter zählt sie (${t?.liegt})`);
/* GEGENPROBE von der anderen Seite der Grenze -- sonst wäre „liegt"
auch dann grün, wenn es immer liegt. */
const dd2 = new DatabaseSync(process.env.WORKSPACE_DB);
dd2.prepare("UPDATE talent_stufe SET seit = ? WHERE eintrag_id = ?")
.run(new Date(Date.now() - (stufe.frist - 1) * 86400000).toISOString(), idTalent);
dd2.close();
const r2 = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi }));
ok(r2.kandidaten[0].liegt === false,
`einen Tag vor der Frist noch nicht (${r2.kandidaten[0].tage} Tage)`);
}
/* ------------------------------------------------------------------- */
melde("");
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ }
process.exit(fehler ? 1 : 0);
@@ -1,5 +1,10 @@
/* =====================================================================
Die drei neuen Seiten — angesehen, nicht nur geprüft (11.09.2026)
Die neuen Seiten — angesehen, nicht nur geprüft (11.09.2026)
HIESS BIS HEUTE pruef-treff-seiten.mjs. Mit den zwei Katalogseiten
(Entwicklung, Talente) stimmte der Name nicht mehr -- und ein
Dateiname, der etwas anderes verspricht als sein Inhalt, ist die
Sorte Kleinigkeit, wegen der jemand die falsche Datei oeffnet.
pruef-treff und pruef-treff-werkzeuge messen, was der Server ANTWORTET.
Diese Datei sieht nach, was auf dem Bildschirm LANDET.
@@ -23,7 +28,7 @@
über das, was hinter einer halbdurchsichtigen Fläche liegt.
* Und auf 390 px Breite: Bricht etwas aus dem Bild heraus?
Aufrufen mit: node server/pruef-treff-seiten.mjs
Aufrufen mit: node server/pruef-neue-seiten.mjs
===================================================================== */
import { mkdtempSync, rmSync, mkdirSync } from "node:fs";
@@ -32,7 +37,7 @@ import { join } from "node:path";
import { request as httpAnfrage } from "node:http";
import { portMussFreiSein } from "./helfer-port.mjs";
const PORT = await portMussFreiSein(4403, "pruef-treff-seiten");
const PORT = await portMussFreiSein(4403, "pruef-neue-seiten");
const ordner = mkdtempSync(join(tmpdir(), "ws-treffs-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
@@ -45,7 +50,7 @@ const ec = express.response.cookie;
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
import { notbremse } from "./helfer-notbremse.mjs";
await import("./index.js");
notbremse(240_000, "pruef-treff-seiten");
notbremse(240_000, "pruef-neue-seiten");
await new Promise((r) => setTimeout(r, 700));
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
@@ -114,6 +119,24 @@ await roh(`/workspace/api/treff/eintrag/${beitrag}/melden`, { cookie: kGast },
await roh("/workspace/api/treff/massnahme", { cookie: kDogi },
{ person_id: idKessi, art: "hinweis", grund: "Bitte keine Nummern posten – einmalig, kein Drama." });
/* 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. */
const dd = new DatabaseSync(process.env.WORKSPACE_DB);
{
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");
}
dd.close();
const { chromium } = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await chromium.launch();
@@ -149,7 +172,19 @@ const KONTRAST = `(() => {
let gemessen = 0;
for (const e of document.querySelectorAll("p, li, h1, h2, h3, td, th, span, a, button")) {
const t = (e.textContent || "").trim();
if (!t || e.children.length) continue;
if (!t) continue;
/* ELEMENTE MIT KINDERN WERDEN UEBERSPRUNGEN, und das ist richtig:
Ihr textContent ist der Text der Kinder, die einzeln drankommen.
Es war aber ein STILLER Aussetzer -- ein <p> mit einem Link
darin fiel heraus, und der eigene Text daneben wurde nie
gemessen. Genau so ist die Fusszeile der Talente-Seite durch die
Pruefung gekommen, waehrend sie auf der Buehne lag.
Jetzt zaehlt der EIGENE Text: Hat ein Element neben seinen
Kindern eigene Zeichen, wird es mitgemessen. */
const eigen = [...e.childNodes]
.filter((n) => n.nodeType === 3).map((n) => n.textContent.trim()).join("");
if (e.children.length && eigen.length < 3) continue;
const k = e.getBoundingClientRect();
if (k.width < 4 || k.height < 4) continue;
const s = getComputedStyle(e);
@@ -297,6 +332,12 @@ for (const [name, pfad] of [
["regeln", "treff-regeln.html"],
["rechte", "rechte.html"],
["moderation", "treff-moderation.html"],
/* Die zwei Katalogseiten. Sie stehen hier und nicht in
pruef-nachwuchs: Dort wird gemessen, was der Server ANTWORTET,
hier, was auf dem Bildschirm LANDET. Beides ist noetig, und beides
findet andere Fehler. */
["entwicklung", "entwicklung.html"],
["talente", "talente.html"],
]) {
melde("");
melde(`=== ${pfad} ===`);
+18
View File
@@ -281,6 +281,24 @@ export const SEITEN = {
Stufen und die Bedingungen des Ausschlusses. */
"/workspace/treff-regeln.html": ["gast", "modi", "hand", "admin"],
/* DIE ENTWICKLUNGSKARTE (11.09.2026).
DogFather und die rechte Hand fuehren sie ueber das Team. Ein Modi
kommt ebenfalls auf die Seite -- aber er sieht dort AUSSCHLIESSLICH
Block 5, die sechs Fragen ueber sich selbst. Das entscheidet nicht
diese Tafel, sondern workspace-entwicklung.js: Hier steht nur, wer
die Tuer oeffnen darf, nicht was dahinter liegt.
ZWEI SCHLOESSER, ABSICHTLICH. Faellt die Rechtepruefung im Modul
weg, steht immer noch diese Zeile; faellt diese Zeile weg, steht
immer noch das Modul. */
"/workspace/entwicklung.html": ["admin", "hand", "modi"],
/* TALENTE. Hier stehen Namen von Menschen, die nichts davon wissen --
ausdruecklich OHNE "modi": Wer beobachtet wird, ohne es zu wissen,
hat ein Recht darauf, dass der Kreis klein bleibt. */
"/workspace/talente.html": ["admin", "hand"],
/* MELDUNGEN & MASSNAHMEN. Nur wer moderiert -- ausdruecklich OHNE
"gast": Dort stehen die Namen derer, die gemeldet haben. Eine
Meldung, die der Gemeldete lesen kann, ist keine Meldung mehr,
+637
View File
@@ -0,0 +1,637 @@
/* =====================================================================
Entwicklung & Nachwuchs — die Mechanik (11.09.2026)
=====================================================================
Filipe: *„ich will dass diese zwei kategorien perfektionniert werden,
ich will dass fertige aufgaben da stehen und so … so dass dogfather
und rechte hand es so einfach wie möglich haben aber so professionell
wie auch nur möglich."*
Die Kataloge stehen in workspace-entwicklungs-punkte.js und
workspace-talent-punkte.js. Hier steht, was mit ihnen passiert.
---------------------------------------------------------------------
EINE EIGENE TABELLE — UND WARUM DER PLAN DARIN FALSCH LAG
Der Plan im Vault sagt: „zwei Spalten, keine neue Tabelle" — der
Stand solle in `punkt_stand` liegen, derselben Tabelle wie die
Checkliste. Nachgemessen geht das NICHT:
PRIMARY KEY (bereich, schluessel, creator_id)
Es passt genau EIN Stand je Punkt und Person hinein. Der Kern dieses
Bereichs ist aber, dass DogFather und die rechte Hand UNABHAENGIG
voneinander setzen und die Abweichung das eigentliche Ergebnis ist.
Dafuer muesste der Schluessel um den Beurteiler wachsen — also ein
Tabellenneubau an lebenden Checklisten-Daten, fuer einen Zweck, der
mit der Checkliste nichts zu tun hat.
Deshalb eine eigene Tabelle. Der Plan war ein Wegweiser, keine
Wahrheit; gemessen wurde vor dem Bauen, nicht danach.
---------------------------------------------------------------------
DREI RIEGEL, DIE VON ANFANG AN STEHEN
1. BLOCK 5 GEHOERT DER PERSON. „Wie geht es dir damit?" beantwortet
nur sie selbst — lesend UND schreibend. Auch DogFather nicht. Was
nach aussen dringt, ist eine Ampel ohne Namen.
2. DER STAND DES ANDEREN KOMMT ERST NACH DEM EIGENEN. Wer zuerst
sieht, was der andere angeklickt hat, klickt dasselbe — das ist
kein Verdacht, sondern ein gut belegter Effekt. Er wird hier nicht
durch Disziplin verhindert, sondern dadurch, dass die Auskunft
erst gar nicht ausgeliefert wird.
3. TALENTE ENTHAELT NAMEN VON MENSCHEN, DIE NICHTS DAVON WISSEN.
Der Bereich gehoert DogFather und der rechten Hand. Kein Modi,
keine Agentur, und ganz sicher nicht die Community.
---------------------------------------------------------------------
UND EINE ABWEICHUNG VOM PLAN, DIE FILIPES SATZ GESCHULDET IST
Der Plan verlangt einen Anlass (die drei SBI-Felder) bei JEDEM Klick
ausser „kann ich nicht sagen". Bei 28 Punkten je Person waeren das 28
Textfelder — und „so einfach wie moeglich" war die andere Haelfte des
Auftrags. Pflicht ist der Anlass deshalb dort, wo er etwas
entscheidet: bei **„Da hakt es"**. Bei „Laeuft" und „Waechst gerade"
steht dasselbe Feld da, freiwillig — wer etwas Konkretes gesehen hat,
schreibt es hin, und beim Gespraech steht es da.
===================================================================== */
import express from "express";
import {
db, sitzungLesen, protokolliere, echteIp, fuehrtTeamDogi, ROLLEN_NAME,
} from "./workspace.js";
import {
ENTWICKLUNG_BLOECKE, ENTWICKLUNG_PUNKTE, ENTWICKLUNG_STUFEN,
ENTWICKLUNG_ERWARTUNG, ENTWICKLUNG_FREMD, ENTWICKLUNG_SELBST,
ENTWICKLUNG_NACH_SCHLUESSEL,
} from "./workspace-entwicklungs-punkte.js";
import {
TALENT_STUFEN, TALENT_GRUPPEN, TALENT_MERKMALE, TALENT_WARNZEICHEN,
TALENT_NACH_SCHLUESSEL, talentStufe, talentNaechste,
} from "./workspace-talent-punkte.js";
export const entwicklungRouter = express.Router();
const jetzt = () => new Date().toISOString();
const TAG = 86400000;
const ANLASS_MAX = 1200;
/* WANN EINE EINSCHAETZUNG VERBLASST.
Nach 90 Tagen zaehlt ein Stand nicht mehr als aktuell. Er wird NICHT
geloescht -- man soll sehen, dass etwas alt ist, statt dass es
verschwindet. Eine Akte, die alles behaelt, hat hier nichts zu
suchen; eine, die heimlich vergisst, auch nicht. */
export const VERBLASST_NACH_TAGEN = 90;
/* Die Rollen, ueber die eine Entwicklungskarte gefuehrt wird. Das Team
von Dogi -- nicht die Agentur, nicht die Community. Als Liste und
nicht als "alle ausser X": Eine neue Rolle soll hier NICHT
automatisch eine Personalakte bekommen. */
const KARTEN_ROLLEN = ["hand", "modi"];
/* =====================================================================
DIE TABELLEN
===================================================================== */
let bereit = false;
function tabellen() {
if (bereit) return;
db().exec(`
/* Eine Einschaetzung: WER hat bei WEM zu WELCHEM Punkt was gesetzt.
Der Beurteiler steht IM Schluessel. Genau daran ist die Idee
gescheitert, dieselbe Tabelle wie die Checkliste zu benutzen --
dort passt nur ein Stand je Punkt und Person hinein. */
CREATE TABLE IF NOT EXISTS entwicklung_stand (
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
schluessel TEXT NOT NULL,
von_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
stufe TEXT NOT NULL CHECK (stufe IN ('laeuft','waechst','hakt','unklar')),
anlass TEXT,
am TEXT NOT NULL,
PRIMARY KEY (person_id, schluessel, von_id)
);
CREATE INDEX IF NOT EXISTS idx_entwicklung_person
ON entwicklung_stand (person_id, am);
/* Ein angeklicktes Merkmal an einem Kandidaten.
KEIN von_id IM SCHLUESSEL, anders als oben -- und das ist
Absicht: Bei einem Talent geht es nicht um zwei Meinungen,
sondern darum, OB etwas beobachtet wurde. Zwei Leute, die
dasselbe gesehen haben, haben es einmal gesehen. Wer es gesetzt
hat, steht trotzdem dabei. */
CREATE TABLE IF NOT EXISTS talent_merkmal (
eintrag_id INTEGER NOT NULL REFERENCES eintraege(id) ON DELETE CASCADE,
schluessel TEXT NOT NULL,
von_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
am TEXT NOT NULL,
PRIMARY KEY (eintrag_id, schluessel)
);
/* Die Trichterstufe eines Kandidaten.
EIGENE TABELLE UND NICHT eintraege.status: Der Status dort hat
drei Werte fuer etwas ganz anderes (offen/erledigt/angenommen),
und die Stufe braucht ausserdem ein SEIT -- ohne das gibt es
keine Standzeit, und ohne Standzeit wird aus dem Trichter wieder
eine Liste. */
CREATE TABLE IF NOT EXISTS talent_stufe (
eintrag_id INTEGER PRIMARY KEY REFERENCES eintraege(id) ON DELETE CASCADE,
stufe TEXT NOT NULL
CHECK (stufe IN ('aufgefallen','beobachtet','angesprochen','probe','imteam')),
seit TEXT NOT NULL,
von_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
notiz TEXT
);
`);
bereit = true;
}
/* =====================================================================
DIE HUERDEN
===================================================================== */
function angemeldet(req, res, next) {
const person = sitzungLesen(req);
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
req.person = person;
try { tabellen(); } catch (f) {
console.error("[entwicklung] Tabellen:", f?.message);
return res.status(503).json({ fehler: "nicht_verfuegbar" });
}
next();
}
/** Nur DogFather und die rechte Hand. 404 und nicht 403 -- wer „kein
* Zugriff" liest, fragt als Naechstes, wer denn dann. */
function nurLeitung(req, res, next) {
if (fuehrtTeamDogi(req.person)) return next();
return res.status(404).json({ fehler: "nicht_gefunden" });
}
function gleicheHerkunft(req, res, next) {
const herkunft = req.get("origin");
if (!herkunft) return next();
let erlaubt;
try { erlaubt = new URL(herkunft).host === req.get("host"); } catch { erlaubt = false; }
if (!erlaubt) return res.status(403).json({ fehler: "fremde_herkunft" });
next();
}
entwicklungRouter.use("/workspace/api/entwicklung", angemeldet);
entwicklungRouter.use("/workspace/api/talente", angemeldet);
entwicklungRouter.use("/workspace/api/talente", nurLeitung);
const nummer = (x) => { const n = Number(x); return Number.isInteger(n) && n > 0 ? n : 0; };
const text = (x, max) => String(x ?? "").trim().slice(0, max);
const tageSeit = (iso) => {
const t = Date.parse(iso || "");
return Number.isFinite(t) ? Math.floor((Date.now() - t) / TAG) : null;
};
/* =====================================================================
ENTWICKLUNG
===================================================================== */
/* Der Katalog, wie ihn DIESE Person sehen darf.
Block 5 faellt fuer alle ausser der Person selbst heraus -- und zwar
schon hier, nicht erst in der Oberflaeche. */
function katalogFuer(person, ueberMichSelbst) {
return ENTWICKLUNG_BLOECKE
.filter((b) => !b.nurSelbst || ueberMichSelbst)
.map((b) => ({ ...b, punkte: ENTWICKLUNG_PUNKTE[b.schluessel] || [] }));
}
entwicklungRouter.get("/workspace/api/entwicklung/lage", (req, res) => {
try {
const leitung = fuehrtTeamDogi(req.person);
const platz = KARTEN_ROLLEN.map(() => "?").join(", ");
const personen = leitung
? db().prepare(`SELECT id, name, rolle, erstellt FROM personen
WHERE rolle IN (${platz}) AND aktiv = 1 ORDER BY name COLLATE NOCASE`)
.all(...KARTEN_ROLLEN)
: [];
/* Je Person: wie viele Punkte ICH schon gesetzt habe, wie viele
davon verblasst sind, und wie viele wir beide verschieden sehen.
Die Abweichung ist das Ergebnis -- deshalb steht sie schon in
der Uebersicht und nicht erst in der Karte. */
const gesetzt = db().prepare(
"SELECT person_id, COUNT(*) AS n FROM entwicklung_stand WHERE von_id = ? GROUP BY person_id")
.all(req.person.id);
const meineZahl = new Map(gesetzt.map((z) => [z.person_id, z.n]));
const alt = db().prepare(
`SELECT person_id, COUNT(*) AS n FROM entwicklung_stand
WHERE von_id = ? AND am < ? GROUP BY person_id`)
.all(req.person.id, new Date(Date.now() - VERBLASST_NACH_TAGEN * TAG).toISOString());
const alteZahl = new Map(alt.map((z) => [z.person_id, z.n]));
const anders = db().prepare(
`SELECT a.person_id, COUNT(*) AS n
FROM entwicklung_stand a JOIN entwicklung_stand b
ON a.person_id = b.person_id AND a.schluessel = b.schluessel
WHERE a.von_id = ? AND b.von_id <> ? AND a.stufe <> b.stufe
AND a.stufe <> 'unklar' AND b.stufe <> 'unklar'
GROUP BY a.person_id`).all(req.person.id, req.person.id);
const andersZahl = new Map(anders.map((z) => [z.person_id, z.n]));
res.json({
leitung,
stufen: ENTWICKLUNG_STUFEN,
erwartung: ENTWICKLUNG_ERWARTUNG,
verblasst_nach: VERBLASST_NACH_TAGEN,
gesamt_fremd: ENTWICKLUNG_FREMD.length,
personen: personen.map((p) => ({
id: p.id, name: p.name,
rolle_name: ROLLEN_NAME[p.rolle] ?? p.rolle,
dabei_seit_tagen: tageSeit(p.erstellt),
gesetzt: meineZahl.get(p.id) || 0,
veraltet: alteZahl.get(p.id) || 0,
abweichungen: andersZahl.get(p.id) || 0,
})),
/* Die eigene Karte -- fuer jeden, der im Team ist. */
ich: { id: req.person.id, name: req.person.name },
});
} catch (f) {
console.error("[entwicklung] Lage:", f?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
entwicklungRouter.get("/workspace/api/entwicklung/person/:id", nurLeitung, (req, res) => {
try {
const id = nummer(req.params.id);
const p = id && db().prepare("SELECT id, name, rolle FROM personen WHERE id = ?").get(id);
if (!p || !KARTEN_ROLLEN.includes(p.rolle)) {
return res.status(404).json({ fehler: "nicht_gefunden" });
}
const meine = new Map(db().prepare(
"SELECT schluessel, stufe, anlass, am FROM entwicklung_stand WHERE person_id = ? AND von_id = ?")
.all(id, req.person.id).map((z) => [z.schluessel, z]));
/* DER STAND DES ANDEREN -- nur zu Punkten, bei denen ich selbst
schon gesetzt habe. Der Riegel sitzt hier, nicht in der
Oberflaeche: Was nicht ausgeliefert wird, kann niemand ankern. */
const fremde = db().prepare(
`SELECT s.schluessel, s.stufe, s.anlass, s.am, v.name AS von_name
FROM entwicklung_stand s LEFT JOIN personen v ON v.id = s.von_id
WHERE s.person_id = ? AND s.von_id <> ?`).all(id, req.person.id);
const grenze = Date.now() - VERBLASST_NACH_TAGEN * TAG;
const bloecke = katalogFuer(req.person, false).map((b) => ({
...b,
punkte: b.punkte.map((pt) => {
const m = meine.get(pt.schluessel) || null;
const andere = m
? fremde.filter((f) => f.schluessel === pt.schluessel)
.map((f) => ({ von_name: f.von_name, stufe: f.stufe, anlass: f.anlass, am: f.am }))
: null;
return {
...pt,
meins: m ? { ...m, verblasst: Date.parse(m.am) < grenze } : null,
andere,
/* WIE VIELE ES SONST NOCH GIBT -- ohne zu sagen, was sie
gesetzt haben. Ohne diese Zahl waere „noch nichts von der
rechten Hand" und „sie hat gesetzt, du siehst es nur
nicht" dasselbe Bild, und das erste ist eine Auskunft,
das zweite eine Taeuschung. */
andere_offen: m ? 0 : fremde.filter((f) => f.schluessel === pt.schluessel).length,
};
}),
}));
/* DIE ABWEICHUNGEN ZUERST. Sie sind das Ergebnis, nicht ein
Nebenprodukt: Wo zwei Leute dasselbe sehen, gibt es nichts zu
besprechen. */
const abweichungen = [];
for (const b of bloecke) {
for (const pt of b.punkte) {
if (!pt.meins || !pt.andere?.length) continue;
for (const a of pt.andere) {
if (a.stufe === pt.meins.stufe) continue;
if (a.stufe === "unklar" || pt.meins.stufe === "unklar") continue;
abweichungen.push({
schluessel: pt.schluessel, titel: pt.titel, block: b.name,
meins: pt.meins.stufe, andere: a.stufe, von_name: a.von_name,
});
}
}
}
res.json({
person: { id: p.id, name: p.name, rolle_name: ROLLEN_NAME[p.rolle] ?? p.rolle },
stufen: ENTWICKLUNG_STUFEN,
erwartung: ENTWICKLUNG_ERWARTUNG,
bloecke,
abweichungen,
gesetzt: meine.size,
gesamt: ENTWICKLUNG_FREMD.length,
});
} catch (f) {
console.error("[entwicklung] Person:", f?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
const STUFEN_SATZ = new Set(ENTWICKLUNG_STUFEN.map((s) => s.schluessel));
entwicklungRouter.put("/workspace/api/entwicklung/person/:id/:schluessel",
gleicheHerkunft, nurLeitung, (req, res) => {
try {
const id = nummer(req.params.id);
const schluessel = String(req.params.schluessel || "");
const stufe = String(req.body?.stufe || "");
const anlass = text(req.body?.anlass, ANLASS_MAX);
const p = id && db().prepare("SELECT id, rolle FROM personen WHERE id = ?").get(id);
if (!p || !KARTEN_ROLLEN.includes(p.rolle)) {
return res.status(404).json({ fehler: "nicht_gefunden" });
}
/* NUR PUNKTE, DIE ANDERE SETZEN DUERFEN. Block 5 faellt hier
heraus -- und zwar an derselben Stelle, an der auch die
Auslieferung ihn wegnimmt. Ein Schluessel aus Block 5 sieht
von aussen genauso aus wie ein erfundener. */
if (!ENTWICKLUNG_FREMD.some((x) => x.schluessel === schluessel)) {
return res.status(404).json({ fehler: "nicht_gefunden" });
}
if (!STUFEN_SATZ.has(stufe)) return res.status(400).json({ fehler: "unbekannte_stufe" });
/* DER ANLASS IST PFLICHT, WO ER ETWAS ENTSCHEIDET.
„Da hakt es" ohne Beispiel ist eine Meinung; mit Beispiel ist
es ein Gespraech. Bei den anderen beiden steht dasselbe Feld
freiwillig da. */
if (stufe === "hakt" && anlass.length < 10) {
return res.status(400).json({
fehler: "Woran hast du das gemerkt? Ein Satz genügt – ohne ihn "
+ "ist es beim Gespräch nur noch ein Gefühl.",
});
}
db().prepare(`INSERT INTO entwicklung_stand
(person_id, schluessel, von_id, stufe, anlass, am)
VALUES (?,?,?,?,?,?)
ON CONFLICT(person_id, schluessel, von_id) DO UPDATE SET
stufe = excluded.stufe, anlass = excluded.anlass, am = excluded.am`)
.run(id, schluessel, req.person.id, stufe, anlass || null, jetzt());
res.json({ ok: true });
} catch (f) {
console.error("[entwicklung] Setzen:", f?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* ---------- Block 5: die eigene Karte -------------------------------- */
/* KEIN :id IN DER ADRESSE, und das ist der Riegel.
Eine Route /entwicklung/mich/:id waere eine Einladung, eine fremde
Nummer einzusetzen -- und dann haengt alles an einer einzigen
Vergleichszeile. Hier gibt es die Nummer gar nicht: Es ist immer die
Person, die fragt. */
entwicklungRouter.get("/workspace/api/entwicklung/mich", (req, res) => {
try {
const meine = new Map(db().prepare(
"SELECT schluessel, stufe, anlass, am FROM entwicklung_stand WHERE person_id = ? AND von_id = ?")
.all(req.person.id, req.person.id).map((z) => [z.schluessel, z]));
const block = ENTWICKLUNG_BLOECKE.find((b) => b.nurSelbst);
res.json({
block,
stufen: ENTWICKLUNG_STUFEN,
punkte: (ENTWICKLUNG_PUNKTE[block.schluessel] || []).map((pt) => ({
...pt, meins: meine.get(pt.schluessel) || null,
})),
});
} catch (f) {
console.error("[entwicklung] Mich:", f?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
entwicklungRouter.put("/workspace/api/entwicklung/mich/:schluessel", gleicheHerkunft,
(req, res) => {
try {
const schluessel = String(req.params.schluessel || "");
const stufe = String(req.body?.stufe || "");
if (!ENTWICKLUNG_SELBST.some((x) => x.schluessel === schluessel)) {
return res.status(404).json({ fehler: "nicht_gefunden" });
}
if (!STUFEN_SATZ.has(stufe)) return res.status(400).json({ fehler: "unbekannte_stufe" });
/* KEIN ANLASS, NIRGENDS. Hier antwortet jemand ueber sich selbst;
ein Pflichtfeld waere die zuverlaessigste Art, ehrliche
Antworten zu verhindern. */
db().prepare(`INSERT INTO entwicklung_stand
(person_id, schluessel, von_id, stufe, anlass, am)
VALUES (?,?,?,?,NULL,?)
ON CONFLICT(person_id, schluessel, von_id) DO UPDATE SET
stufe = excluded.stufe, am = excluded.am`)
.run(req.person.id, schluessel, req.person.id, stufe, jetzt());
res.json({ ok: true });
} catch (f) {
console.error("[entwicklung] Mich setzen:", f?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* ---------- Die Ampel ohne Namen -------------------------------------- */
/* WAS HIER HERAUSKOMMT UND WAS NICHT.
Heraus kommt: wie viele im Team gerade mindestens einen Punkt aus
Block 5 auf „da hakt es" stehen haben -- und ab zwei Personen auch
das nicht mehr genau, sondern als Spanne.
Nicht heraus kommt: wer, welcher Punkt, seit wann.
WARUM UEBERHAUPT ETWAS: Ein Block, den niemand je sieht, ist ein
Block, den niemand ausfuellt. Die Ampel ist der einzige Weg von
dort nach draussen -- und sie ist absichtlich so schmal, dass man
daraus niemanden erkennen kann.
AB WELCHER ZAHL SIE UEBERHAUPT ETWAS SAGT: Bei einem Team von zwei
Personen waere „einer hat gerade viel" dasselbe wie ein Name.
Deshalb schweigt sie unter MINDESTENS_TEAM Personen -- mit Grund,
nicht stillschweigend. */
const MINDESTENS_TEAM = 3;
entwicklungRouter.get("/workspace/api/entwicklung/ampel", nurLeitung, (req, res) => {
try {
const platz = KARTEN_ROLLEN.map(() => "?").join(", ");
const { n: team } = db().prepare(
`SELECT COUNT(*) AS n FROM personen WHERE rolle IN (${platz}) AND aktiv = 1`)
.all(...KARTEN_ROLLEN)[0] || { n: 0 };
if (team < MINDESTENS_TEAM) {
/* DREI VERSCHIEDENE LAGEN, DREI VERSCHIEDENE SAETZE.
Der erste Entwurf hatte einen Satz fuer alle: "Solange ihr zu
{n} seid …". Im Bildschirmfoto stand dort "Solange ihr zu 1
seid" -- und bei einem leeren Team "zu 0". Ein Satz, der bei
den haeufigsten Faellen falsches Deutsch ergibt, liest sich
wie ein Fehler, und ab da glaubt man auch dem Rest nicht mehr.
Gefunden hat das kein Test, sondern ein Blick auf die Seite. */
return res.json({
sagbar: false, team,
grund: team === 0
? "Noch ist niemand im Team. Sobald jemand dazukommt, kann hier etwas stehen."
: team === 1
? "Es ist gerade eine Person im Team – da wäre jede Auskunft hier "
+ "dasselbe wie ein Name. Frag sie direkt."
: `Ihr seid zu ${team}. Jede Auskunft an dieser Stelle wäre damit `
+ "so gut wie ein Name – frag sie direkt, das ist hier ohnehin der bessere Weg.",
});
}
const schluessel = ENTWICKLUNG_SELBST.map((p) => p.schluessel);
const pl = schluessel.map(() => "?").join(", ");
const { n: betroffen } = db().prepare(
`SELECT COUNT(DISTINCT person_id) AS n FROM entwicklung_stand
WHERE schluessel IN (${pl}) AND stufe = 'hakt' AND person_id = von_id`)
.all(...schluessel)[0] || { n: 0 };
const { n: geantwortet } = db().prepare(
`SELECT COUNT(DISTINCT person_id) AS n FROM entwicklung_stand
WHERE schluessel IN (${pl}) AND person_id = von_id`)
.all(...schluessel)[0] || { n: 0 };
res.json({
sagbar: true, team, geantwortet,
/* Keine genaue Zahl: „einer" waere bei kleinen Teams wieder ein
Name. Es gibt drei Antworten, und mehr braucht es nicht. */
lage: betroffen === 0 ? "ruhig" : betroffen === 1 ? "jemand" : "mehrere",
text: betroffen === 0
? (geantwortet === 0
? "Noch hat niemand geantwortet. Die Ampel sagt erst etwas, wenn jemand da war."
: "Gerade meldet niemand, dass es zu viel wird.")
: betroffen === 1
? "Jemand im Team hat gerade viel. Kein Name – frag reihum, nicht gezielt."
: "Mehrere im Team haben gerade viel. Das ist selten ein Zufall.",
});
} catch (f) {
console.error("[entwicklung] Ampel:", f?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* =====================================================================
TALENTE
===================================================================== */
/** Die Karte eines Kandidaten -- Stufe, Standzeit, was zu tun ist. */
function talentKarte(e, merkmale) {
const stand = db().prepare("SELECT stufe, seit, notiz FROM talent_stufe WHERE eintrag_id = ?")
.get(e.id);
const stufe = talentStufe(stand?.stufe) || TALENT_STUFEN[0];
const seit = stand?.seit || e.erstellt;
const tage = tageSeit(seit);
const gesetzt = merkmale.filter((m) => m.eintrag_id === e.id).map((m) => m.schluessel);
const warn = gesetzt.filter((s) => TALENT_NACH_SCHLUESSEL.get(s)?.warnung);
return {
id: e.id, titel: e.titel, text: e.text, einsatz: e.einsatz,
stufe: stufe.schluessel, stufe_name: stufe.name,
bedeutet: stufe.bedeutet, schritt: stufe.schritt, knopf: stufe.knopf,
naechste: talentNaechste(stufe.schluessel)?.schluessel || null,
seit, tage,
/* LIEGENGEBLIEBEN IST EINE AUSKUNFT UEBER UNS, nicht ueber den
Kandidaten. Deshalb steht sie an der Karte und nicht in einer
Erinnerungsmail, die man wegklickt. */
liegt: stufe.frist !== null && tage !== null && tage > stufe.frist,
merkmale: gesetzt.filter((s) => !TALENT_NACH_SCHLUESSEL.get(s)?.warnung),
warnzeichen: warn,
notiz: stand?.notiz || null,
};
}
entwicklungRouter.get("/workspace/api/talente/lage", (req, res) => {
try {
const eintraege = db().prepare(
`SELECT id, titel, text, einsatz, erstellt FROM eintraege
WHERE bereich = 'talente' ORDER BY id DESC LIMIT 300`).all();
const merkmale = eintraege.length
? db().prepare(
`SELECT eintrag_id, schluessel FROM talent_merkmal
WHERE eintrag_id IN (${eintraege.map(() => "?").join(", ")})`)
.all(...eintraege.map((e) => e.id))
: [];
const karten = eintraege.map((e) => talentKarte(e, merkmale));
res.json({
stufen: TALENT_STUFEN,
gruppen: TALENT_GRUPPEN.map((g) => ({ ...g, merkmale: TALENT_MERKMALE[g.schluessel] || [] })),
warnzeichen: TALENT_WARNZEICHEN,
kandidaten: karten,
/* Wie viele auf jeder Stufe liegen -- der Trichter als Zahlen.
Abgeleitet, damit er nicht danebenliegen kann. */
trichter: TALENT_STUFEN.map((s) => ({
schluessel: s.schluessel, name: s.name,
anzahl: karten.filter((k) => k.stufe === s.schluessel).length,
liegt: karten.filter((k) => k.stufe === s.schluessel && k.liegt).length,
})),
});
} catch (f) {
console.error("[talente] Lage:", f?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
entwicklungRouter.put("/workspace/api/talente/:id/merkmal/:schluessel", gleicheHerkunft,
(req, res) => {
try {
const id = nummer(req.params.id);
const schluessel = String(req.params.schluessel || "");
const e = id && db().prepare(
"SELECT id, bereich FROM eintraege WHERE id = ?").get(id);
if (!e || e.bereich !== "talente") return res.status(404).json({ fehler: "nicht_gefunden" });
if (!TALENT_NACH_SCHLUESSEL.has(schluessel)) {
return res.status(404).json({ fehler: "nicht_gefunden" });
}
/* EIN KNOPF, ZWEI RICHTUNGEN. Ein Merkmal ist gesehen oder nicht;
ein zweiter Klick nimmt es zurueck. Kein eigener Weg zum
Abwaehlen -- der waere die Stelle, an der die Rechtepruefung
irgendwann fehlt. */
const schon = db().prepare(
"SELECT 1 AS x FROM talent_merkmal WHERE eintrag_id = ? AND schluessel = ?")
.get(id, schluessel);
if (schon) {
db().prepare("DELETE FROM talent_merkmal WHERE eintrag_id = ? AND schluessel = ?")
.run(id, schluessel);
} else {
db().prepare(
"INSERT INTO talent_merkmal (eintrag_id, schluessel, von_id, am) VALUES (?,?,?,?)")
.run(id, schluessel, req.person.id, jetzt());
}
res.json({ ok: true, gesetzt: !schon });
} catch (f) {
console.error("[talente] Merkmal:", f?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
entwicklungRouter.put("/workspace/api/talente/:id/stufe", gleicheHerkunft, (req, res) => {
try {
const id = nummer(req.params.id);
const stufe = String(req.body?.stufe || "");
const notiz = text(req.body?.notiz, ANLASS_MAX);
const e = id && db().prepare("SELECT id, bereich, titel FROM eintraege WHERE id = ?").get(id);
if (!e || e.bereich !== "talente") return res.status(404).json({ fehler: "nicht_gefunden" });
if (!talentStufe(stufe)) return res.status(400).json({ fehler: "unbekannte_stufe" });
db().prepare(`INSERT INTO talent_stufe (eintrag_id, stufe, seit, von_id, notiz)
VALUES (?,?,?,?,?)
ON CONFLICT(eintrag_id) DO UPDATE SET
stufe = excluded.stufe, seit = excluded.seit,
von_id = excluded.von_id, notiz = excluded.notiz`)
.run(id, stufe, jetzt(), req.person.id, notiz || null);
protokolliere("talent_stufe", {
personId: req.person.id, ip: echteIp(req),
detail: `${e.titel}: ${stufe}`.slice(0, 120),
});
res.json({ ok: true, stufe });
} catch (f) {
console.error("[talente] Stufe:", f?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
+275
View File
@@ -0,0 +1,275 @@
/* =====================================================================
Der Katalog „Entwicklung" (11.09.2026)
Filipe: *"ich will dass diese zwei kategorien perfektionniert werden,
ich will dass fertige aufgaben da stehen und so … so dass dogfather
und rechte hand es so einfach wie möglich haben aber so
professionell wie auch nur möglich."*
Hier steht die eine Hälfte davon: das, was man anklickt, statt es zu
tippen. Die Herleitung jeder einzelnen Zeile steht im Vault
(02 Projekte/Entwicklung & Nachwuchs – Plan.md, Abschnitt 1 und 3);
hier steht bei jedem Punkt in zwei Sätzen, WARUM er im Katalog ist.
---------------------------------------------------------------------
VIER ENTSCHEIDUNGEN, DIE MAN DEM KATALOG NICHT ANSIEHT
---------------------------------------------------------------------
1. ES SIND BEOBACHTUNGEN, KEINE EIGENSCHAFTEN.
Nicht „ist zuverlässig", sondern „Absagen kommen rechtzeitig".
Eine Eigenschaft kann man nur bestätigen oder bestreiten; ein
Verhalten kann man gesehen haben — oder eben nicht. Das ist der
Unterschied zwischen einem Gespräch und einem Urteil.
2. JEDER PUNKT IST ALS ETWAS FORMULIERT, DAS PASSIERT — nicht als
etwas, das fehlt. „Fragen bleiben nicht unbeantwortet" statt
„ignoriert Fragen". Die Liste soll man mit jemandem durchgehen
können, ohne dass sie wie eine Anklageschrift klingt.
3. DIE STUFEN SIND KEINE NOTEN, SONDERN ERWARTUNGEN.
`probe` ist das, was ab Tag eins gilt. `standard` das, was nach
ein paar Monaten normal ist. `senior` das, wofür man jemanden
fragt. Wer auf Probe ist, wird nicht daran gemessen, ob ein
Bereich ohne ihn läuft — und die Oberfläche zeigt das auch so.
4. KEINE GESAMTNOTE, NIRGENDS. Es gibt keine Punktzahl, keinen
Durchschnitt, keine Rangliste. Das ist keine Nachlässigkeit,
sondern der wichtigste Einzelentscheid des ganzen Bereichs: Eine
Zahl über einem Menschen wird gelesen, verglichen und
weitererzählt. Gebraucht wird ein Gesprächsanlass, keine Note.
---------------------------------------------------------------------
UND DER LETZTE BLOCK GEHOERT NICHT EUCH
Block 5 beantwortet ausschliesslich die Person selbst. DogFather und
die rechte Hand sehen dort keinen einzigen Klick — nur eine Ampel
ohne Namen. Das ist kein Entgegenkommen, sondern die Bedingung
dafuer, dass ehrlich geantwortet wird; eine unehrliche Antwort waere
schlechter als keine.
Die sechs Punkte darin stammen aus der Forschung dazu, warum
freiwillige Moderatoren aufhoeren — in dieser Reihenfolge: zu wenig
Zeit, Streit im Team, fehlende Eskalationswege, seelische Last ohne
Ansprechpartner, fehlende Autonomie, fehlende Anerkennung.
===================================================================== */
/** Die vier Antworten. Reihenfolge und Bedeutung stehen hier, nicht in
* der Oberflaeche -- sonst hiesse dieselbe Antwort auf zwei Seiten
* unterschiedlich.
*
* "unklar" IST EINE ECHTE ANTWORT und kein Weglassen. Ohne sie klickt
* man im Zweifel "laeuft", weil das die freundlichste Taste ist --
* und ab da steht im Katalog eine Beobachtung, die niemand gemacht
* hat. */
export const ENTWICKLUNG_STUFEN = [
{ schluessel: "laeuft", name: "Läuft", zeichen: "✓",
text: "Habe ich gesehen. Passt." },
{ schluessel: "waechst", name: "Wächst gerade", zeichen: "↗",
text: "Geht in die richtige Richtung – noch nicht durchgehend." },
{ schluessel: "hakt", name: "Da hakt es", zeichen: "!",
text: "Ist mir aufgefallen. Kein Vorwurf – ein Thema." },
{ schluessel: "unklar", name: "Kann ich nicht sagen", zeichen: "–",
text: "Habe ich nicht gesehen. Ehrlicher als zu raten." },
];
/** Die Erwartungsstufen. `rang` nur zum Sortieren -- er ist keine Note. */
export const ENTWICKLUNG_ERWARTUNG = [
{ schluessel: "probe", name: "Ab Tag eins", rang: 1,
text: "Das gilt für alle, auch in der Probezeit." },
{ schluessel: "standard", name: "Nach ein paar Monaten", rang: 2,
text: "Das ist normal, wenn jemand angekommen ist." },
{ schluessel: "senior", name: "Wofür man jemanden fragt", rang: 3,
text: "Das kann nicht jeder – und es wird auch nicht von jedem erwartet." },
];
/** Die fünf Blöcke. `nurSelbst` ist der Riegel: Was hier `true` steht,
* sieht ausser der Person NIEMAND -- auch DogFather nicht. */
export const ENTWICKLUNG_BLOECKE = [
{ schluessel: "chat", name: "Im Chat",
text: "Die tägliche Arbeit. Was hier passiert, sehen die Zuschauer – "
+ "und es entscheidet, ob sie wiederkommen." },
{ schluessel: "druck", name: "Wenn es eng wird",
text: "Der Teil, der sich nicht üben lässt. Hier zeigt sich, was bleibt, "
+ "wenn es unangenehm wird." },
{ schluessel: "team", name: "Im Team",
text: "Wie die Zusammenarbeit läuft. Streit im Team ist der zweithäufigste "
+ "Grund, warum Leute aufhören – noch vor der Arbeit selbst." },
{ schluessel: "selbst", name: "Wie viel von selbst",
text: "Der Unterschied zwischen Ausführen und Mitdenken. Und – weiter unten – "
+ "zwischen Mitdenken und Überlastung." },
{ schluessel: "befinden", name: "Wie geht es dir damit?", nurSelbst: true,
text: "Diese sechs Fragen beantwortest nur du. Niemand aus dem Team sieht "
+ "deine Antworten – auch DogFather nicht. Sichtbar wird nur, ob es im "
+ "Team gerade jemandem zu viel wird, ohne Namen." },
];
export const ENTWICKLUNG_PUNKTE = {
/* ---- Block 1: Im Chat --------------------------------------------- */
chat: [
{ schluessel: "e-chat-neue-begruesst", stufe: "probe",
titel: "Neue werden begrüßt statt übersehen",
text: "Der erste Satz an einen Neuen entscheidet, ob er wiederkommt. "
+ "Kostet nichts und passiert trotzdem selten." },
{ schluessel: "e-chat-fragen-beantwortet", stufe: "probe",
titel: "Fragen bleiben nicht unbeantwortet",
text: "Eine unbeantwortete Frage ist schlimmer als eine abgelehnte – "
+ "sie sagt: du bist hier nicht gemeint." },
{ schluessel: "e-chat-regel-erklaert", stufe: "probe",
titel: "Regeln werden erklärt, bevor sie durchgesetzt werden",
text: "Wer erst löscht und dann erklärt, hat einen Gegner statt eines Mitglieds." },
{ schluessel: "e-chat-frueh-abgefangen", stufe: "standard",
titel: "Ein Spruch wird abgefangen, bevor eine Debatte daraus wird",
text: "Die eigentliche Arbeit passiert vorher, nicht beim Timeout. "
+ "Was man hier sieht, ist das, was NICHT passiert ist." },
{ schluessel: "e-chat-ton-bleibt", stufe: "standard",
titel: "Der Ton bleibt derselbe, auch wenn der Chat kippt",
text: "Nicht Freundlichkeit – Gleichmäßigkeit. Genau darauf achtet ein Chat: "
+ "ob dieselbe Sache heute anders behandelt wird als gestern." },
{ schluessel: "e-chat-ohne-buehne", stufe: "standard",
titel: "Es wird eingegriffen, ohne den Chat zur Bühne zu machen",
text: "Eine Bestrafung mit Publikum erzeugt Publikum." },
{ schluessel: "e-chat-laeuft-ohne-blick", stufe: "senior",
titel: "Der Chat läuft auch, wenn gerade niemand hinsieht",
text: "Das Gegenteil von Anwesenheit. Man merkt es nur daran, dass nichts passiert." },
{ schluessel: "e-chat-weitergereicht", stufe: "senior",
titel: "Neue werden weitergereicht, nicht nur bedient",
text: "Aus einem beantworteten Neuen wird ein Mitglied, sobald er jemanden kennt." },
],
/* ---- Block 2: Wenn es eng wird ------------------------------------ */
druck: [
{ schluessel: "e-druck-fragt-statt-raet", stufe: "probe",
titel: "Bei Unsicherheit wird gefragt statt geraten",
text: "Fragen ist ein Zeichen von Sicherheit, nicht von Schwäche – "
+ "und genau das misst, ob sich jemand im Team traut." },
{ schluessel: "e-druck-eigener-fehler", stufe: "probe",
titel: "Eine eigene Fehlentscheidung wird von selbst angesprochen",
text: "Der einzige Punkt der Liste, der sich nicht vortäuschen lässt." },
{ schluessel: "e-druck-nicht-persoenlich", stufe: "standard",
titel: "Provokation geht nicht ins Persönliche zurück",
text: "Unhöflichkeit ist in vergleichbaren Gemeinschaften der häufigste "
+ "Ablehnungsgrund – noch vor jedem Können." },
{ schluessel: "e-druck-notiz-statt-erinnerung", stufe: "standard",
titel: "Nach einem Vorfall steht eine Notiz da, keine Erinnerung",
text: "Am nächsten Tag fehlt die Hälfte. Eine Notiz von gestern schlägt "
+ "ein gutes Gedächtnis." },
{ schluessel: "e-druck-frueh-eskaliert", stufe: "standard",
titel: "Es wird eskaliert, bevor es zu spät ist – nicht danach",
text: "Zu früh eskalieren ist ein kleiner Fehler. Zu spät ist der teure." },
{ schluessel: "e-druck-streit-beruhigt", stufe: "senior",
titel: "Ein Streit zwischen zwei anderen wird beruhigt, ohne Partei zu sein",
text: "Unparteilichkeit unter Druck – in jeder Moderationsanleitung der erste Punkt." },
{ schluessel: "e-druck-unpopulaer-getragen", stufe: "senior",
titel: "Eine unpopuläre Entscheidung wird getragen, auch öffentlich",
text: "Wer eine Regel nur durchsetzt, solange Applaus kommt, setzt sie nicht durch." },
],
/* ---- Block 3: Im Team --------------------------------------------- */
team: [
{ schluessel: "e-team-absage-rechtzeitig", stufe: "probe",
titel: "Absagen kommen rechtzeitig, nicht im letzten Moment",
text: "Nicht Zuverlässigkeit – Vorlaufzeit. Die kann man planen, "
+ "eine Absage um 19:55 nicht." },
{ schluessel: "e-team-teamchat-gelesen", stufe: "probe",
titel: "Was im Teamchat steht, wird gelesen",
text: "Der billigste Punkt der Liste und der am häufigsten gerissene." },
{ schluessel: "e-team-wissen-geteilt", stufe: "standard",
titel: "Wissen wird geteilt, nicht gehortet",
text: "Wer als Einziger weiß, wie etwas geht, ist kein Gewinn, sondern ein "
+ "Ausfallrisiko – und darf nicht krank werden." },
{ schluessel: "e-team-kritik-direkt", stufe: "standard",
titel: "Kritik an anderen kommt zu ihnen, nicht über sie",
text: "Streit im Team ist der zweithäufigste Grund, warum Moderatoren "
+ "aufhören. Dieser eine Punkt bewacht ihn." },
{ schluessel: "e-team-zusagen-halten", stufe: "standard",
titel: "Zusagen halten – und wenn nicht, wird es gesagt",
text: "Der zweite Halbsatz ist der wichtigere." },
{ schluessel: "e-team-neue-eingearbeitet", stufe: "senior",
titel: "Neue werden eingearbeitet, ohne dass jemand darum bittet",
text: "Der Punkt, an dem aus Mitarbeit Verantwortung wird." },
{ schluessel: "e-team-widerspruch-nach-oben", stufe: "senior",
titel: "Widerspruch kommt auch mir gegenüber",
text: "Ausdrücklich so formuliert: Ein Team, das dem Sprecher nicht "
+ "widerspricht, hat aufgehört zu denken. Hier steht niemand über dem anderen." },
],
/* ---- Block 4: Wie viel von selbst ---------------------------------- */
selbst: [
{ schluessel: "e-selbst-ohne-erinnerung", stufe: "probe",
titel: "Aufgaben werden abgearbeitet, ohne Erinnerung",
text: "Die untere Kante. Alles darüber ist ein Plus, das hier ist die Grundlage." },
{ schluessel: "e-selbst-nicht-in-der-liste", stufe: "standard",
titel: "Etwas wird gesehen und gemacht, was nicht in der Liste stand",
text: "Der Unterschied zwischen Ausführen und Mitdenken." },
{ schluessel: "e-selbst-vorschlag-statt-klage", stufe: "standard",
titel: "Eigene Vorschläge kommen als Vorschlag, nicht als Beschwerde",
text: "Dieselbe Beobachtung, zwei Formen – eine davon ist brauchbar." },
{ schluessel: "e-selbst-abgegeben", stufe: "standard",
titel: "Aufgaben werden abgegeben, statt alles selbst zu machen",
text: "Die häufigste Ursache für Überlastung ist nicht die Menge, "
+ "sondern das Nicht-Abgeben." },
{ schluessel: "e-selbst-bereich-laeuft-allein", stufe: "senior",
titel: "Ein Bereich läuft komplett ohne mich",
text: "Der Punkt, an dem eine Stufe wirklich verdient ist." },
{ schluessel: "e-selbst-nein-gesagt", stufe: "senior",
titel: "Es wird nein gesagt, wenn es zu viel ist",
text: "Steht bewusst ganz oben statt als Mangel: Nein sagen zu können ist "
+ "die schwerste Fähigkeit im Ehrenamt – und sie verhindert genau den "
+ "Ausfall, der sonst kommt." },
],
/* ---- Block 5: Wie geht es dir damit? (nur die Person selbst) ------- */
befinden: [
{ schluessel: "e-ich-menge-passt", stufe: "probe",
titel: "Die Menge passt gerade",
text: "Zu wenig Zeit ist der meistgenannte Grund, warum Moderatoren aufhören." },
{ schluessel: "e-ich-niemand-schwierig", stufe: "probe",
titel: "Es gibt gerade niemanden im Team, mit dem es schwierig ist",
text: "Der zweithäufigste Grund – und der, über den am wenigsten geredet wird." },
{ schluessel: "e-ich-weiss-an-wen", stufe: "probe",
titel: "Ich weiß, an wen ich mich wende, wenn etwas eskaliert",
text: "Ein fehlender Eskalationsweg fühlt sich nicht wie ein fehlender Weg an, "
+ "sondern wie Unsicherheit." },
{ schluessel: "e-ich-jemand-zum-reden", stufe: "probe",
titel: "Nach einer hässlichen Schicht gibt es jemanden zum Reden",
text: "Wer Hässliches wegmoderiert, nimmt es mit. Das geht nicht von allein weg." },
{ schluessel: "e-ich-nein-ohne-problem", stufe: "probe",
titel: "Ich kann nein sagen, ohne dass es ein Problem ist",
text: "Nicht ob du darfst – ob es sich so anfühlt. Das ist der Unterschied." },
{ schluessel: "e-ich-faellt-auf", stufe: "probe",
titel: "Ich habe das Gefühl, dass es auffällt, wenn ich da bin",
text: "Fehlende Anerkennung ist der am häufigsten genannte emotionale Preis – "
+ "und der billigste zu beheben." },
],
};
/* ---------------------------------------------------------------------
ABGELEITET, NICHT ABGESCHRIEBEN
Jede Zahl ueber diesen Katalog wird gerechnet. Eine Ueberschrift
"38 Beobachtungen" waere beim naechsten Punkt still falsch -- und
genau das war sie schon: Der Plan im Vault sagt 38, gezaehlt sind es
34. Deshalb steht nirgends eine Zahl, die jemand pflegen muesste.
------------------------------------------------------------------- */
/** Alle Punkte als flache Liste, mit ihrem Block. */
export const ENTWICKLUNG_ALLE = ENTWICKLUNG_BLOECKE.flatMap((b) =>
(ENTWICKLUNG_PUNKTE[b.schluessel] || []).map((p) => ({ ...p, block: b.schluessel })));
/** Die Punkte, die andere ueber jemanden setzen duerfen -- also alles
* ausser Block 5. Als eigene Liste, damit die Riegel im Server sie
* nicht jedes Mal neu zusammensuchen muessen. */
export const ENTWICKLUNG_FREMD = ENTWICKLUNG_ALLE.filter((p) => {
const b = ENTWICKLUNG_BLOECKE.find((x) => x.schluessel === p.block);
return !b?.nurSelbst;
});
/** Und die, die nur die Person selbst beantwortet. */
export const ENTWICKLUNG_SELBST = ENTWICKLUNG_ALLE.filter((p) => {
const b = ENTWICKLUNG_BLOECKE.find((x) => x.schluessel === p.block);
return !!b?.nurSelbst;
});
/** Schnellzugriff Schluessel -> Punkt. */
export const ENTWICKLUNG_NACH_SCHLUESSEL = new Map(
ENTWICKLUNG_ALLE.map((p) => [p.schluessel, p]));
+225
View File
@@ -0,0 +1,225 @@
/* =====================================================================
Der Katalog „Talente" (11.09.2026)
Zuschauer, die ins Team passen könnten. Die andere Hälfte von
Filipes Auftrag: *„fertige Sachen, so dass DogFather und rechte Hand
es so einfach wie möglich haben."*
Herleitung im Vault (02 Projekte/Entwicklung & Nachwuchs – Plan.md,
Abschnitt 1.3 und 4). Hier steht bei jedem Eintrag, warum er drin
ist — und bei zweien, warum sie es NICHT sind.
---------------------------------------------------------------------
TALENTE IST EIN TRICHTER, KEINE LISTE
Eine Liste wächst und wird nie wieder angesehen. Ein Trichter hat
Stufen, und jede Stufe hat GENAU EINEN nächsten Schritt — nicht drei
Möglichkeiten, zwischen denen man sich entscheiden müsste. Wer eine
Karte öffnet, soll in zwei Sekunden wissen, was jetzt dran ist.
UND DIE STANDZEIT GEHOERT ZUR ANZEIGE. „Seit 41 Tagen beobachtet,
nie angesprochen" ist eine Auskunft über UNS, nicht über den
Kandidaten. Ohne sie verwandelt sich der Trichter in eine Liste
zurück.
---------------------------------------------------------------------
ZWEI DINGE STEHEN AUSDRUECKLICH NICHT IM KATALOG
„SCHREIBT VIEL IM CHAT." Angenehm im Chat zu sein und ein guter
Moderator zu sein sind nachweislich verschiedene Dinge. Die beiden
zu verwechseln ist der häufigste Fehler bei der Modsuche — und weil
die Zahl so leicht zu bekommen wäre, steht hier, dass sie fehlt.
HEIMLICHE TESTS. Kein Zweitkonto, das jemanden auf die Probe stellt.
Das kostet mehr Vertrauen, als es an Erkenntnis bringt — und zwar
auch bei denen, die davon nur hören.
---------------------------------------------------------------------
UND EIN RIEGEL, DER NICHT VERHANDELBAR IST
In diesem Katalog stehen Namen von Menschen, die nichts davon
wissen. Er gehört DogFather und der rechten Hand, sonst niemandem —
kein Modi, keine Agentur, und ganz sicher nicht der Community.
Durchgesetzt wird das in workspace-entwicklung.js und in der
Rechtetafel, nicht hier; hier steht nur, warum.
===================================================================== */
/** Die fünf Stufen des Trichters.
*
* `schritt` ist der EINE nächste Handgriff. `frist` die Zahl der Tage,
* nach denen eine Karte auf dieser Stufe als liegengeblieben gilt --
* sie wird nicht gemahnt, sie wird ANGEZEIGT. Eine Erinnerung, die
* man wegklicken kann, wird weggeklickt; eine Zahl, die dasteht,
* nicht. */
export const TALENT_STUFEN = [
{
schluessel: "aufgefallen", name: "Aufgefallen", rang: 1, frist: 14,
bedeutet: "Ein Name ist gefallen. Mehr nicht.",
schritt: "Beobachten einschalten – ab jetzt Merkmale sammeln.",
knopf: "Beobachten",
},
{
schluessel: "beobachtet", name: "Beobachtet", rang: 2, frist: 30,
bedeutet: "Merkmale werden über Wochen gesammelt.",
schritt: "Ansprechen – der Vorlagentext liegt bereit.",
knopf: "Ansprechen",
},
{
schluessel: "angesprochen", name: "Angesprochen", rang: 3, frist: 7,
bedeutet: "Gefragt, ob überhaupt Interesse besteht.",
schritt: "Antwort eintragen: ja, nein oder später.",
knopf: "Auf Probe nehmen",
},
{
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.",
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.",
knopf: null,
},
];
/** Die Merkmale, in fünf Gruppen. Angeklickt, nicht getippt.
*
* DIE REIHENFOLGE DER GRUPPEN IST NICHT ZUFAELLIG: „Ruhe" steht
* vorn, weil es das am besten belegte Einzelmerkmal ueberhaupt ist --
* und weil die erste Gruppe die ist, die man immer ausfuellt. */
export const TALENT_GRUPPEN = [
{
schluessel: "ruhe", name: "Ruhe",
text: "Das am stärksten belegte Einzelmerkmal. Wer unter Druck ruhig bleibt, "
+ "kann alles andere lernen – umgekehrt nicht.",
},
{
schluessel: "hilft", name: "Hilft von selbst",
text: "Nicht ob jemand helfen WILL, sondern ob er es schon tut, ohne gefragt zu sein.",
},
{
schluessel: "haus", name: "Kennt das Haus",
text: "Regeln, Leute, Witze. Das kann man nicht einarbeiten – das hat man oder nicht.",
},
{
schluessel: "da", name: "Ist da",
text: "Regelmäßigkeit schlägt Menge. Und eine Zeit, zu der ihr niemanden habt, "
+ "schlägt beides.",
},
{
schluessel: "empfohlen", name: "Aus dem Team",
text: "Wer aus dem Team kommt, ist schon einmal durch ein Augenpaar gegangen.",
},
];
export const TALENT_MERKMALE = {
ruhe: [
{ schluessel: "t-ruhe-streit", 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",
text: "Nicht ignorieren – nicht mitgehen. Zwei verschiedene Dinge." },
{ schluessel: "t-ruhe-widerspricht", 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",
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",
text: "Tut die Arbeit schon, ohne den Titel zu haben." },
{ schluessel: "t-hilft-erklaert-neuen", 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",
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",
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",
text: "Und wendet sie im Gespräch an, nicht als Zitat." },
{ schluessel: "t-haus-leute", 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",
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",
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",
text: "Regelmäßigkeit ist planbar, Begeisterung nicht." },
{ schluessel: "t-da-auch-wenig-los", 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",
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",
text: "Pausen sind normal. Wiederkommen ist das Signal." },
],
empfohlen: [
{ schluessel: "t-emp-vom-team", 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",
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",
text: "Die ehrlichste Frage, die man dem Team stellen kann." },
{ schluessel: "t-emp-schon-ausgeholfen", 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",
text: "Schwächer als eine Nennung aus dem Team, aber es zählt." },
],
};
/** Die Warnzeichen. Getrennt und in einem warmen Ton — nicht rot.
*
* WARUM SIE UEBERHAUPT GETRENNT STEHEN: Bei Moderatoren sagt das
* Fehlverhalten mehr voraus als das Koennen. Ein Warnzeichen neben
* zwanzig Haken ginge unter; getrennt steht es da.
*
* UND WARUM NICHT ROT: Keines davon ist ein Ausschluss. Rot heisst
* "nein" -- gemeint ist "hier laenger hinsehen". Ein Ton, der mehr
* sagt als die Sache, macht aus einem Hinweis ein Urteil. */
export const TALENT_WARNZEICHEN = [
{ schluessel: "t-warn-will-die-rolle", titel: "Will die Rolle sehr deutlich",
text: "Der klassischste Warnhinweis überhaupt. Kein Ausschluss – "
+ "aber ein Grund, länger hinzusehen." },
{ schluessel: "t-warn-war-ausfaellig", titel: "War im Chat schon einmal ausfällig",
text: "Muss nicht das letzte Wort sein. Aber es gehört auf die Karte, "
+ "nicht in die Erinnerung von einem." },
{ schluessel: "t-warn-nur-ein-thema", titel: "Nur bei einem Thema da",
text: "Begeisterung für eine Sache, nicht für den Ort. Hält, solange das Thema hält." },
{ schluessel: "t-warn-verschwindet", titel: "Verschwindet für Wochen und kommt wieder",
text: "Schwer zu verplanen – unabhängig davon, wie gut jemand ist." },
{ schluessel: "t-warn-redet-ueber", titel: "Redet über andere, nicht mit ihnen",
text: "Sagt Streit im Team voraus – den zweithäufigsten Ausstiegsgrund." },
{ schluessel: "t-warn-nur-bei-dir", titel: "Nur da, wenn du sendest",
text: "An eine Person gebunden, nicht an die Community. Das hält nicht." },
];
/* ---------------------------------------------------------------------
Abgeleitet, nicht abgeschrieben -- dieselbe Regel wie im anderen
Katalog: keine Zahl, die jemand pflegen muesste.
------------------------------------------------------------------- */
export const TALENT_ALLE = TALENT_GRUPPEN.flatMap((g) =>
(TALENT_MERKMALE[g.schluessel] || []).map((m) => ({ ...m, gruppe: g.schluessel })));
export const TALENT_NACH_SCHLUESSEL = new Map([
...TALENT_ALLE.map((m) => [m.schluessel, { ...m, warnung: false }]),
...TALENT_WARNZEICHEN.map((w) => [w.schluessel, { ...w, warnung: true }]),
]);
/** Die Stufe zu einem Schluessel -- und `null`, wenn es sie nicht gibt.
* Kein Rueckfall auf die erste Stufe: Eine erfundene Stufe soll
* auffallen, nicht stillschweigend zu "Aufgefallen" werden. */
export const talentStufe = (s) =>
TALENT_STUFEN.find((x) => x.schluessel === s) || null;
/** Die naechste Stufe -- oder null am Ende des Trichters. */
export function talentNaechste(s) {
const i = TALENT_STUFEN.findIndex((x) => x.schluessel === s);
if (i < 0 || i + 1 >= TALENT_STUFEN.length) return null;
return TALENT_STUFEN[i + 1];
}
+60 -7
View File
@@ -629,13 +629,18 @@ const FUEHRUNG_GRUPPE = {
const ENTWICKLUNG_KACHEL = {
...FUEHRUNG_GRUPPE,
name: "Entwicklung", unter: "Was gut läuft, woran ihr arbeitet – je Person",
zeichen: "steckbrief", ton: 26, ziel: "bereich.html?b=entwicklung", szene: "studio",
/* SEIT DEM 11.09.2026 FUEHRT DIE KACHEL AUF DEN KATALOG, nicht mehr
auf das Notizbrett. Filipe: "ich will dass fertige aufgaben da
stehen und so". Das Brett gibt es weiter -- es ist von der
Katalogseite aus verlinkt und nimmt auf, was zwischen zwei
Durchgaengen auffaellt. */
zeichen: "steckbrief", ton: 26, ziel: "entwicklung.html", szene: "studio",
};
const TALENTE_KACHEL = {
...FUEHRUNG_GRUPPE,
name: "Talente", unter: "Zuschauer, die ins Team passen könnten",
zeichen: "scouting", ton: 27, ziel: "bereich.html?b=talente", szene: "portal",
zeichen: "scouting", ton: 27, ziel: "talente.html", szene: "portal",
};
const PERSONEN_KACHEL = {
@@ -700,11 +705,29 @@ function vorGruppe(liste, gruppe, ...neue) {
community", 11.09.2026), und sie kommen dort ueber
ZUSATZ_BEREICHE.admin mit eigenem gruppeNach. Zwei Adressen, zwei
Plaetze, beide entschieden. */
/* WIE GEHT'S DIR? -- die einzige Kachel des Hauses, deren Inhalt
niemand ausser der Person selbst je sieht (11.09.2026).
SIE STEHT BEI "FUER DICH" und nicht bei "Taeglich": Es sind sechs
Fragen, die man einmal im Monat beantwortet, nicht jeden Tag. Eine
taegliche Kachel, die man nicht taeglich braucht, wird nach einer
Woche uebersehen.
UND SIE STEHT AUCH BEI DOGFATHER UND DER RECHTEN HAND. Sie sind
Teil des Teams, nicht darueber -- wenn es jemandem zu viel wird, dann
ihnen genauso. Eine Kachel, die nur die anderen ausfuellen, waere
genau die Schieflage, die dieses Haus nicht haben soll. */
const BEFINDEN_KACHEL = {
gruppe: "Für dich", gruppeUnter: "Dein Platz im Team",
name: "Wie geht's dir?", unter: "Sechs Fragen – die Antworten sieht nur du",
zeichen: "steckbrief", ton: 37, ziel: "entwicklung.html", szene: "lounge",
};
const HAND_BEREICHE = vorGruppe(
[...MODI_BEREICHE, EINGANG_KACHEL, PERSONEN_KACHEL],
"Rund ums Live",
ENTWICKLUNG_KACHEL, TALENTE_KACHEL,
);
).concat(BEFINDEN_KACHEL);
/* =====================================================================
@@ -797,7 +820,8 @@ export const TREFF_ROLLEN = new Set(["gast", "modi", "hand", "admin"]);
Zusammengesetzt und nicht in MODI_BEREICHE hineingeschrieben, weil
diese Liste weiter oben steht als die Kacheln des Treffs. Eine Kopie
dort waere die Stelle, an der beim naechsten neuen Brett eines fehlt. */
const MODI_MIT_TREFF = [...MODI_BEREICHE, ...TREFF_BEREICHE, TREFF_MODERATION_KACHEL];
const MODI_MIT_TREFF = [...MODI_BEREICHE, BEFINDEN_KACHEL, ...TREFF_BEREICHE,
TREFF_MODERATION_KACHEL];
const HAND_MIT_TREFF = [...HAND_BEREICHE, ...TREFF_BEREICHE, TREFF_MODERATION_KACHEL];
/* AUSSEN_ROLLEN kommt aus crew-adresse.js und wird hier nur
@@ -861,10 +885,39 @@ const AGENTUR_LISTE = new Set(["spicy", "admin", "manager", "scout", "creator"])
Gebraucht wird sie, weil `bereich.html` seit heute fuer ihn offen ist
-- ohne diese Einschraenkung kaeme er ueber die Adresszeile auch in
die Agentur-Ablage, die ihn nichts angeht. */
export const MODI_BEREICHE_ERLAUBT = new Set(
HAND_MIT_TREFF
/* EINE KACHEL IST NICHT MEHR DER EINZIGE WEG ZU EINEM BRETT
(11.09.2026, gefunden von pruef-entwicklung).
Die Ableitung oben liest die Bretter aus den Kachelzielen. Das war
richtig, solange jede Kachel auf ein Brett zeigte. Seit „Entwicklung"
und „Talente" auf ihre KATALOGSEITEN zeigen, fielen genau diese
beiden Bretter aus der Liste -- und die rechte Hand bekam auf dem
Notizbrett, das die Katalogseite selbst verlinkt, ein 404.
Kein Fehler im Riegel, sondern in seiner QUELLE: Die Frage "welche
Bretter darf diese Rolle oeffnen" laesst sich nicht mehr allein aus
den Kacheln beantworten, weil es jetzt Bretter gibt, die man ueber
eine Seite erreicht statt ueber eine Kachel.
Beides wird deshalb zusammengezogen -- und die zweite Liste ist
ausdruecklich KURZ und begruendet, nicht offen: Hier stehen nur die
Bretter, zu denen eine erlaubte SEITE verlinkt. Wer ein drittes
dazunimmt, ohne es einzutragen, bekommt dasselbe 404 und findet
diesen Absatz. */
const BRETTER_UEBER_SEITEN = [
/* Beide sind von entwicklung.html bzw. talente.html aus verlinkt --
das Notizbrett neben dem Katalog. Wer die Seite oeffnen darf,
muss auch dorthin kommen. */
"entwicklung",
"talente",
];
export const MODI_BEREICHE_ERLAUBT = new Set([
...HAND_MIT_TREFF
.map((k) => (/bereich\.html\?b=([a-z]+)/.exec(k.ziel) || [])[1])
.filter(Boolean));
.filter(Boolean),
...BRETTER_UEBER_SEITEN,
]);
/* Der Satz unter der Begruessung. Die fuenf bekannten Rollen haben ihn
in assets/js/start.js stehen ("Creator · dein eigener Bereich"); fuer
+4 -4
View File
@@ -13,25 +13,25 @@
"theme_color": "#06090f",
"icons": [
{
"src": "/assets/img/app-symbole/workspace-192.png?v=202609111836",
"src": "/assets/img/app-symbole/workspace-192.png?v=202609112217",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/app-symbole/workspace-512.png?v=202609111836",
"src": "/assets/img/app-symbole/workspace-512.png?v=202609112217",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609111836",
"src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609112217",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609111836",
"src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609112217",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
+365
View File
@@ -0,0 +1,365 @@
/* =====================================================================
Entwicklung & Nachwuchs — die zwei Katalogseiten (11.09.2026)
Zwei Seiten, ein Stil: Auf beiden klickt man kurze Aussagen an,
beide zeigen daneben eine Zahl, die etwas über UNS sagt (wie alt die
Einschätzung ist, wie lange eine Karte liegt).
---------------------------------------------------------------------
DIE WICHTIGSTE ENTSCHEIDUNG IST EINE FARBE, DIE NICHT VORKOMMT
Es gibt hier kein Rot. „Da hakt es" ist eine Beobachtung, kein Alarm;
ein Warnzeichen an einem Kandidaten ist ein Grund hinzusehen, kein
Nein. Rot würde in beiden Fällen mehr sagen als die Sache — und aus
einem Gesprächsanlass ein Urteil machen.
Stattdessen: warm (Bernstein) für „hakt" und für Warnzeichen, kühl
für „läuft", babyblau für „wächst", grau für „kann ich nicht sagen".
Vier unterscheidbare Töne, keiner davon laut. Das ist auch die
Hausregel — augenschonend, gedeckt, dunkles Schema.
---------------------------------------------------------------------
UND EINE, DIE MAN NICHT SIEHT: ES GIBT KEINEN BALKEN
Kein Fortschrittsbalken je Person, kein Prozentwert, keine Punktzahl.
Eine Zahl über einem Menschen wird gelesen, verglichen und
weitererzählt. Was hier gebraucht wird, ist ein Gesprächsanlass.
===================================================================== */
/* ---------------------------------------------------------------------
Gemeinsam
------------------------------------------------------------------- */
.e-abschnitt {
margin: 26px 0 12px;
font-size: .82rem;
font-weight: 650;
letter-spacing: .09em;
text-transform: uppercase;
color: var(--text-still);
}
/* ---------------------------------------------------------------------
Die Ampel
Sie steht ganz oben und ist absichtlich die einzige Fläche auf der
Seite, die eine Farbe trägt. Unter 34 Beobachtungen hätte sie
niemand je gelesen.
------------------------------------------------------------------- */
.e-ampel {
margin: 0 0 22px;
padding: 14px 18px;
border: 1px solid var(--rand);
border-left: 3px solid var(--rand-hell);
background: color-mix(in srgb, var(--tinte) 94%, #ffffff 6%);
}
.e-ampel[data-lage="jemand"] { border-left-color: #cc9451; }
.e-ampel[data-lage="mehrere"] { border-left-color: #cc7a51; }
.e-ampel[data-lage="ruhig"] { border-left-color: #668e6e; }
.e-ampel__satz { margin: 0; font-size: .93rem; line-height: 1.6; color: var(--text); }
.e-ampel__zusatz {
margin: 7px 0 0; font-size: .8rem; line-height: 1.55; color: var(--text-still);
}
/* ---------------------------------------------------------------------
Die Personenreihe
------------------------------------------------------------------- */
.e-personen { display: flex; flex-wrap: wrap; gap: 10px; }
.e-person {
display: flex; flex-direction: column; gap: 3px;
min-width: 210px; flex: 1 1 210px;
padding: 13px 15px; text-align: left; cursor: pointer;
font: inherit;
background: color-mix(in srgb, var(--tinte) 94%, #ffffff 6%);
border: 1px solid var(--rand);
transition: border-color .16s ease;
}
.e-person:hover, .e-person:focus-visible {
border-color: color-mix(in srgb, var(--akzent) 45%, transparent);
}
.e-person__name { font-size: .98rem; font-weight: 640; color: var(--text); }
.e-person__rolle { font-size: .78rem; color: var(--text-still); }
.e-person__zahlen { display: flex; flex-wrap: wrap; gap: 5px 10px; margin-top: 6px; }
.e-person__zahl { font-size: .77rem; color: var(--text-leise); }
/* Die Abweichung ist das Ergebnis dieser Seite — sie darf als Einzige
auffallen. */
.e-person__zahl--wichtig { color: var(--text); font-weight: 620; }
.e-person__zahl--alt { color: var(--text-still); }
/* ---------------------------------------------------------------------
Die Karte
------------------------------------------------------------------- */
.e-karte { margin: 26px 0 0; }
.e-karte__kopf {
display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px;
padding-bottom: 10px; margin-bottom: 16px;
border-bottom: 1px solid var(--rand-hell);
}
.e-karte__name { margin: 0; font-size: 1.22rem; font-weight: 660; color: var(--text); }
.e-karte__rolle { font-size: .82rem; color: var(--text-still); }
.e-karte__stand { font-size: .8rem; color: var(--text-leise); margin-left: auto; }
.e-karte__brett { text-decoration: none; }
/* Die Abweichungen. Sie stehen oben und bekommen als Einzige einen
Rahmen, der auffällt — sie sind der Grund, warum zwei Leute getrennt
klicken. */
.e-abweichung {
margin: 0 0 22px; padding: 15px 18px;
border: 1px solid color-mix(in srgb, var(--akzent) 32%, transparent);
background: color-mix(in srgb, var(--akzent) 7%, var(--tinte));
}
.e-abweichung__titel { margin: 0 0 5px; font-size: 1rem; font-weight: 650; color: var(--text); }
.e-abweichung__satz { margin: 0 0 10px; font-size: .85rem; color: var(--text-leise); }
.e-abweichung__liste { margin: 0; padding-left: 1.1em; }
.e-abweichung__liste li { margin-bottom: 6px; }
.e-abweichung__punkt { display: block; font-size: .88rem; color: var(--text); }
.e-abweichung__gegen { display: block; font-size: .8rem; color: var(--text-leise); }
/* ---------------------------------------------------------------------
Ein Block und seine Punkte
------------------------------------------------------------------- */
.e-block { margin: 0 0 26px; }
.e-block__titel { margin: 0 0 4px; font-size: 1.02rem; font-weight: 650; color: var(--text); }
.e-block__satz {
margin: 0 0 12px; max-width: 64ch;
font-size: .85rem; line-height: 1.6; color: var(--text-leise);
}
.e-punkt {
margin: 0 0 9px; padding: 12px 15px;
background: color-mix(in srgb, var(--tinte) 94%, #ffffff 6%);
border: 1px solid var(--rand);
/* Der linke Balken trägt die Antwort. Er ist die einzige Stelle, an
der man beim Überfliegen sieht, wie eine Zeile steht — ohne dass
die ganze Karte einfärbt. */
border-left: 3px solid transparent;
}
.e-punkt[data-stufe="laeuft"] { border-left-color: #668e6e; }
.e-punkt[data-stufe="waechst"] { border-left-color: #8ec9ff; }
.e-punkt[data-stufe="hakt"] { border-left-color: #cc9451; }
.e-punkt[data-stufe="unklar"] { border-left-color: var(--rand-hell); }
/* Verblasst: die Zeile tritt zurück, verschwindet aber nicht. Eine
Akte, die heimlich vergisst, wäre schlimmer als eine, die alles
behält. */
.e-punkt[data-verblasst="ja"] { opacity: .68; }
.e-punkt__kopf { display: flex; flex-wrap: wrap; align-items: baseline; gap: 9px; }
.e-punkt__titel { margin: 0; font-size: .95rem; font-weight: 620; color: var(--text); }
.e-punkt__erwartung {
/* .75rem = 12 px und nicht .7rem = 11,2 px.
pruef-css-klassen zaehlt die Stellen unter 11,5 px gegen eine
Grundlinie und meldet JEDE neue -- hier war es eine. Elf Pixel in
Grossbuchstaben mit Sperrung liest man auf einem Handy nicht mehr,
man erkennt sie hoechstens wieder. Und das hier ist keine Zierde,
sondern die Angabe, ab wann etwas ueberhaupt erwartet wird. */
font-size: .75rem; letter-spacing: .04em; text-transform: uppercase;
padding: 2px 7px; border: 1px solid var(--rand-hell); border-radius: 999px;
color: var(--text-still);
}
/* Die Erwartungsstufe ist KEINE Note — deshalb drei fast gleiche Töne
und kein Ampelverlauf. Sie sagt, ab wann etwas erwartet wird, nicht
wie gut jemand ist. */
.e-punkt__erwartung[data-erwartung="senior"] { color: var(--text-leise); }
.e-punkt__text {
margin: 5px 0 10px; max-width: 66ch;
font-size: .83rem; line-height: 1.6; color: var(--text-leise);
}
.e-wahl { display: flex; flex-wrap: wrap; gap: 6px; }
.e-wahl__knopf {
display: inline-flex; align-items: center; gap: 6px;
font: inherit; font-size: .8rem; font-weight: 600;
padding: 6px 12px; border-radius: 999px; cursor: pointer;
color: var(--text-leise);
background: transparent;
border: 1px solid var(--rand-hell);
transition: background .16s ease, color .16s ease, border-color .16s ease;
}
.e-wahl__zeichen { font-size: .9rem; line-height: 1; }
.e-wahl__knopf:hover:not(:disabled) { color: var(--text); }
.e-wahl__knopf[aria-pressed="true"] { color: var(--text); }
.e-wahl__knopf[data-stufe="laeuft"][aria-pressed="true"] {
background: color-mix(in srgb, #668e6e 22%, transparent);
border-color: color-mix(in srgb, #668e6e 55%, transparent);
}
.e-wahl__knopf[data-stufe="waechst"][aria-pressed="true"] {
background: color-mix(in srgb, #8ec9ff 18%, transparent);
border-color: color-mix(in srgb, #8ec9ff 50%, transparent);
}
.e-wahl__knopf[data-stufe="hakt"][aria-pressed="true"] {
background: color-mix(in srgb, #cc9451 20%, transparent);
border-color: color-mix(in srgb, #cc9451 55%, transparent);
}
.e-wahl__knopf[data-stufe="unklar"][aria-pressed="true"] {
background: rgba(255, 255, 255, .06);
border-color: var(--rand-hell);
}
.e-punkt__anlass {
display: block; width: 100%; margin: 9px 0 0;
font: inherit; font-size: .84rem; line-height: 1.5;
padding: 8px 10px; resize: vertical;
color: var(--text);
background: rgba(0, 0, 0, .28);
border: 1px solid var(--rand);
}
.e-punkt__anlass::placeholder { color: var(--text-still); }
.e-punkt__andere, .e-punkt__verdeckt, .e-punkt__wann {
margin: 8px 0 0; font-size: .78rem; line-height: 1.5; color: var(--text-still);
}
.e-punkt__andere { color: var(--text-leise); }
/* Der verdeckte Stand: sichtbar, dass es ihn gibt — unsichtbar, was er
ist. Ohne diese Zeile sähen „noch niemand" und „du darfst es noch
nicht sehen" gleich aus, und das erste ist eine Auskunft, das zweite
eine Täuschung. */
.e-punkt__verdeckt { font-style: italic; }
/* Die eigenen sechs Fragen. Sie stehen ganz unten und tragen als
Einzige eine eigene Umrandung — sie gehören nicht in dieselbe Liste
wie das, was über andere gesagt wird. */
.e-meins {
margin: 34px 0 0; padding: 20px 22px;
border: 1px solid color-mix(in srgb, var(--akzent) 26%, transparent);
background: color-mix(in srgb, var(--tinte) 94%, #ffffff 6%);
}
.e-meins__satz {
margin: 0 0 16px; max-width: 62ch;
font-size: .88rem; line-height: 1.65; color: var(--text-leise);
}
.e-punkt--meins { background: rgba(0, 0, 0, .2); }
/* ---------------------------------------------------------------------
Talente — der Trichter
------------------------------------------------------------------- */
.t-trichter { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 24px; }
.t-stufe {
display: flex; flex-direction: column; gap: 1px;
flex: 1 1 130px; padding: 11px 14px;
background: color-mix(in srgb, var(--tinte) 94%, #ffffff 6%);
border: 1px solid var(--rand);
/* Der Trichter wird nach hinten leiser — nicht bunter. Fünf Farben
für fünf Stufen wären fünf Bedeutungen, die keine sind. */
border-top: 2px solid color-mix(in srgb, var(--akzent) 40%, transparent);
}
.t-stufe[data-stufe="probe"] { border-top-color: color-mix(in srgb, var(--akzent) 65%, transparent); }
.t-stufe[data-stufe="imteam"] { border-top-color: #668e6e; }
.t-stufe__zahl { font-size: 1.5rem; font-weight: 660; color: var(--text); line-height: 1.1; }
.t-stufe__name { font-size: .79rem; color: var(--text-leise); }
.t-stufe__liegt { font-size: .74rem; color: #cc9451; margin-top: 3px; }
.t-liste { display: block; }
.t-karte {
margin: 0 0 14px; padding: 16px 18px;
background: color-mix(in srgb, var(--tinte) 94%, #ffffff 6%);
border: 1px solid var(--rand);
border-left: 3px solid transparent;
}
.t-karte[data-liegt="ja"] { border-left-color: #cc9451; }
.t-karte__kopf { display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px; }
.t-karte__titel { margin: 0; font-size: 1.05rem; font-weight: 650; color: var(--text); }
.t-karte__stufe {
font-size: .74rem; letter-spacing: .05em; text-transform: uppercase;
padding: 2px 8px; border-radius: 999px;
border: 1px solid color-mix(in srgb, var(--akzent) 40%, transparent);
color: var(--text-leise);
}
.t-karte__zeit { font-size: .78rem; color: var(--text-still); }
.t-karte__warn {
font-size: .76rem; color: #cc9451; margin-left: auto;
}
.t-karte__wo, .t-karte__text {
margin: 8px 0 0; max-width: 66ch;
font-size: .85rem; line-height: 1.6; color: var(--text-leise);
}
.t-karte__wo { color: var(--text-still); font-size: .8rem; }
.t-schritt {
display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
margin: 13px 0 0; padding: 11px 13px;
background: rgba(0, 0, 0, .24);
border: 1px solid var(--rand);
}
.t-schritt__satz { margin: 0; flex: 1 1 220px; font-size: .86rem; color: var(--text); }
.t-schritt__liegt { margin: 0; flex: 1 1 100%; font-size: .79rem; color: #cc9451; }
.t-karte__auf {
margin: 12px 0 0; font: inherit; font-size: .8rem;
padding: 5px 0; cursor: pointer;
background: none; border: 0; border-bottom: 1px solid var(--rand-hell);
color: var(--text-leise);
}
.t-karte__auf:hover { color: var(--text); }
.t-gruppe { margin: 16px 0 0; }
.t-gruppe__titel { margin: 0 0 3px; font-size: .9rem; font-weight: 640; color: var(--text); }
.t-gruppe__satz {
margin: 0 0 8px; max-width: 64ch;
font-size: .8rem; line-height: 1.55; color: var(--text-still);
}
.t-merkmale { display: flex; flex-wrap: wrap; gap: 6px; }
.t-merkmal {
display: inline-flex; align-items: center; gap: 7px;
font: inherit; font-size: .8rem; text-align: left;
padding: 6px 12px; border-radius: 999px; cursor: pointer;
color: var(--text-leise);
background: transparent;
border: 1px solid var(--rand-hell);
transition: background .16s ease, color .16s ease, border-color .16s ease;
}
.t-merkmal__haken { font-size: .85rem; line-height: 1; color: var(--text-still); }
.t-merkmal:hover:not(:disabled) { color: var(--text); }
.t-merkmal[aria-pressed="true"] {
color: var(--text);
background: color-mix(in srgb, var(--akzent) 14%, transparent);
border-color: color-mix(in srgb, var(--akzent) 42%, transparent);
}
.t-merkmal[aria-pressed="true"] .t-merkmal__haken { color: var(--akzent); }
/* Warnzeichen: warm, nicht rot. Keines davon ist ein Nein. */
.t-merkmal[data-warnung="ja"][aria-pressed="true"] {
background: color-mix(in srgb, #cc9451 20%, transparent);
border-color: color-mix(in srgb, #cc9451 55%, transparent);
}
.t-merkmal[data-warnung="ja"][aria-pressed="true"] .t-merkmal__haken { color: #cc9451; }
.t-gruppe--warn .t-gruppe__titel { color: #cc9451; }
.t-leer {
padding: 22px; text-align: center;
border: 1px dashed var(--rand-hell);
color: var(--text-leise);
}
.t-leer p { margin: 0 0 12px; max-width: 54ch; margin-inline: auto; font-size: .88rem; line-height: 1.6; }
.t-leer .schritt { text-decoration: none; }
/* DIE FUSSZEILE BRAUCHT DENSELBEN GRUND WIE ALLES ANDERE.
Sie stand als einziger Text der Seite direkt auf der Bühne — dem
Spicy-Media-Motiv mit Neonlicht und Chilischoten. Lesbar war es
knapp; gemessen war es gar nicht, weil die Kontrastprüfung
Elemente mit Kindern überspringt und hier ein Link darin steht.
Das ist derselbe Befund wie am Nachmittag bei der Rechtetafel, nur
eine Ecke weiter — und der Grund, warum in diesem Haus jede Fläche
mit Text eine deckende Unterlage bekommt statt einer
halbdurchsichtigen. */
.t-fuss {
margin: 18px 0 0; padding: 11px 14px;
font-size: .8rem; line-height: 1.55; color: var(--text-still);
background: color-mix(in srgb, var(--tinte) 94%, #ffffff 6%);
border: 1px solid var(--rand);
}
.t-fuss a { color: var(--text-leise); }
@media (prefers-reduced-motion: reduce) {
.e-person, .e-wahl__knopf, .t-merkmal { transition: none; }
}
@media (max-width: 620px) {
.e-karte__stand { margin-left: 0; }
.t-karte__warn { margin-left: 0; }
.e-punkt__text, .e-block__satz { font-size: .88rem; }
}
+7
View File
@@ -1091,6 +1091,13 @@ body.start > main { position: relative; z-index: 1; }
nächsten (#8e5b77 „Highlights", #2f2f7f „Wer sieht was"). Auch das
liegt über dem schwächsten Paar der vorhandenen Töne (24,9). */
[data-ton="36"] { --ton: #7c2771; } /* Meldungen & Maßnahmen */
/* Der 37., nach derselben Rechnung: #46762e, Abstand 25,6 zum nächsten
Nachbarn — knapp über dem schwächsten Paar der ursprünglichen 34
(24,9). Bei 37 Tönen in einem augenschonenden Ausschnitt (kein Rot,
keine Neonsättigung) ist das die Grenze des Machbaren; wird es enger,
ist die nächste Frage nicht „noch ein Ton", sondern „braucht diese
Kachel wirklich eine eigene Farbe". */
[data-ton="37"] { --ton: #46762e; } /* Wie geht's dir? */
/* Rückfallwert für alles, was einen Ton braucht, aber keine Nummer hat.
Steht bewusst in :where() -- damit zählt die Zeile beim Wettstreit der
+311
View File
@@ -0,0 +1,311 @@
/* =====================================================================
Die Entwicklungskarte (11.09.2026)
Vier Knöpfe je Beobachtung, ein Feld für den Anlass, und oben die
Abweichungen zwischen zwei Augenpaaren. Mehr ist es nicht — und das
ist Absicht: „so einfach wie möglich, aber so professionell wie
möglich" (Filipe).
WAS HIER NICHT PASSIERT: rechnen. Es gibt keine Punktzahl, keinen
Durchschnitt, keine Ampel je Person. Eine Zahl über einem Menschen
wird gelesen, verglichen und weitererzählt — gebraucht wird ein
Gesprächsanlass.
UND KEINE RECHTEPRÜFUNG. Ob jemand Block 5 sehen darf, entscheidet
der Server: Er liefert ihn gar nicht erst aus. Was hier steht, ist
höchstens ein Abschnitt, der leer bleibt.
===================================================================== */
(() => {
const $ = (id) => document.getElementById(id);
const el = (t, k, text) => {
const n = document.createElement(t);
if (k) n.className = k;
if (text !== undefined) n.textContent = text;
return n;
};
const melde = (t) => { const f = $('fehler'); if (f) { f.textContent = t; f.hidden = !t; } };
const datum = (iso) => {
if (!iso) return '';
const d = new Date(iso);
return Number.isNaN(d.getTime()) ? '' : d.toLocaleDateString('de-DE',
{ day: 'numeric', month: 'short', year: 'numeric' });
};
async function hole(pfad, optionen = {}) {
const a = await fetch(pfad, { credentials: 'same-origin', ...optionen });
if (a.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); }
if (a.status === 404) { location.assign('/workspace/start.html'); throw new Error('unbekannt'); }
return a;
}
let stufen = [];
let offen = null; // die gerade geöffnete Person
/* ---- Die vier Knöpfe ------------------------------------------------
Sie stehen immer in derselben Reihenfolge, auch wenn einer gesetzt
ist. Ein Knopfsatz, der sich umsortiert, sobald man ihn benutzt,
ist der sicherste Weg, beim zweiten Mal danebenzutippen. */
function knopfsatz(aktuell, waehle) {
const reihe = el('div', 'e-wahl');
for (const s of stufen) {
const b = el('button', 'e-wahl__knopf');
b.type = 'button';
b.dataset.stufe = s.schluessel;
b.setAttribute('aria-pressed', String(aktuell === s.schluessel));
b.title = s.text;
b.append(el('span', 'e-wahl__zeichen', s.zeichen));
b.append(el('span', 'e-wahl__name', s.name));
b.addEventListener('click', () => waehle(s.schluessel, b));
reihe.append(b);
}
return reihe;
}
/* ---- Die Ampel ------------------------------------------------------ */
async function ampel() {
const ziel = $('ampel');
let a;
try { a = await hole('/workspace/api/entwicklung/ampel'); } catch { return; }
if (!a.ok) return; // für einen Modi gibt es sie nicht
const r = await a.json();
ziel.textContent = '';
ziel.hidden = false;
ziel.dataset.lage = r.sagbar ? r.lage : 'still';
ziel.append(el('p', 'e-ampel__satz', r.sagbar ? r.text : r.grund));
if (r.sagbar) {
ziel.append(el('p', 'e-ampel__zusatz',
`${r.geantwortet} von ${r.team} im Team haben die sechs Fragen schon beantwortet. `
+ 'Namen stehen hier nicht – auch für dich nicht.'));
}
}
/* ---- Die Personen --------------------------------------------------- */
function personKarte(p, lage) {
const k = el('button', 'e-person');
k.type = 'button';
k.append(el('span', 'e-person__name', p.name));
k.append(el('span', 'e-person__rolle', p.rolle_name));
const zahlen = el('span', 'e-person__zahlen');
zahlen.append(el('span', 'e-person__zahl',
`${p.gesetzt} von ${lage.gesamt_fremd} angesehen`));
/* DIE ABWEICHUNG STEHT VORNE UND NICHT IM DETAIL. Sie ist das
Ergebnis dieser Seite: Wo zwei Leute dasselbe sehen, gibt es
nichts zu besprechen. */
if (p.abweichungen) {
const z = el('span', 'e-person__zahl e-person__zahl--wichtig',
`${p.abweichungen} × verschieden gesehen`);
z.title = 'Hier lohnt das Gespräch – nicht dort, wo ihr einig seid.';
zahlen.append(z);
}
if (p.veraltet) {
const z = el('span', 'e-person__zahl e-person__zahl--alt',
`${p.veraltet} älter als ${lage.verblasst_nach} Tage`);
z.title = 'Eine alte Einschätzung ist keine Einschätzung mehr.';
zahlen.append(z);
}
k.append(zahlen);
k.addEventListener('click', () => karte(p.id));
return k;
}
/* ---- Die Karte einer Person ----------------------------------------- */
async function karte(id) {
offen = id;
const ziel = $('karte');
ziel.hidden = false;
ziel.textContent = '';
ziel.append(el('p', 'leise', 'wird geladen …'));
ziel.scrollIntoView({ behavior: 'smooth', block: 'start' });
const a = await hole(`/workspace/api/entwicklung/person/${id}`);
if (!a.ok) { melde('Die Karte ließ sich nicht laden.'); return; }
const r = await a.json();
stufen = r.stufen;
ziel.textContent = '';
const kopf = el('div', 'e-karte__kopf');
kopf.append(el('h2', 'e-karte__name', r.person.name));
kopf.append(el('span', 'e-karte__rolle', r.person.rolle_name));
kopf.append(el('span', 'e-karte__stand', `${r.gesetzt} von ${r.gesamt} angesehen`));
/* Der Weg zum Notizbrett. Der Katalog ist das, was man regelmäßig
durchgeht; die Notiz das, was zwischendurch auffällt. Beides
nebeneinander, nicht statt. */
const brett = el('a', 'schritt e-karte__brett', 'Notizen zu dieser Person');
brett.href = 'bereich.html?b=entwicklung';
kopf.append(brett);
ziel.append(kopf);
/* ---- Abweichungen zuerst ------------------------------------------
Sie stehen oben, weil sie das Einzige sind, was diese Seite
liefert und eine Liste nicht: die Stelle, an der zwei Leute
dieselbe Person verschieden sehen. */
if (r.abweichungen.length) {
const box = el('div', 'e-abweichung');
box.append(el('h3', 'e-abweichung__titel',
`${r.abweichungen.length} × seht ihr das verschieden`));
box.append(el('p', 'e-abweichung__satz',
'Genau hier lohnt das Gespräch. Wo ihr einig seid, gibt es nichts zu besprechen.'));
const liste = el('ul', 'e-abweichung__liste');
for (const ab of r.abweichungen) {
const li = document.createElement('li');
li.append(el('span', 'e-abweichung__punkt', ab.titel));
const nn = (s) => stufen.find((x) => x.schluessel === s)?.name || s;
li.append(el('span', 'e-abweichung__gegen',
`du: ${nn(ab.meins)} · ${ab.von_name}: ${nn(ab.andere)}`));
liste.append(li);
}
box.append(liste);
ziel.append(box);
}
/* ---- Die Blöcke ---------------------------------------------------- */
for (const b of r.bloecke) {
const abschnitt = el('section', 'e-block');
abschnitt.append(el('h3', 'e-block__titel', b.name));
abschnitt.append(el('p', 'e-block__satz', b.text));
for (const pt of b.punkte) abschnitt.append(punktZeile(id, pt, r.erwartung));
ziel.append(abschnitt);
}
}
function punktZeile(personId, pt, erwartung) {
const z = el('article', 'e-punkt');
if (pt.meins) z.dataset.stufe = pt.meins.stufe;
if (pt.meins?.verblasst) z.dataset.verblasst = 'ja';
const kopf = el('div', 'e-punkt__kopf');
kopf.append(el('h4', 'e-punkt__titel', pt.titel));
const erw = erwartung.find((e) => e.schluessel === pt.stufe);
if (erw) {
const m = el('span', 'e-punkt__erwartung', erw.name);
m.dataset.erwartung = pt.stufe;
m.title = erw.text;
kopf.append(m);
}
z.append(kopf);
z.append(el('p', 'e-punkt__text', pt.text));
/* Das Anlassfeld. Es steht IMMER da, nicht erst nach dem Klick:
Ein Feld, das aufklappt, kostet einen zweiten Blick, und genau
an der Stelle hört man auf zu schreiben. */
const feld = el('textarea', 'e-punkt__anlass');
feld.rows = 2;
feld.value = pt.meins?.anlass || '';
feld.placeholder = 'Woran hast du das gemerkt? (bei „Da hakt es" nötig)';
z.append(knopfsatz(pt.meins?.stufe || null, async (stufe, knopf) => {
const reihe = knopf.parentElement;
for (const b of reihe.querySelectorAll('.e-wahl__knopf')) {
b.setAttribute('aria-pressed', String(b === knopf));
}
z.dataset.stufe = stufe;
const a = await hole(`/workspace/api/entwicklung/person/${personId}/${pt.schluessel}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ stufe, anlass: feld.value }),
});
if (!a.ok) {
const j = await a.json().catch(() => ({}));
melde(j.fehler || 'Ging nicht.');
feld.focus();
return;
}
melde('');
/* Neu laden, sobald der erste Klick auf einem Punkt sitzt: Erst
danach darf der Stand des anderen überhaupt ausgeliefert
werden, und dann soll er auch sichtbar werden. */
if (!pt.meins) await karte(personId);
}));
z.append(feld);
if (pt.andere?.length) {
for (const a of pt.andere) {
const nn = stufen.find((x) => x.schluessel === a.stufe);
const zeile = el('p', 'e-punkt__andere',
`${a.von_name}: ${nn?.name || a.stufe}${a.anlass ? ' – ' + a.anlass : ''}`);
zeile.dataset.stufe = a.stufe;
z.append(zeile);
}
} else if (pt.andere_offen) {
/* WARUM DA ETWAS STEHT UND NICHT NICHTS: „Noch niemand" und „du
darfst es noch nicht sehen" sähen sonst gleich aus — und das
erste ist eine Auskunft, das zweite eine Täuschung. */
z.append(el('p', 'e-punkt__verdeckt',
`${pt.andere_offen} andere Einschätzung liegt vor – sichtbar, sobald du selbst gesetzt hast.`));
}
if (pt.meins) {
z.append(el('p', 'e-punkt__wann',
`von dir am ${datum(pt.meins.am)}${pt.meins.verblasst ? ' – älter als 90 Tage' : ''}`));
}
return z;
}
/* ---- Die eigenen sechs Fragen ---------------------------------------- */
async function meins() {
const a = await hole('/workspace/api/entwicklung/mich');
if (!a.ok) return;
const r = await a.json();
stufen = stufen.length ? stufen : r.stufen;
$('meins-block').hidden = false;
$('meins-titel').textContent = r.block.name;
$('meins-satz').textContent = r.block.text;
const ziel = $('meins');
ziel.textContent = '';
ziel.setAttribute('aria-busy', 'false');
for (const pt of r.punkte) {
const z = el('article', 'e-punkt e-punkt--meins');
if (pt.meins) z.dataset.stufe = pt.meins.stufe;
z.append(el('h4', 'e-punkt__titel', pt.titel));
z.append(el('p', 'e-punkt__text', pt.text));
z.append(knopfsatz(pt.meins?.stufe || null, async (stufe, knopf) => {
for (const b of knopf.parentElement.querySelectorAll('.e-wahl__knopf')) {
b.setAttribute('aria-pressed', String(b === knopf));
}
z.dataset.stufe = stufe;
const s = await hole(`/workspace/api/entwicklung/mich/${pt.schluessel}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ stufe }),
});
if (!s.ok) melde('Ging nicht.');
}));
ziel.append(z);
}
}
/* ---- Start ------------------------------------------------------------ */
(async () => {
try {
const a = await hole('/workspace/api/entwicklung/lage');
if (!a.ok) { melde('Ging nicht.'); return; }
const lage = await a.json();
stufen = lage.stufen;
if (lage.leitung) {
$('personen-block').hidden = false;
const ziel = $('personen');
ziel.textContent = '';
ziel.setAttribute('aria-busy', 'false');
if (!lage.personen.length) {
ziel.append(el('p', 'leise',
'Noch niemand im Team. Zugänge legst du in „Personen & Zugänge" an.'));
}
for (const p of lage.personen) ziel.append(personKarte(p, lage));
await ampel();
} else {
/* Für einen Modi ist DIESE Seite die sechs Fragen. Der Titel
sagt das auch — sonst steht über sechs Fragen das Wort
„Entwicklung", und das liest sich wie eine Beurteilung. */
$('titel').textContent = "Wie geht's dir?";
$('unterzeile').textContent =
'Sechs Fragen über dich. Niemand aus dem Team sieht deine Antworten.';
}
await meins();
} catch { /* umgeleitet */ }
})();
})();
+214
View File
@@ -0,0 +1,214 @@
/* =====================================================================
Talente — der Trichter (11.09.2026)
Fünf Stufen, und jede hat GENAU EINEN nächsten Schritt. Das ist der
ganze Unterschied zu einer Liste: Eine Liste wächst und wird nie
wieder angesehen; ein Trichter fragt bei jeder Karte „und jetzt?".
DREI DINGE, DIE DIESE SEITE ABSICHTLICH TUT:
1. SIE ZEIGT DIE STANDZEIT. „Seit 41 Tagen beobachtet, nie
angesprochen" ist eine Auskunft über UNS, nicht über den
Kandidaten. Ohne sie wird der Trichter wieder zur Liste.
2. SIE TRENNT WARNZEICHEN VON MERKMALEN. Bei Moderatoren sagt das
Fehlverhalten mehr voraus als das Können — ein Warnzeichen neben
zwanzig Haken ginge unter.
3. SIE RECHNET NICHT. Keine Punktzahl, keine Eignung in Prozent.
Zwanzig Haken und ein Warnzeichen ergeben keine Zahl, sondern ein
Gespräch.
===================================================================== */
(() => {
const $ = (id) => document.getElementById(id);
const el = (t, k, text) => {
const n = document.createElement(t);
if (k) n.className = k;
if (text !== undefined) n.textContent = text;
return n;
};
const melde = (t) => { const f = $('fehler'); if (f) { f.textContent = t; f.hidden = !t; } };
async function hole(pfad, optionen = {}) {
const a = await fetch(pfad, { credentials: 'same-origin', ...optionen });
if (a.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); }
if (a.status === 404) { location.assign('/workspace/start.html'); throw new Error('unbekannt'); }
return a;
}
let lage = null;
const offen = new Set(); // welche Karten aufgeklappt sind
/* ---- Der Trichter oben ---------------------------------------------- */
function trichter() {
const ziel = $('trichter');
ziel.textContent = '';
ziel.setAttribute('aria-busy', 'false');
for (const s of lage.trichter) {
const k = el('div', 't-stufe');
k.dataset.stufe = s.schluessel;
k.append(el('span', 't-stufe__zahl', String(s.anzahl)));
k.append(el('span', 't-stufe__name', s.name));
if (s.liegt) {
/* LIEGENGEBLIEBEN IST UNSERE ZAHL, nicht die des Kandidaten.
Sie steht deshalb am Trichter und nicht als Mahnung. */
const z = el('span', 't-stufe__liegt', `${s.liegt} liegt`);
z.title = 'So lange, dass es eine Auskunft über uns ist – nicht über sie.';
k.append(z);
}
ziel.append(k);
}
}
/* ---- Eine Karte ------------------------------------------------------ */
function karte(k) {
const a = el('article', 't-karte');
a.dataset.stufe = k.stufe;
if (k.liegt) a.dataset.liegt = 'ja';
const kopf = el('div', 't-karte__kopf');
kopf.append(el('h2', 't-karte__titel', k.titel));
kopf.append(el('span', 't-karte__stufe', k.stufe_name));
kopf.append(el('span', 't-karte__zeit',
k.tage === null ? '' : `seit ${k.tage} ${k.tage === 1 ? 'Tag' : 'Tagen'}`));
if (k.warnzeichen.length) {
const w = el('span', 't-karte__warn', `${k.warnzeichen.length} × hinsehen`);
w.title = 'Kein Ausschluss – ein Grund, länger hinzusehen.';
kopf.append(w);
}
a.append(kopf);
if (k.einsatz) a.append(el('p', 't-karte__wo', k.einsatz));
if (k.text) a.append(el('p', 't-karte__text', k.text));
/* DER EINE NÄCHSTE SCHRITT. Nicht drei Möglichkeiten — einer.
Wer eine Karte öffnet, soll in zwei Sekunden wissen, was dran
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);
b.type = 'button';
b.addEventListener('click', async () => {
b.disabled = true;
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; }
offen.add(k.id);
await laden();
});
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.'));
}
a.append(schritt);
/* ---- Merkmale, aufklappbar ------------------------------------------
Zugeklappt steht dort eine Zahl, aufgeklappt die 21 Merkmale.
Immer offen wären es bei zehn Kandidaten 270 Kästchen auf einer
Seite — und dann klickt niemand mehr eines an. */
const auf = offen.has(k.id);
const schalter = el('button', 't-karte__auf',
auf ? 'Merkmale zuklappen'
: `Merkmale ansehen (${k.merkmale.length} gesetzt${k.warnzeichen.length
? `, ${k.warnzeichen.length} Warnzeichen` : ''})`);
schalter.type = 'button';
schalter.setAttribute('aria-expanded', String(auf));
schalter.addEventListener('click', () => {
if (offen.has(k.id)) offen.delete(k.id); else offen.add(k.id);
zeichne();
});
a.append(schalter);
if (auf) {
for (const g of lage.gruppen) {
const block = el('section', 't-gruppe');
block.append(el('h3', 't-gruppe__titel', g.name));
block.append(el('p', 't-gruppe__satz', g.text));
const reihe = el('div', 't-merkmale');
for (const m of g.merkmale) reihe.append(merkmal(k, m, false));
block.append(reihe);
a.append(block);
}
const warn = el('section', 't-gruppe t-gruppe--warn');
warn.append(el('h3', 't-gruppe__titel', 'Hinsehen'));
warn.append(el('p', 't-gruppe__satz',
'Keines davon ist ein Ausschluss. Bei Moderatoren sagt das Verhalten, '
+ 'das stört, mehr voraus als das, was jemand kann.'));
const reihe = el('div', 't-merkmale');
for (const m of lage.warnzeichen) reihe.append(merkmal(k, m, true));
warn.append(reihe);
a.append(warn);
}
return a;
}
function merkmal(k, m, istWarnung) {
const gesetzt = (istWarnung ? k.warnzeichen : k.merkmale).includes(m.schluessel);
const b = el('button', 't-merkmal');
b.type = 'button';
if (istWarnung) b.dataset.warnung = 'ja';
b.setAttribute('aria-pressed', String(gesetzt));
b.title = m.text;
b.append(el('span', 't-merkmal__haken', gesetzt ? '✓' : '+'));
b.append(el('span', 't-merkmal__titel', m.titel));
b.addEventListener('click', async () => {
b.disabled = true;
const r = await hole(`/workspace/api/talente/${k.id}/merkmal/${m.schluessel}`,
{ method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: '{}' });
b.disabled = false;
if (!r.ok) { melde('Ging nicht.'); return; }
await laden();
});
return b;
}
/* ---- Zeichnen --------------------------------------------------------- */
function zeichne() {
trichter();
const ziel = $('liste');
ziel.textContent = '';
ziel.setAttribute('aria-busy', 'false');
if (!lage.kandidaten.length) {
const leer = el('div', 't-leer');
leer.append(el('p', null,
'Noch niemand auf der Liste. Ein Talent entsteht als Eintrag im Brett '
+ '„Talente" – dort schreibst du auf, wem du etwas zutraust, und hier '
+ 'wird daraus ein Weg.'));
const a = el('a', 'schritt', 'Zum Brett „Talente"');
a.href = 'bereich.html?b=talente';
leer.append(a);
ziel.append(leer);
return;
}
/* LIEGENGEBLIEBENE ZUERST. Die Reihenfolge ist die eigentliche
Arbeitsanweisung dieser Seite: Was liegt, ist das, was dran ist. */
const sortiert = [...lage.kandidaten].sort((a, b) =>
(b.liegt ? 1 : 0) - (a.liegt ? 1 : 0) || (b.tage || 0) - (a.tage || 0));
for (const k of sortiert) ziel.append(karte(k));
const fuss = el('p', 't-fuss');
fuss.append('Ein neues Talent legst du im Brett an – ');
const a = el('a', null, 'Talente-Brett öffnen');
a.href = 'bereich.html?b=talente';
fuss.append(a);
fuss.append('. Von dort landet es hier automatisch auf „Aufgefallen".');
ziel.append(fuss);
}
async function laden() {
const a = await hole('/workspace/api/talente/lage');
if (!a.ok) { melde('Ging nicht.'); return; }
lage = await a.json();
zeichne();
}
(async () => { try { await laden(); } catch { /* umgeleitet */ } })();
})();
+12 -12
View File
@@ -12,19 +12,19 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111836" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111836" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/start.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/module.css?v=202609111836" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609112217" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609112217" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/start.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/module.css?v=202609112217" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609112217" />
</head>
<body class="start">
@@ -252,10 +252,10 @@
</form>
</dialog>
<script src="assets/js/wahl.js?v=202609111836" defer></script>
<script src="assets/js/bereiche.js?v=202609111836" defer></script>
<script src="assets/js/kopf.js?v=202609111836" defer></script>
<script src="assets/js/glocke.js?v=202609111836" defer></script>
<script src="assets/js/aufgaben.js?v=202609111836" defer></script>
<script src="assets/js/wahl.js?v=202609112217" defer></script>
<script src="assets/js/bereiche.js?v=202609112217" defer></script>
<script src="assets/js/kopf.js?v=202609112217" defer></script>
<script src="assets/js/glocke.js?v=202609112217" defer></script>
<script src="assets/js/aufgaben.js?v=202609112217" defer></script>
</body>
</html>
+13 -13
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111836" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111836" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/start.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/automation.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/module.css?v=202609111836" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609112217" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609112217" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/start.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/automation.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/module.css?v=202609112217" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609112217" />
</head>
<body class="start">
@@ -147,10 +147,10 @@
</main>
<script src="assets/js/wahl.js?v=202609111836" defer></script>
<script src="assets/js/bereiche.js?v=202609111836" defer></script>
<script src="assets/js/kopf.js?v=202609111836" defer></script>
<script src="assets/js/glocke.js?v=202609111836" defer></script>
<script src="assets/js/automation.js?v=202609111836" defer></script>
<script src="assets/js/wahl.js?v=202609112217" defer></script>
<script src="assets/js/bereiche.js?v=202609112217" defer></script>
<script src="assets/js/kopf.js?v=202609112217" defer></script>
<script src="assets/js/glocke.js?v=202609112217" defer></script>
<script src="assets/js/automation.js?v=202609112217" defer></script>
</body>
</html>
+17 -17
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111836" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111836" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/start.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/module.css?v=202609111836" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609112217" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609112217" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/start.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/module.css?v=202609112217" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609112217" />
</head>
<body class="start">
@@ -181,14 +181,14 @@
</main>
<script src="assets/js/wahl.js?v=202609111836" defer></script>
<script src="assets/js/bereiche.js?v=202609111836" defer></script>
<script src="assets/js/kopf.js?v=202609111836" defer></script>
<script src="assets/js/glocke.js?v=202609111836" defer></script>
<script src="assets/js/checkliste.js?v=202609111836" defer></script>
<script src="assets/js/ampel.js?v=202609111836" defer></script>
<script src="assets/js/schulung.js?v=202609111836" defer></script>
<script src="assets/js/vorlagen.js?v=202609111836" defer></script>
<script src="assets/js/bereich.js?v=202609111836" defer></script>
<script src="assets/js/wahl.js?v=202609112217" defer></script>
<script src="assets/js/bereiche.js?v=202609112217" defer></script>
<script src="assets/js/kopf.js?v=202609112217" defer></script>
<script src="assets/js/glocke.js?v=202609112217" defer></script>
<script src="assets/js/checkliste.js?v=202609112217" defer></script>
<script src="assets/js/ampel.js?v=202609112217" defer></script>
<script src="assets/js/schulung.js?v=202609112217" defer></script>
<script src="assets/js/vorlagen.js?v=202609112217" defer></script>
<script src="assets/js/bereich.js?v=202609112217" defer></script>
</body>
</html>
+14 -14
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111836" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111836" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/start.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/calls.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/module.css?v=202609111836" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609112217" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609112217" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/start.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/calls.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/module.css?v=202609112217" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609112217" />
</head>
<body class="start">
@@ -89,11 +89,11 @@
</main>
<script src="assets/js/ki.js?v=202609111836" defer></script>
<script src="assets/js/wahl.js?v=202609111836" defer></script>
<script src="assets/js/bereiche.js?v=202609111836" defer></script>
<script src="assets/js/kopf.js?v=202609111836" defer></script>
<script src="assets/js/glocke.js?v=202609111836" defer></script>
<script src="assets/js/calls.js?v=202609111836" defer></script>
<script src="assets/js/ki.js?v=202609112217" defer></script>
<script src="assets/js/wahl.js?v=202609112217" defer></script>
<script src="assets/js/bereiche.js?v=202609112217" defer></script>
<script src="assets/js/kopf.js?v=202609112217" defer></script>
<script src="assets/js/glocke.js?v=202609112217" defer></script>
<script src="assets/js/calls.js?v=202609112217" defer></script>
</body>
</html>
+13 -13
View File
@@ -12,26 +12,26 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111836" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111836" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/start.css?v=202609111836" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609112217" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609112217" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/start.css?v=202609112217" />
<!-- aufgaben.css bringt die Dialog- und Formularstile mit (.dialog,
.neu__zeile, .dialog__knoepfe). Sie stehen dort seit dem
Aufgabenbrett; sie hierher zu kopieren hiesse, sie an zwei Stellen
zu pflegen. Dieselbe Lösung nutzen kalender.html und content.html.
Gemeldet von pruef-css-klassen.mjs, bevor jemand einen ungestylten
Dialog zu sehen bekam. -->
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/chat.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/module.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/chat.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/module.css?v=202609112217" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609112217" />
</head>
<body class="start">
@@ -341,10 +341,10 @@
</form>
</dialog>
<script src="assets/js/bereiche.js?v=202609111836"></script>
<script src="assets/js/wahl.js?v=202609111836" defer></script>
<script src="assets/js/kopf.js?v=202609111836"></script>
<script src="assets/js/glocke.js?v=202609111836"></script>
<script src="assets/js/chat.js?v=202609111836"></script>
<script src="assets/js/bereiche.js?v=202609112217"></script>
<script src="assets/js/wahl.js?v=202609112217" defer></script>
<script src="assets/js/kopf.js?v=202609112217"></script>
<script src="assets/js/glocke.js?v=202609112217"></script>
<script src="assets/js/chat.js?v=202609112217"></script>
</body>
</html>
+15 -15
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111836" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111836" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/start.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/content.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/module.css?v=202609111836" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609112217" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609112217" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/start.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/content.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/module.css?v=202609112217" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609112217" />
</head>
<body class="start">
@@ -179,12 +179,12 @@
</main>
<script src="assets/js/wahl.js?v=202609111836" defer></script>
<script src="assets/js/bereiche.js?v=202609111836" defer></script>
<script src="assets/js/kopf.js?v=202609111836" defer></script>
<script src="assets/js/glocke.js?v=202609111836" defer></script>
<script src="assets/js/checkliste.js?v=202609111836" defer></script>
<script src="assets/js/vorlagen.js?v=202609111836" defer></script>
<script src="assets/js/content.js?v=202609111836" defer></script>
<script src="assets/js/wahl.js?v=202609112217" defer></script>
<script src="assets/js/bereiche.js?v=202609112217" defer></script>
<script src="assets/js/kopf.js?v=202609112217" defer></script>
<script src="assets/js/glocke.js?v=202609112217" defer></script>
<script src="assets/js/checkliste.js?v=202609112217" defer></script>
<script src="assets/js/vorlagen.js?v=202609112217" defer></script>
<script src="assets/js/content.js?v=202609112217" defer></script>
</body>
</html>
+5 -5
View File
@@ -36,15 +36,15 @@
<link rel="preload" as="image" href="assets/img/buehne/crew-gate-hoch-580.webp"
type="image/webp" media="(max-width: 899px)" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/crew-32.png?v=202609111836" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/crew-180.png?v=202609111836" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609111836" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/crew-32.png?v=202609112217" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/crew-180.png?v=202609112217" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609112217" />
<!-- Diese Wand steht NUR auf der Adresse von Team Dogi (ueberall sonst
404, siehe crewWeiche). Sie darf das Haus deshalb direkt nennen
statt ueber den Platzhalter haus.css zu gehen -- und sie MUSS es:
Eine Zugangswand, die sich erst nach dem Umbiegen faerbt, waere
die eine Seite, auf der man das Umfaerben sieht. -->
<link rel="stylesheet" href="assets/css/crew-haus.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/crew-haus.css?v=202609112217" />
</head>
<body class="gate gate--crew">
@@ -319,6 +319,6 @@
</main>
</div>
<script src="assets/js/gate.js?v=202609111836" defer></script>
<script src="assets/js/gate.js?v=202609112217" defer></script>
</body>
</html>
+4 -4
View File
@@ -13,25 +13,25 @@
"theme_color": "#06090f",
"icons": [
{
"src": "/assets/img/app-symbole/crew-192.png?v=202609111836",
"src": "/assets/img/app-symbole/crew-192.png?v=202609112217",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/app-symbole/crew-512.png?v=202609111836",
"src": "/assets/img/app-symbole/crew-512.png?v=202609112217",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609111836",
"src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609112217",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609111836",
"src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609112217",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
+13 -13
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111836" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111836" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/start.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/dateien.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/module.css?v=202609111836" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609112217" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609112217" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/start.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/dateien.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/module.css?v=202609112217" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609112217" />
</head>
<body class="start">
@@ -108,10 +108,10 @@
</main>
<script src="assets/js/wahl.js?v=202609111836" defer></script>
<script src="assets/js/bereiche.js?v=202609111836" defer></script>
<script src="assets/js/kopf.js?v=202609111836" defer></script>
<script src="assets/js/glocke.js?v=202609111836" defer></script>
<script src="assets/js/dateien.js?v=202609111836" defer></script>
<script src="assets/js/wahl.js?v=202609112217" defer></script>
<script src="assets/js/bereiche.js?v=202609112217" defer></script>
<script src="assets/js/kopf.js?v=202609112217" defer></script>
<script src="assets/js/glocke.js?v=202609112217" defer></script>
<script src="assets/js/dateien.js?v=202609112217" defer></script>
</body>
</html>
+87
View File
@@ -0,0 +1,87 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Entwicklung · Spicy &amp; Dogi</title>
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609112217" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609112217" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/start.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/module.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609112217" />
</head>
<body class="start">
<div class="schleier" aria-hidden="true"></div>
<header class="kopfleiste">
<button type="button" class="zurueck-knopf" id="zurueck" hidden>
<svg class="zurueck-knopf__pfeil" viewBox="0 0 24 24" aria-hidden="true">
<path d="M14.5 5.5 8 12l6.5 6.5" />
</svg>
<span class="zurueck-knopf__text">Zurück</span>
</button>
<p class="marke"><span class="marke__text"><a class="zurueck" href="start.html">Team&nbsp;Dogi</a> · Entwicklung</span></p>
<div class="kopfleiste__rechts">
<span class="wer" id="wer">…</span>
<button type="button" class="abmelden" id="abmelden">Abmelden</button>
</div>
</header>
<main class="inhalt inhalt--breit">
<section class="kopf-zeile">
<div>
<p class="marke" id="ober">Entwicklung &amp; Nachwuchs</p>
<h1 class="titel" id="titel">Entwicklung</h1>
<p class="unterzeile" id="unterzeile">
Anklicken statt tippen. Keine Noten, keine Punktzahl – ein Gesprächsanlass.
</p>
</div>
</section>
<p class="fehler" id="fehler" role="alert" aria-live="polite"></p>
<!-- ==================================================================
DIE REIHENFOLGE AUF DIESER SEITE IST DAS EIGENTLICHE DESIGN
1. Die Ampel — sie betrifft Menschen, nicht Prozesse.
2. Die Personen — wen gehe ich heute durch.
3. Die Karte — und darin die ABWEICHUNGEN zuerst.
4. Ganz unten: die eigenen sechs Fragen.
Wer die Seite öffnet, soll die wichtigste Sache zuerst sehen, und
das ist nicht die Liste. Eine Ampel unter 34 Beobachtungen hätte
niemand je gelesen.
============================================================== -->
<section id="ampel" class="e-ampel" hidden></section>
<section id="personen-block" hidden>
<h2 class="e-abschnitt">Wen gehst du durch?</h2>
<div id="personen" class="e-personen" aria-busy="true"></div>
</section>
<section id="karte" class="e-karte" hidden></section>
<section id="meins-block" class="e-meins" hidden>
<h2 class="e-abschnitt" id="meins-titel">Wie geht es dir damit?</h2>
<p class="e-meins__satz" id="meins-satz">…</p>
<div id="meins" aria-busy="true"></div>
</section>
</main>
<script src="assets/js/wahl.js?v=202609112217" defer></script>
<script src="assets/js/bereiche.js?v=202609112217" defer></script>
<script src="assets/js/kopf.js?v=202609112217" defer></script>
<script src="assets/js/entwicklung.js?v=202609112217" defer></script>
</body>
</html>
+4 -4
View File
@@ -30,9 +30,9 @@
<link rel="preload" as="image" href="assets/img/buehne/gate-hoch-580.webp"
type="image/webp" media="(max-width: 899px)" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111836" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111836" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609111836" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609112217" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609112217" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609112217" />
</head>
<body class="gate">
@@ -401,6 +401,6 @@
</main>
</div>
<script src="assets/js/gate.js?v=202609111836" defer></script>
<script src="assets/js/gate.js?v=202609112217" defer></script>
</body>
</html>
+14 -14
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111836" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111836" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/start.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/kalender.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/module.css?v=202609111836" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609112217" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609112217" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/start.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/kalender.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/module.css?v=202609112217" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609112217" />
</head>
<body class="start">
@@ -377,11 +377,11 @@
</main>
<script src="assets/js/wahl.js?v=202609111836" defer></script>
<script src="assets/js/bereiche.js?v=202609111836" defer></script>
<script src="assets/js/kopf.js?v=202609111836" defer></script>
<script src="assets/js/glocke.js?v=202609111836" defer></script>
<script src="assets/js/kalender.js?v=202609111836" defer></script>
<script src="assets/js/kalender-abo.js?v=202609111836" defer></script>
<script src="assets/js/wahl.js?v=202609112217" defer></script>
<script src="assets/js/bereiche.js?v=202609112217" defer></script>
<script src="assets/js/kopf.js?v=202609112217" defer></script>
<script src="assets/js/glocke.js?v=202609112217" defer></script>
<script src="assets/js/kalender.js?v=202609112217" defer></script>
<script src="assets/js/kalender-abo.js?v=202609112217" defer></script>
</body>
</html>
+13 -13
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111836" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111836" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/start.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/leistung.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/module.css?v=202609111836" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609112217" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609112217" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/start.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/leistung.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/module.css?v=202609112217" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609112217" />
</head>
<body class="start">
@@ -382,10 +382,10 @@
</form>
</dialog>
<script src="assets/js/wahl.js?v=202609111836"></script>
<script src="assets/js/bereiche.js?v=202609111836"></script>
<script src="assets/js/kopf.js?v=202609111836"></script>
<script src="assets/js/glocke.js?v=202609111836"></script>
<script src="assets/js/leistung.js?v=202609111836"></script>
<script src="assets/js/wahl.js?v=202609112217"></script>
<script src="assets/js/bereiche.js?v=202609112217"></script>
<script src="assets/js/kopf.js?v=202609112217"></script>
<script src="assets/js/glocke.js?v=202609112217"></script>
<script src="assets/js/leistung.js?v=202609112217"></script>
</body>
</html>
+13 -13
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111836" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111836" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/start.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/personen.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/module.css?v=202609111836" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609112217" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609112217" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/start.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/personen.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/module.css?v=202609112217" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609112217" />
</head>
<body class="start">
@@ -423,10 +423,10 @@
</main>
<script src="assets/js/wahl.js?v=202609111836" defer></script>
<script src="assets/js/bereiche.js?v=202609111836" defer></script>
<script src="assets/js/kopf.js?v=202609111836" defer></script>
<script src="assets/js/glocke.js?v=202609111836" defer></script>
<script src="assets/js/personen.js?v=202609111836" defer></script>
<script src="assets/js/wahl.js?v=202609112217" defer></script>
<script src="assets/js/bereiche.js?v=202609112217" defer></script>
<script src="assets/js/kopf.js?v=202609112217" defer></script>
<script src="assets/js/glocke.js?v=202609112217" defer></script>
<script src="assets/js/personen.js?v=202609112217" defer></script>
</body>
</html>
+14 -14
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111836" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111836" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/start.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/module.css?v=202609111836" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609112217" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609112217" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/start.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/module.css?v=202609112217" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609112217" />
</head>
<body class="start">
@@ -214,11 +214,11 @@
</main>
<script src="assets/js/wahl.js?v=202609111836" defer></script>
<script src="assets/js/bereiche.js?v=202609111836" defer></script>
<script src="assets/js/kopf.js?v=202609111836" defer></script>
<script src="assets/js/glocke.js?v=202609111836" defer></script>
<script src="assets/js/steckbrief.js?v=202609111836" defer></script>
<script src="assets/js/profil.js?v=202609111836" defer></script>
<script src="assets/js/wahl.js?v=202609112217" defer></script>
<script src="assets/js/bereiche.js?v=202609112217" defer></script>
<script src="assets/js/kopf.js?v=202609112217" defer></script>
<script src="assets/js/glocke.js?v=202609112217" defer></script>
<script src="assets/js/steckbrief.js?v=202609112217" defer></script>
<script src="assets/js/profil.js?v=202609112217" defer></script>
</body>
</html>
+12 -12
View File
@@ -7,14 +7,14 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111836" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111836" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/start.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/module.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609111836" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609112217" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609112217" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/start.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/module.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609112217" />
</head>
<body class="start">
@@ -117,9 +117,9 @@
Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen
Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen,
die seit dem 07.09. jede Seite danach absucht. -->
<script src="assets/js/wahl.js?v=202609111836" defer></script>
<script src="assets/js/bereiche.js?v=202609111836" defer></script>
<script src="assets/js/kopf.js?v=202609111836" defer></script>
<script src="assets/js/rechte.js?v=202609111836" defer></script>
<script src="assets/js/wahl.js?v=202609112217" defer></script>
<script src="assets/js/bereiche.js?v=202609112217" defer></script>
<script src="assets/js/kopf.js?v=202609112217" defer></script>
<script src="assets/js/rechte.js?v=202609112217" defer></script>
</body>
</html>
+14 -14
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111836" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111836" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/start.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/report.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/module.css?v=202609111836" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609112217" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609112217" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/start.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/report.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/module.css?v=202609112217" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609112217" />
</head>
<body class="start">
@@ -106,11 +106,11 @@
</main>
<script src="assets/js/ki.js?v=202609111836" defer></script>
<script src="assets/js/wahl.js?v=202609111836" defer></script>
<script src="assets/js/bereiche.js?v=202609111836" defer></script>
<script src="assets/js/kopf.js?v=202609111836" defer></script>
<script src="assets/js/glocke.js?v=202609111836" defer></script>
<script src="assets/js/report.js?v=202609111836" defer></script>
<script src="assets/js/ki.js?v=202609112217" defer></script>
<script src="assets/js/wahl.js?v=202609112217" defer></script>
<script src="assets/js/bereiche.js?v=202609112217" defer></script>
<script src="assets/js/kopf.js?v=202609112217" defer></script>
<script src="assets/js/glocke.js?v=202609112217" defer></script>
<script src="assets/js/report.js?v=202609112217" defer></script>
</body>
</html>
+13 -13
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111836" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111836" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/start.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/scouting.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/module.css?v=202609111836" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609112217" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609112217" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/start.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/scouting.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/module.css?v=202609112217" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609112217" />
</head>
<body class="start">
@@ -173,10 +173,10 @@
</div>
</div>
<script src="assets/js/wahl.js?v=202609111836" defer></script>
<script src="assets/js/bereiche.js?v=202609111836" defer></script>
<script src="assets/js/kopf.js?v=202609111836" defer></script>
<script src="assets/js/glocke.js?v=202609111836" defer></script>
<script src="assets/js/scouting.js?v=202609111836" defer></script>
<script src="assets/js/wahl.js?v=202609112217" defer></script>
<script src="assets/js/bereiche.js?v=202609112217" defer></script>
<script src="assets/js/kopf.js?v=202609112217" defer></script>
<script src="assets/js/glocke.js?v=202609112217" defer></script>
<script src="assets/js/scouting.js?v=202609112217" defer></script>
</body>
</html>
+12 -12
View File
@@ -12,21 +12,21 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111836" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111836" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/start.css?v=202609111836" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609112217" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609112217" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/start.css?v=202609112217" />
<!-- Nur die Startseite: Begrüßungskachel, Uhr, Glockenmulde. Siehe
den Hinweis in start.css -- dort stand es bis zum 07.09.2026. -->
<link rel="stylesheet" href="assets/css/heim.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/module.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/heim.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/module.css?v=202609112217" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609112217" />
</head>
<body class="start">
@@ -674,10 +674,10 @@
</p>
</main>
<script src="assets/js/wahl.js?v=202609111836" defer></script>
<script src="assets/js/bereiche.js?v=202609111836" defer></script>
<script src="assets/js/kopf.js?v=202609111836" defer></script>
<script src="assets/js/glocke.js?v=202609111836" defer></script>
<script src="assets/js/start.js?v=202609111836" defer></script>
<script src="assets/js/wahl.js?v=202609112217" defer></script>
<script src="assets/js/bereiche.js?v=202609112217" defer></script>
<script src="assets/js/kopf.js?v=202609112217" defer></script>
<script src="assets/js/glocke.js?v=202609112217" defer></script>
<script src="assets/js/start.js?v=202609112217" defer></script>
</body>
</html>
+14 -14
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111836" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111836" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/start.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/module.css?v=202609111836" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609112217" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609112217" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/start.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/module.css?v=202609112217" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609112217" />
</head>
<body class="start">
@@ -85,11 +85,11 @@
</main>
<script src="assets/js/ki.js?v=202609111836" defer></script>
<script src="assets/js/wahl.js?v=202609111836" defer></script>
<script src="assets/js/bereiche.js?v=202609111836" defer></script>
<script src="assets/js/kopf.js?v=202609111836" defer></script>
<script src="assets/js/glocke.js?v=202609111836" defer></script>
<script src="assets/js/startcheck.js?v=202609111836" defer></script>
<script src="assets/js/ki.js?v=202609112217" defer></script>
<script src="assets/js/wahl.js?v=202609112217" defer></script>
<script src="assets/js/bereiche.js?v=202609112217" defer></script>
<script src="assets/js/kopf.js?v=202609112217" defer></script>
<script src="assets/js/glocke.js?v=202609112217" defer></script>
<script src="assets/js/startcheck.js?v=202609112217" defer></script>
</body>
</html>
+13 -13
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111836" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111836" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/start.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/module.css?v=202609111836" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609112217" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609112217" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/start.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/module.css?v=202609112217" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609112217" />
</head>
<body class="start">
@@ -126,10 +126,10 @@
</main>
<script src="assets/js/wahl.js?v=202609111836" defer></script>
<script src="assets/js/bereiche.js?v=202609111836" defer></script>
<script src="assets/js/kopf.js?v=202609111836" defer></script>
<script src="assets/js/glocke.js?v=202609111836" defer></script>
<script src="assets/js/steckbrief.js?v=202609111836" defer></script>
<script src="assets/js/wahl.js?v=202609112217" defer></script>
<script src="assets/js/bereiche.js?v=202609112217" defer></script>
<script src="assets/js/kopf.js?v=202609112217" defer></script>
<script src="assets/js/glocke.js?v=202609112217" defer></script>
<script src="assets/js/steckbrief.js?v=202609112217" defer></script>
</body>
</html>
+80
View File
@@ -0,0 +1,80 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Talente · Spicy &amp; Dogi</title>
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609112217" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609112217" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/start.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/module.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609112217" />
</head>
<body class="start">
<div class="schleier" aria-hidden="true"></div>
<header class="kopfleiste">
<button type="button" class="zurueck-knopf" id="zurueck" hidden>
<svg class="zurueck-knopf__pfeil" viewBox="0 0 24 24" aria-hidden="true">
<path d="M14.5 5.5 8 12l6.5 6.5" />
</svg>
<span class="zurueck-knopf__text">Zurück</span>
</button>
<p class="marke"><span class="marke__text"><a class="zurueck" href="start.html">Team&nbsp;Dogi</a> · Talente</span></p>
<div class="kopfleiste__rechts">
<span class="wer" id="wer">…</span>
<button type="button" class="abmelden" id="abmelden">Abmelden</button>
</div>
</header>
<main class="inhalt inhalt--breit">
<section class="kopf-zeile">
<div>
<p class="marke">Entwicklung &amp; Nachwuchs</p>
<h1 class="titel">Talente</h1>
<p class="unterzeile">
Zuschauer, die ins Team passen könnten. Ein Trichter, keine Liste –
jede Stufe hat genau einen nächsten Schritt.
</p>
</div>
</section>
<p class="fehler" id="fehler" role="alert" aria-live="polite"></p>
<!-- ==================================================================
HIER STEHEN NAMEN VON MENSCHEN, DIE NICHTS DAVON WISSEN.
Diese Seite gehört DogFather und der rechten Hand — kein Modi,
keine Agentur, und ganz sicher nicht die Community. Durchgesetzt
wird das in der Rechtetafel UND in workspace-entwicklung.js; der
Satz hier ist nur die Erinnerung daran, warum.
Deshalb gibt es auch keine Rückmeldung an einen Kandidaten und
keinen heimlichen Test mit einem Zweitkonto. Wer beobachtet wird,
ohne es zu wissen, hat ein Recht darauf, dass daraus nichts
Öffentliches wird.
============================================================== -->
<section id="trichter" class="t-trichter" aria-busy="true"></section>
<section id="liste" class="t-liste" aria-busy="true">
<p class="leise">wird geladen …</p>
</section>
</main>
<script src="assets/js/wahl.js?v=202609112217" defer></script>
<script src="assets/js/bereiche.js?v=202609112217" defer></script>
<script src="assets/js/kopf.js?v=202609112217" defer></script>
<script src="assets/js/talente.js?v=202609112217" defer></script>
</body>
</html>
+13 -13
View File
@@ -7,20 +7,20 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111836" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111836" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/start.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/team.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/module.css?v=202609111836" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609112217" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609112217" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/start.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/team.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/module.css?v=202609112217" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609112217" />
</head>
<body class="start">
@@ -110,10 +110,10 @@
</main>
<script src="assets/js/wahl.js?v=202609111836" defer></script>
<script src="assets/js/bereiche.js?v=202609111836" defer></script>
<script src="assets/js/kopf.js?v=202609111836" defer></script>
<script src="assets/js/glocke.js?v=202609111836" defer></script>
<script src="assets/js/team.js?v=202609111836" defer></script>
<script src="assets/js/wahl.js?v=202609112217" defer></script>
<script src="assets/js/bereiche.js?v=202609112217" defer></script>
<script src="assets/js/kopf.js?v=202609112217" defer></script>
<script src="assets/js/glocke.js?v=202609112217" defer></script>
<script src="assets/js/team.js?v=202609112217" defer></script>
</body>
</html>
+12 -12
View File
@@ -7,20 +7,20 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111836" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111836" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/start.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/team.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/module.css?v=202609111836" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609112217" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609112217" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/start.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/team.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/module.css?v=202609112217" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609112217" />
</head>
<!-- `data-buehne` steht hier fest und nicht im Skript: Die Kachel zu
@@ -84,9 +84,9 @@
Kaputt ist dabei nichts -- er funktioniert. Genau deshalb faellt
es niemandem auf, und deshalb prueft pruef-css-klassen die
ABHAENGIGKEIT statt der Optik. Hier hat sie angeschlagen. -->
<script src="assets/js/wahl.js?v=202609111836" defer></script>
<script src="assets/js/bereiche.js?v=202609111836" defer></script>
<script src="assets/js/kopf.js?v=202609111836" defer></script>
<script src="assets/js/teamlage.js?v=202609111836" defer></script>
<script src="assets/js/wahl.js?v=202609112217" defer></script>
<script src="assets/js/bereiche.js?v=202609112217" defer></script>
<script src="assets/js/kopf.js?v=202609112217" defer></script>
<script src="assets/js/teamlage.js?v=202609112217" defer></script>
</body>
</html>
+12 -12
View File
@@ -7,14 +7,14 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111836" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111836" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/start.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/module.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609111836" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609112217" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609112217" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/start.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/module.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609112217" />
</head>
<body class="start">
@@ -110,9 +110,9 @@
</main>
<script src="assets/js/wahl.js?v=202609111836" defer></script>
<script src="assets/js/bereiche.js?v=202609111836" defer></script>
<script src="assets/js/kopf.js?v=202609111836" defer></script>
<script src="assets/js/treff-moderation.js?v=202609111836" defer></script>
<script src="assets/js/wahl.js?v=202609112217" defer></script>
<script src="assets/js/bereiche.js?v=202609112217" defer></script>
<script src="assets/js/kopf.js?v=202609112217" defer></script>
<script src="assets/js/treff-moderation.js?v=202609112217" defer></script>
</body>
</html>
+12 -12
View File
@@ -7,18 +7,18 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111836" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111836" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/start.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/module.css?v=202609111836" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609112217" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609112217" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/start.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/module.css?v=202609112217" />
<!-- haus.css MUSS die letzte Stilvorlage sein. Auf der Adresse des
Treffs biegt der Server genau diesen Dateinamen auf crew-haus.css
um, und die Seite kommt schon beim ERSTEN Abruf in Lila und
Babyblau an -- ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609112217" />
</head>
<body class="start">
@@ -190,9 +190,9 @@
Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen
Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen,
die seit dem 07.09. jede Seite danach absucht. -->
<script src="assets/js/wahl.js?v=202609111836" defer></script>
<script src="assets/js/bereiche.js?v=202609111836" defer></script>
<script src="assets/js/kopf.js?v=202609111836" defer></script>
<script src="assets/js/treff-regeln.js?v=202609111836" defer></script>
<script src="assets/js/wahl.js?v=202609112217" defer></script>
<script src="assets/js/bereiche.js?v=202609112217" defer></script>
<script src="assets/js/kopf.js?v=202609112217" defer></script>
<script src="assets/js/treff-regeln.js?v=202609112217" defer></script>
</body>
</html>
+13 -13
View File
@@ -12,22 +12,22 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111836" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111836" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/start.css?v=202609111836" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609112217" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609112217" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/start.css?v=202609112217" />
<!-- Formular- und Dialogstile: seit dem 07.09.2026 steht hier der
Dialog "Creator anlegen". -->
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/module.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/module.css?v=202609112217" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609112217" />
</head>
<body class="start">
@@ -149,11 +149,11 @@
</main>
<script src="assets/js/bereiche.js?v=202609111836" defer></script>
<script src="assets/js/bereiche.js?v=202609112217" defer></script>
<script src="assets/js/wahl.js?v=202609111836" defer></script>
<script src="assets/js/kopf.js?v=202609111836" defer></script>
<script src="assets/js/glocke.js?v=202609111836" defer></script>
<script src="assets/js/uebersicht.js?v=202609111836" defer></script>
<script src="assets/js/wahl.js?v=202609112217" defer></script>
<script src="assets/js/kopf.js?v=202609112217" defer></script>
<script src="assets/js/glocke.js?v=202609112217" defer></script>
<script src="assets/js/uebersicht.js?v=202609112217" defer></script>
</body>
</html>
+13 -13
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111836" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111836" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/start.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/wissen.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/module.css?v=202609111836" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609112217" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609112217" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/start.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/wissen.css?v=202609112217" />
<link rel="stylesheet" href="assets/css/module.css?v=202609112217" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609111836" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609112217" />
</head>
<body class="start">
@@ -190,10 +190,10 @@
</main>
<script src="assets/js/wahl.js?v=202609111836" defer></script>
<script src="assets/js/bereiche.js?v=202609111836" defer></script>
<script src="assets/js/kopf.js?v=202609111836" defer></script>
<script src="assets/js/glocke.js?v=202609111836" defer></script>
<script src="assets/js/wissen.js?v=202609111836" defer></script>
<script src="assets/js/wahl.js?v=202609112217" defer></script>
<script src="assets/js/bereiche.js?v=202609112217" defer></script>
<script src="assets/js/kopf.js?v=202609112217" defer></script>
<script src="assets/js/glocke.js?v=202609112217" defer></script>
<script src="assets/js/wissen.js?v=202609112217" defer></script>
</body>
</html>