Files
dogfather-universe/workspace/assets/js/talente.js
T
DogFatherGitandClaude Opus 5 bba3642664 Aus der Probe wird ein Zugang - und die Seite sagt nicht mehr "Agentur"
Die drei offenen Punkte aus Abschnitt 9 des Plans, plus Filipes zweite
Ansage: "auf dieser seite soll nichts stehen von agentur, diese seite
hier ist nur fuer mich meine modis und meine community."

1. PROBE -> ZUGANG (ein Knopf statt eines Satzes)

Auf der Stufe "Probe" stand bisher "Zugang in Personen & Zugaenge
anlegen, Rolle Modi" - und wer das vergass, hatte eine Karte auf "Im
Team" und keinen Menschen darin. Jetzt haengt das Anlegen am Schritt
selbst.

Ueber DENSELBEN Weg wie in "Personen & Zugaenge", nicht ueber einen
zweiten: POST /workspace/api/verwaltung/personen, mit ihrer
Rechtepruefung, ihrem Protokolleintrag und ihrem genau einmal
angezeigten Code. Der Talente-Server legt selbst keinen einzigen Zugang
an, und die Pruefung zaehlt nach, dass es bei einem Weg bleibt.

Der Code steht im gleichen Kasten wie dort - die Gestaltung ist aus
personen.css nach start.css umgezogen, weil talente.html sie sonst nicht
laedt. Ein Geheimnis sieht im ganzen Haus gleich aus. Er steht
ausserhalb der Liste, sonst waere er in dem Moment weg, in dem er
entsteht: wenn die Karte auf "Im Team" springt.

Die rechte Hand bekommt an dieser Stelle KEINEN Knopf, sondern einen
Satz. Ein Knopf, der ihr jedes Mal "darfst du nicht" antwortet, waere
schlechter als gar keiner - er verspricht etwas.

2. DIE EIGENE KARTE (Abschnitt "Deine Karte")

Wer beschrieben wird, darf es lesen - aber erst, wenn ALLE gesetzt
haben (sonst waere die erste Einschaetzung eine Vorgabe fuer die
zweite), und ohne Namen und ohne Anlasstext. Gemessen in beide
Richtungen: vorher nicht sichtbar, nachher sichtbar.

3. DER VORLAGENTEXT

Gebaut aus den angeklickten Merkmalen, in einem Feld zum Aendern, nicht
zum Abschicken. Ohne Merkmale steht auch keines drin.

4. "GILT FUER" SAGT DER SERVER

In bereich.js stand woertlich "Agentur" - richtig auf der
Agenturadresse, falsch auf jeder anderen. Jetzt liefert der Server
`gehoert` ("Der Treff" / "Team Dogi" / "Agentur"); gemessen mit einem
einzigen Menschen auf zwei Adressen.

PRUEFUNGEN: treff 58 -> 66, nachwuchs 69 -> 109, neue-seiten 70 -> 93.
Die Katalogseiten werden jetzt auch mit den Augen der rechten Hand
angesehen - ohne das waere "Deine Karte" nie auf einem Bildschirm
gewesen. Und pruef-neue-seiten wartet nicht mehr 900 ms, sondern bis
sich der Text nicht mehr aendert: Die Karte stand da und wurde
trotzdem als fehlend gemeldet, weil zu frueh gelesen wurde.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-11 23:13:50 +02:00

409 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
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;
let darfAnlegen = false; // darf ich hier einen Modi-Zugang anlegen?
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));
/* DER LETZTE SCHRITT IST KEIN SCHRITT, SONDERN EINE ENTSCHEIDUNG.
Aus einem Namen wird ein Mensch mit einem Zugang. Deshalb haengt
genau daran das Anlegen selbst -- und nicht ein Satz, den man
danach von Hand nachbaut. Bis heute stand hier "Zugang in
Personen & Zugaenge anlegen, Rolle Modi", und wer das vergass,
hatte eine Karte auf "Im Team" und keinen Menschen darin. */
const zugangNoetig = k.naechste === 'imteam';
if (k.knopf && k.naechste && (!zugangNoetig || darfAnlegen)) {
const b = el('button', 'schritt t-schritt__knopf',
zugangNoetig ? 'Ins Team holen · Zugang anlegen' : k.knopf);
b.type = 'button';
b.addEventListener('click', async () => {
b.disabled = true;
let zugang = null;
if (zugangNoetig) {
zugang = await zugangAnlegen(k);
if (!zugang) { b.disabled = false; return; }
}
const r = await hole(`/workspace/api/talente/${k.id}/stufe`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ stufe: k.naechste }),
});
b.disabled = false;
if (!r.ok) {
/* ZWEI DINGE SIND PASSIERT, EINES DAVON HAT GEKLAPPT. Ein
blosses "ging nicht" waere hier die gefaehrlichste
Auskunft von allen -- der Zugang steht ja, und niemand
wuesste es. */
melde(zugang
? `Der Zugang für ${zugang.name} steht – der Code steht oben. Nur die Karte `
+ 'blieb auf „Probe“. Bitte den Schritt gleich noch einmal setzen.'
: 'Ging nicht.');
return;
}
offen.add(k.id);
await laden();
});
schritt.append(b);
} else if (zugangNoetig && k.knopf) {
/* KEIN KNOPF INS LEERE. Die rechte Hand sieht den Trichter
vollstaendig, aber Zugaenge vergibt nur DogFather. Ein Knopf,
der ihr jedes Mal "darfst du nicht" antwortet, waere schlechter
als gar keiner -- er verspricht etwas. */
schritt.append(el('p', 't-schritt__liegt',
'Den Zugang legt DogFather an – sag ihm Bescheid. Die Karte bleibt so '
+ 'lange auf „Probe“.'));
}
/* DER VORLAGENTEXT -- genau auf der Stufe, auf der er gebraucht
wird. Eine Schaltfläche „Text anzeigen" auf jeder Stufe wäre auf
vier von fünf davon eine Ablenkung. */
if (k.stufe === 'beobachtet' && k.ansprechtext) {
const b = el('button', 'schritt t-schritt__text', 'Vorlagentext');
b.type = 'button';
b.addEventListener('click', () => textZeigen(k));
schritt.append(b);
}
if (k.liegt) {
schritt.append(el('p', 't-schritt__liegt',
'Das liegt länger, als es sollte. Entweder weiter – oder bewusst beenden.'));
}
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;
}
/* ---- Aus der Probe wird ein Zugang ---------------------------------
DERSELBE WEG WIE IN "PERSONEN & ZUGAENGE", nicht ein zweiter.
/workspace/api/verwaltung/personen ist die einzige Stelle im ganzen
Haus, an der ein Zugang entsteht -- mit ihrer Rechtepruefung, ihrer
Rollenpruefung, ihrem Protokolleintrag und ihrem genau einmal
angezeigten Code. Ein eigener Weg von hier waere eine zweite Tuer
zu genau dem, was am wenigsten zwei Tueren haben darf.
NICHT ueber hole(): Diese Adresse antwortet Nicht-DogFather mit
404, und hole() versteht ein 404 als "Seite gibt es nicht" und
schickt zur Startseite. Ein Rechtefehler wuerde die rechte Hand
also aus dem Trichter werfen. Hier wird deshalb selbst gelesen.
@returns {object|null} die angelegte Person, oder null bei Abbruch. */
async function zugangAnlegen(k) {
if (!darfAnlegen) { melde('Zugänge legt DogFather an.'); return null; }
const vorschlag = String(k.titel || '').split(/[\s,(–-]+/)[0] || '';
const name = prompt(
'Wie soll die Person im Workspace heißen?\n\n'
+ 'Das ist der Name, den euer Team sieht. Gleich danach bekommst du '
+ 'einmalig ihren Zugangscode – danach nie wieder.',
vorschlag);
if (name === null) return null; // abgebrochen
if (String(name).trim().length < 2) { melde('Der Name ist zu kurz.'); return null; }
let a;
try {
a = await fetch('/workspace/api/verwaltung/personen', {
credentials: 'same-origin',
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: String(name).trim(), rolle: 'modi' }),
});
} catch { melde('Keine Verbindung – der Zugang wurde nicht angelegt.'); return null; }
if (a.status === 404 || a.status === 403) {
melde('Zugänge legt DogFather an – sag ihm Bescheid.');
return null;
}
if (!a.ok) {
const grund = (await a.json().catch(() => ({}))).fehler;
melde(grund || 'Der Zugang ließ sich nicht anlegen.');
return null;
}
const neu = await a.json();
codeZeigen(neu);
return neu;
}
/* Der Code steht in einem Kasten, den man SCHLIESSEN muss -- nicht in
einer Meldung, die von selbst verschwindet. Er ist das Einzige auf
dieser Seite, das es nur ein einziges Mal gibt. */
function codeZeigen(neu) {
$('code-name').textContent = `${neu.name} · Modi`;
$('code-wert').textContent = neu.code;
$('code-kasten').hidden = false;
$('code-kasten').scrollIntoView({ behavior: 'smooth', block: 'center' });
$('code-kopieren').focus();
}
$('code-kopieren')?.addEventListener('click', async () => {
const code = $('code-wert').textContent;
try { await navigator.clipboard.writeText(code); melde('Code kopiert.'); }
catch {
/* Ohne Zwischenablage-Recht: markieren, dann geht Strg+C. Der
Code steht ja da -- "ging nicht" waere hier unbrauchbar. */
getSelection()?.removeAllRanges();
const r = document.createRange();
r.selectNodeContents($('code-wert'));
getSelection()?.addRange(r);
melde('Markiert – mit Strg+C kopieren.');
}
});
$('code-schliessen')?.addEventListener('click', () => {
$('code-kasten').hidden = true;
$('code-wert').textContent = ''; // auch aus der Seite raus
melde('');
});
/* DER TEXT WIRD GEZEIGT, NICHT VERSCHICKT.
Eine Nachricht, die ein Programm abschickt, liest sich wie eine
Nachricht, die ein Programm abgeschickt hat — und genau das soll
dieser Text nicht sein. Er steht da zum Lesen, Ändern und
Kopieren, in einem Feld, das man bearbeiten kann. */
function textZeigen(k) {
const alt = document.getElementById('t-textfeld');
if (alt) alt.closest('.t-text')?.remove();
const kasten = el('div', 't-text');
kasten.append(el('h3', 't-text__titel', `Vorschlag für ${k.titel}`));
kasten.append(el('p', 't-text__satz',
'Gebaut aus den Merkmalen, die du angeklickt hast. Ändere ihn ruhig – '
+ 'er soll nach dir klingen, nicht nach einer Vorlage. Abgeschickt wird '
+ 'hier nichts.'));
const feld = el('textarea', 't-text__feld');
feld.id = 't-textfeld';
feld.rows = 14;
feld.value = k.ansprechtext;
kasten.append(feld);
const reihe = el('div', 't-text__knoepfe');
const kopieren = el('button', 'schritt', 'Kopieren');
kopieren.type = 'button';
kopieren.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(feld.value);
melde('Kopiert. Jetzt einfügen, wo du ihn ansprichst.');
} catch {
/* Ohne Zwischenablage-Recht: markieren, dann geht Strg+C.
Eine Fehlermeldung „ging nicht" wäre hier unbrauchbar — der
Text ist ja da. */
feld.select();
melde('Markiert – mit Strg+C kopieren.');
}
});
reihe.append(kopieren);
const zu = el('button', 'schritt', 'Schließen');
zu.type = 'button';
zu.addEventListener('click', () => kasten.remove());
reihe.append(zu);
kasten.append(reihe);
const karte = [...document.querySelectorAll('.t-karte')]
.find((x) => x.querySelector('.t-karte__titel')?.textContent === k.titel);
(karte || $('liste')).append(kasten);
feld.focus();
}
function merkmal(k, m, istWarnung) {
const gesetzt = (istWarnung ? k.warnzeichen : k.merkmale).includes(m.schluessel);
const b = el('button', 't-merkmal');
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();
}
/* WER ZUGAENGE ANLEGEN DARF, SAGT DER SERVER -- dieselbe Auskunft,
aus der auch "Personen & Zugaenge" ihre Rollenknoepfe baut. Eine
eigene Rollenabfrage hier waere eine zweite Meinung darueber, und
die zweite ist die, die irgendwann mehr durchlaesst.
VOR dem ersten Zeichnen, nicht daneben: Sonst steht der Knopf
einen Wimpernschlag lang da und verschwindet wieder. */
async function ichHolen() {
try {
const a = await hole('/workspace/api/ich');
if (!a.ok) return;
const ich = await a.json();
window.werZeigen?.(ich);
darfAnlegen = Array.isArray(ich?.darf_anlegen) && ich.darf_anlegen.includes('modi');
} catch { /* umgeleitet */ }
}
(async () => {
try { await ichHolen(); await laden(); } catch { /* umgeleitet */ }
})();
})();