Files
dogfather-universe/workspace/assets/js/werdegang.js
T
DogFatherGitandClaude Opus 5 18a5231b70 Die Tuer ohne Klingel, der Anruf ohne Ende, die Glocke ohne Liste
Siebter Durchgang durch die Team-Dogi-Seiten aus Benutzersicht. Alles am
Code belegt, die Groessenangaben nachgerechnet.

=== DIE ANMELDUNG ===

WOHER BEKOMMT MAN EINEN CODE? Stand nirgends. Vier Kacheln, ein Feld,
ein Knopf -- wer keinen Code hatte, fand keinen einzigen Satz dazu. Fuer
die Community besonders teuer: Sie ist die einzige Rolle, die von
aussen kommt und niemanden im Haus kennt.

"BITTE KURZ WARTEN" SIND BIS ZU ZEHN MINUTEN (VERSUCHE_FENSTER_MIN).
Wer nach zwanzig Sekunden nachsieht und dieselbe Meldung liest, haelt
die Seite fuer kaputt. Jetzt steht die Zahl da.

EIN RICHTIGER CODE AN DER FALSCHEN KACHEL fiel in dieselbe Absage wie
ein erfundener -- der Server sucht nur unter der gewaehlten Rolle. Wer
aus der Community kommt und auf "Modi" tippt (das klingt ja danach),
tippt seinen richtigen Code dreimal Buchstabe fuer Buchstabe. Ab dem
zweiten Fehlversuch kommt jetzt ein Hinweis auf die Auswahl -- ohne zu
sagen, welche die richtige waere.

=== DER ANRUF: VIER FEHLER, EINE ERFAHRUNG ===

DAS FREIZEICHEN LIEF UNENDLICH. Es gab keinen Zeitgeber, der aufhoert.
Wer niemanden erreichte, sass vor einem piependen Kasten mit "02:47"
darauf, als telefoniere er laengst. Der Server schickt den Verfallszeit-
punkt sogar mit (`laeuft_bis`) -- benutzt hat ihn nie jemand.

DER KLINGELKASTEN VERSCHWAND NACH 46 SEKUNDEN, waehrend der Server 120
gibt. Wer beim Klingeln das Handy aus der Tasche holt und in Sekunde 50
hinsieht, fand nichts mehr -- kein Kasten, keine Knoepfe -- waehrend der
Anrufer noch siebzig Sekunden Freizeichen hoerte. Die 120 Sekunden sind
im Server ausdruecklich damit begruendet, dass man Zeit zum Entsperren
braucht; eine Zeile im Browser hat diese Entscheidung aufgehoben.

DAS FREIZEICHEN PIEPTE WEITER, waehrend daneben "Keine Verbindung."
stand. Ton sagte "es klingelt noch", Text sagte "vorbei".

DIE UHR ZAEHLTE AB DEM WAEHLEN. Wer 30 Sekunden klingeln liess und 12
Sekunden sprach, las im Kasten "00:42" und danach im Chat "Anruf . 12
Sekunden". Der Server rechnet es richtig; der Kasten hat es wieder
kaputtgemacht.

Dazu: Die Mikrofon-Absage verwies auf "das Schloss-Symbol neben der
Adresse" -- in der installierten App gibt es weder Adresszeile noch
Schloss. Das Haus kennt diesen Fehler und hat ihn beim Weg zur
Anruf-Probe schon behoben; hier stand er noch.

Und die Anruf-Probe: Ihr einziger Punkt ohne Handlungsanweisung war
"Antwort 503." -- auf einer Seite, die ausdruecklich fuer jemanden
gebaut ist, der nicht technisch ist. Ihr Rueckweg fuehrte ausserdem
immer in den Chat, auch wenn man von der Startseite kam.

=== DER TREFF ===

EIN EINZEILER MACHTE SECHS ERKLAERTEXTE UNSICHTBAR. bewerben.js las
`g.unter`, der Server schickt `g.text` -- also immer undefined. Damit
fehlten ALLE SECHS Gruppeneinleitungen des Fragebogens ("Kein Test mit
richtigen Antworten"), und die CSS-Regel dafuer lief ins Leere. Uebrig
blieben sechs nackte Ueberschriften ueber fuenfzehn Fragen.

DIE EINZIGE "SO LAEUFT DAS HIER"-SEITE WAR FAST UNERREICHBAR. Die
Kachel "Regeln & Hilfe" zeigte auf ein Brett, das am ersten Tag leer
ist. Die Regeln, die es wirklich gibt (Begruessung, fuenf Regeln, drei
Stufen, Mindestalter), stehen auf treff-regeln.html -- erreichbar ueber
genau einen kleinen Textlink auf einer Brettseite. Die Kachel zeigt
jetzt dorthin.

FIEL /api/treff/lage AUS, versprach die Seite einem Zuschauer
Schreibrechte, die der Server ablehnt: Die Pruefung fiel bei `lage ===
null` auf `true` durch -- von "darf auf keinem Brett" auf "darf
ueberall". Formular auf, getippt, 403.

Dazu: hilfe.js hatte eine eigene kleine Fehlertabelle fuer zwei
Kennungen und endete sonst bei "Ging nicht." -- ausgerechnet die Seite,
auf der jemand mit einem Problem sitzt, hatte die inhaltsleerste Absage
im Haus. Und der gesperrte "Neue Meldung"-Knopf erklaerte sich nur im
Maus-Tooltip; die Community kommt von TikTok, also vom Handy.

=== DIE STARTSEITE ===

DIE COMMUNITY BEKAM EIN WORT. Unter dem Titel steht bei jeder Rolle ein
Satz -- beim Community-Mitglied stand "Community". Woertlich derselbe
Mangel, den der Kommentar bei MODI_ROLLENTEXT beschreibt und fuer den
Modi behebt.

DER RING MELDETE 100 %, bevor man irgendetwas getan hat. Am ersten Tag,
ohne eine einzige Aufgabe, stand das groesste Element der Seite auf
"100 % -- HEUTE NICHTS OFFEN". Gelesen wird zuerst die Zahl, und 100 %
heisst fuer jeden "fertig", nicht "leer".

UND BEI EINER STOERUNG BLIEB "wird geladen" FUER IMMER STEHEN -- der
fruehe `return` liess den Platzhalter unberuehrt.

=== DIE ZUGANGSVERWALTUNG ===

Der Code-Kasten sagte "jetzt weitergeben" und gab die Haelfte nicht
mit, die man weitergeben muss: WO sich die Person anmeldet. DogFather
ruft den Code durchs Zimmer, der andere fragt "und wo?". Die Adresse
kommt jetzt vom Server (anmeldeAdresseFuer, abgeleitet aus derselben
Weiche), dazu ein Knopf "Code und Adresse kopieren".

Er verschwieg ausserdem den Rettungsweg: "danach nicht mehr abrufbar"
stimmt fuer DIESEN Code -- man kann jederzeit einen neuen vergeben. Wer
das nicht weiss, glaubt, er habe einen Menschen angelegt, der nie
hereinkommt. Und das Fenster liess sich wortlos schliessen, waehrend
der Code offen stand.

=== OPTIK: NACHGERECHNET, NICHT VERMUTET ===

DIE GLOCKE STAND NICHT IN DER LISTE. Sie ist ein echtes <button> und
wurde von der 44-px-Regel erfasst, waehrend ihre vier Nachbarn durch
einen staerkeren Selektor auf 36 px gehen. Zwischen 401 und 560 px --
also bei 412 px (haeufigste Android-Breite) und 430 px (iPhone Pro Max)
-- stand eine 44 px hohe runde Pille zwischen fuenf 36-px-Quadraten.
Der 400er-Block listet sie korrekt; genau dieses Band fiel durch beide
Rechnungen. Dasselbe Muster wie am 06.09.

DIE 9,6-px-REPARATUR IM KALENDER WAR SEIT WOCHEN WIRKUNGSLOS.
kalender.css wird NACH start.css geladen, beide Selektoren sind gleich
stark -- also gewann `.6rem` auf jeder Breite unter 760 px. Genau der
Wert, den start.css als behobenen Fehler protokolliert.

Und die Hebung selbst unterschritt ihre eigene Grenze: Die Regel, die
zu kleine Schrift hochziehen soll, zog `.k-pille` auf 11,2 px -- 0,3 px
unter die 11,5, die 33 Zeilen darueber aufgestellt werden.

Weitere nachgerechnete Groessen: Spaltenkopf und KW-Spalte im Kalender
(10,56 px), Kalenderwoche am Balken (9,92 px), Wecker-Knopf (30 px --
der kleinste im Haus, 14 unter der Vorgabe), Chat-Zurueck auf dem
Tablet (34x34, 40 % weniger Flaeche als noetig, und der einzige Weg
zurueck in die Liste), Emoji-Knoepfe (34 px bei 2 px Abstand -- wer
danebentippt, verschickt ein anderes Zeichen, und das ist sofort
abgeschickt).

IM WINDOWS-KONTRASTMODUS HATTE DIE STARTSEITE KEINE UEBERSCHRIFT.
`.ztitel` und der Team-Dogi-Schriftzug sind mit `background-clip: text`
gesetzt; faellt der Verlauf weg, bleibt durchsichtiger Text. Beide
haben jetzt den Rueckfall, den gate.css schon lange hat.

UND MEIN EIGENES STREIFENRASTER rechnete die Spaltenzahl aus der Breite
statt aus den Daten (`auto-fit`): Bei 390 px passten sechs, alles
darueber fiel in eine zweite, unbeschriftete Zeile. Das `overflow-x`
daneben konnte nie greifen -- ein auto-fit-Raster wird nie breiter als
sein Kasten. Der Kommentar beschrieb ein Verhalten, das es nicht gab.

GEMESSEN: pruef-meldungen 8/0, pruef-css-klassen ALLES IN ORDNUNG,
pruef-rechtetafel 19/0, alle Module laden, keine doppelten
Kachelnamen/-ziele/-farben in allen vier Rollen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-19 15:07:31 +02:00

528 lines
22 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.
/* =====================================================================
ENTWICKLUNG — die Auswertung je Person (19.09.2026)
Zwei Gesichter, und welches man sieht, entscheidet der Server:
Wer das Team führt, bekommt die Karten; wer dazugehört, bekommt
ausschließlich die Bewegung in der eigenen.
---------------------------------------------------------------------
DIE DIAGRAMME SIND SELBST GEZEICHNET
Kein Diagrammpaket. Zwei Formen, beide aus wenigen Zeilen:
BEWEGUNG Balken um eine Nulllinie, nach oben „nach vorn“, nach
unten „zurück“. Sechs Monate, mehr passt auf ein Handy
nicht und sagt auch nichts.
STREIFEN Ein Raster: Zeile = Punkt, Spalte = Monat, Zelle = die
Stufe, die damals galt. Dieselbe Bauform wie der
Befinden-Verlauf — wer die eine kennt, liest die andere.
Ein Paket dafür wären 60 kB über eine Leitung, auf der jemand mit dem
Handy im Stream steht. Und es brächte seine eigenen Farben mit, die
dann neben den Hausfarben stünden.
---------------------------------------------------------------------
DIE FARBE IST NIE DIE AUSKUNFT
Jede Zelle trägt das Zeichen ihrer Stufe als Text (✓ ↗ ! –), jeder
Balken seine Zahl. Wer Farben schlecht unterscheidet, liest genau
dasselbe. Das ist im Haus seit dem Befinden-Verlauf so.
Und es gibt hier kein Rot. „Da hakt es“ ist eine Beobachtung, kein
Alarm; ein Rückschritt heißt oft nur, dass jemand genauer hingesehen
hat als beim letzten Mal.
===================================================================== */
(() => {
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) {
const a = await fetch(pfad, { credentials: 'same-origin' });
if (a.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); }
return a;
}
/* Die vier Stufen, wie der Server sie schickt — Zeichen inklusive.
Eine eigene Liste hier wäre die, die beim nächsten Nachschärfen
auseinanderläuft. */
let stufen = [];
const zeichenVon = (s) => stufen.find((x) => x.schluessel === s)?.zeichen || '·';
const namenVon = (s) => stufen.find((x) => x.schluessel === s)?.name || s;
const MONATSNAME = ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun',
'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'];
function monatKurz(m) {
const [j, mm] = String(m).split('-').map(Number);
if (!j || !mm) return m;
return `${MONATSNAME[mm - 1]}${mm === 1 ? ' ' + String(j).slice(2) : ''}`;
}
function datumKurz(iso) {
const t = Date.parse(iso || '');
if (!Number.isFinite(t)) return '';
return new Date(t).toLocaleDateString('de-DE',
{ day: '2-digit', month: '2-digit', year: '2-digit' });
}
/* =====================================================================
DIAGRAMM 1 — BEWEGUNG
ZWEI RICHTUNGEN UM EINE NULLLINIE. „Nach vorn“ und „zurück“ sind
dieselbe Größe mit verschiedenem Vorzeichen; zwei getrennte
Diagramme würden dazu verleiten, sie wie zwei Themen zu lesen.
EINE ACHSE, EIN MASSSTAB. Der höchste Balken bestimmt ihn, und er
gilt für oben und unten gleichermaßen — sonst sähe ein einzelner
Rückschritt so groß aus wie fünf Fortschritte.
KEINE ZAHL AN JEDEM BALKEN. Nur dort, wo etwas steht. Eine Null an
jedem leeren Monat wäre Lärm; die leere Spalte sagt dasselbe.
===================================================================== */
function bewegungBauen(bewegung, spalten) {
const wurzel = el('div', 'w-bewegung');
const hoechst = Math.max(1, ...bewegung.map(
(m) => Math.max(m.vor + m.geklaert + m.neu, m.zurueck + m.offen)));
const gitter = el('div', 'w-bewegung__gitter');
gitter.style.setProperty('--spalten', String(spalten.length));
for (const m of bewegung) {
const spalte = el('div', 'w-bewegung__spalte');
const oben = el('div', 'w-bewegung__halb w-bewegung__halb--oben');
const obenWert = m.vor + m.geklaert + m.neu;
if (obenWert) {
const b = el('div', 'w-bewegung__balken');
b.dataset.richtung = 'vor';
b.style.height = `${Math.round((obenWert / hoechst) * 100)}%`;
b.title = [
m.vor ? `${m.vor}× nach vorn` : null,
m.geklaert ? `${m.geklaert}× geklärt` : null,
m.neu ? `${m.neu}× zum ersten Mal` : null,
].filter(Boolean).join(', ');
oben.append(el('span', 'w-bewegung__zahl', String(obenWert)), b);
}
spalte.append(oben);
spalte.append(el('div', 'w-bewegung__null'));
const unten = el('div', 'w-bewegung__halb w-bewegung__halb--unten');
const untenWert = m.zurueck + m.offen;
if (untenWert) {
const b = el('div', 'w-bewegung__balken');
b.dataset.richtung = 'zurueck';
b.style.height = `${Math.round((untenWert / hoechst) * 100)}%`;
b.title = [
m.zurueck ? `${m.zurueck}× zurückgesetzt` : null,
m.offen ? `${m.offen}× auf „Kann ich nicht sagen“` : null,
].filter(Boolean).join(', ');
unten.append(b, el('span', 'w-bewegung__zahl', String(untenWert)));
}
spalte.append(unten);
spalte.append(el('div', 'w-bewegung__monat', monatKurz(m.monat)));
gitter.append(spalte);
}
wurzel.append(gitter);
/* DIE LEGENDE IST PFLICHT, nicht Zierde: Ohne sie trägt allein die
Farbe die Bedeutung, und das ist genau die Stelle, an der ein
Diagramm für manche Leute aufhört zu funktionieren. */
const legende = el('div', 'w-legende');
for (const [richtung, text] of [
['vor', 'nach vorn · geklärt · zum ersten Mal'],
['zurueck', 'zurückgesetzt · wieder offen'],
]) {
const p = el('span', 'w-legende__punkt');
const fleck = el('span', 'w-legende__fleck');
fleck.dataset.richtung = richtung;
p.append(fleck, el('span', 'w-legende__text', text));
legende.append(p);
}
wurzel.append(legende);
/* DIE ZAHLEN IN WORTEN (ergänzt 19.09.2026).
Über dem Balken steht „4". Was die 4 ausmacht, stand nur im
`title` — also beim Mausüberfahren, also am Handy nie. Und genau
dort steht jemand damit: im Stream, zwischen zwei Szenen.
Nur Monate mit Inhalt, damit aus sechs Zeilen nicht fünf leere
werden. */
const worte = el('div', 'w-bewegung__worte');
for (const m of bewegung) {
const teile = [
m.vor ? `${m.vor}× nach vorn` : null,
m.geklaert ? `${m.geklaert}× geklärt` : null,
m.neu ? `${m.neu}× zum ersten Mal` : null,
m.zurueck ? `${m.zurueck}× zurückgesetzt` : null,
m.offen ? `${m.offen}× wieder offen` : null,
].filter(Boolean);
if (!teile.length) continue;
const zeile = el('p', 'w-bewegung__wortzeile');
zeile.append(el('strong', 'w-bewegung__wortmonat', monatKurz(m.monat)));
zeile.append(el('span', 'w-bewegung__worttext', teile.join(' · ')));
worte.append(zeile);
}
if (worte.childElementCount) wurzel.append(worte);
return wurzel;
}
/* =====================================================================
DIAGRAMM 2 — DER STREIFEN
===================================================================== */
function streifenBauen(streifen, spalten) {
const gitter = el('div', 'w-streifen__gitter');
/* DIE SPALTENZAHL KOMMT AUS DEN DATEN (19.09.2026). Vorher rechnete
das CSS sie aus der Breite (`auto-fit`) -- bei 390 px passten
sechs, und alles darueber hinaus fiel in eine zweite,
unbeschriftete Zeile. Dieselbe Loesung wie beim
Bewegungsdiagramm nebenan. */
gitter.style.setProperty('--monate', String(spalten.length));
const kopf = el('div', 'w-streifen__zeile w-streifen__zeile--kopf');
kopf.append(el('span', 'w-streifen__punkt-name', ''));
for (const m of spalten) kopf.append(el('span', 'w-streifen__monat', monatKurz(m)));
gitter.append(kopf);
for (const z of streifen) {
const zeile = el('div', 'w-streifen__zeile');
zeile.append(el('span', 'w-streifen__punkt-name', z.titel));
for (const c of z.zellen) {
/* LEER IST NICHT „KANN ICH NICHT SAGEN“ (getrennt 19.09.2026).
Beim Durchgehen aus Benutzersicht aufgefallen: Hier stand für
„nie etwas gesetzt“ ein Mittelpunkt `·` — und „Kann ich nicht
sagen“ trägt im Katalog den Halbgeviertstrich `–`. Zwei kleine
waagerechte Striche in Nachbarzellen, und der Unterschied
zwischen ihnen ist genau der, den dieses Haus sonst sehr ernst
nimmt: „niemand hat hingesehen“ gegen „jemand hat hingesehen
und konnte es nicht sagen“.
Leer ist jetzt WIRKLICH leer — kein Zeichen, dafür ein
gestrichelter Rahmen. Man sieht das Fehlen, statt es zu lesen. */
const zelle = el('span', 'w-streifen__zelle',
c.stufe ? zeichenVon(c.stufe) : '');
if (c.stufe) {
zelle.dataset.stufe = c.stufe;
if (c.alt) zelle.dataset.alt = 'ja';
zelle.title = `${monatKurz(c.monat)}: ${namenVon(c.stufe)}`
+ (c.seit ? ` (gesetzt am ${datumKurz(c.seit)})` : '')
+ (c.alt ? ' – älter als drei Monate' : '');
} else {
zelle.dataset.stufe = 'leer';
zelle.title = `${monatKurz(c.monat)}: noch keine Einschätzung`;
}
zeile.append(zelle);
}
gitter.append(zeile);
}
/* DIE ZEICHENERKLÄRUNG — und sie ist der Grund, warum die Zeichen
überhaupt etwas taugen (ergänzt 19.09.2026).
Im Kopf dieser Datei steht: „Jede Zelle trägt das Zeichen ihrer
Stufe als Text. Wer Farben schlecht unterscheidet, liest genau
dasselbe." Das stimmt nur, wenn irgendwo steht, WAS die Zeichen
heißen. Tat es nicht — die Bedeutung steckte ausschließlich im
Mausüberfahren, und auf einem Handy gibt es das nicht.
Gebaut aus `stufen` vom Server, nicht abgeschrieben: Wer dort ein
Zeichen ändert, ändert die Erklärung mit. */
const legende = el('div', 'w-legende w-legende--streifen');
for (const st of stufen) {
const p = el('span', 'w-legende__punkt');
const z = el('span', 'w-legende__zeichen', st.zeichen);
z.dataset.stufe = st.schluessel;
p.append(z, el('span', 'w-legende__text', st.name));
legende.append(p);
}
const leerP = el('span', 'w-legende__punkt');
const leerZ = el('span', 'w-legende__zeichen', '');
leerZ.dataset.stufe = 'leer';
leerP.append(leerZ, el('span', 'w-legende__text', 'noch nichts gesetzt'));
legende.append(leerP);
const huelle = el('div');
huelle.append(gitter, legende);
return huelle;
}
/* =====================================================================
DIE TEXTE
===================================================================== */
function texteBauen(texte) {
const wurzel = el('div', 'w-texte');
for (const t of texte) {
const kasten = el('section', 'w-text');
kasten.dataset.art = t.art;
kasten.append(el('h4', 'w-text__titel', t.titel));
kasten.append(el('p', 'w-text__satz', t.satz));
if (t.punkte?.length) {
const liste = el('ul', 'w-text__liste');
for (const p of t.punkte) {
const zeile = el('li', 'w-text__zeile');
zeile.append(el('span', 'w-text__punkt', p.titel));
const rechts = el('span', 'w-text__rechts');
if (p.von && p.zu) {
rechts.append(el('span', 'w-text__stufe', namenVon(p.von)));
rechts.append(el('span', 'w-text__pfeil', '→'));
}
if (p.zu) {
const s = el('span', 'w-text__stufe', namenVon(p.zu));
s.dataset.stufe = p.zu;
rechts.append(s);
}
if (p.erwartung) rechts.append(el('span', 'w-text__stufe', p.erwartung));
if (p.tage != null) rechts.append(el('span', 'w-text__tage', `seit ${p.tage} T.`));
else if (p.am) rechts.append(el('span', 'w-text__tage', datumKurz(p.am)));
zeile.append(rechts);
liste.append(zeile);
}
kasten.append(liste);
}
wurzel.append(kasten);
}
return wurzel;
}
/* =====================================================================
DIE KARTE EINER PERSON
===================================================================== */
async function karteZeigen(id) {
const a = await hole(`/workspace/api/werdegang/person/${id}`);
if (!a.ok) { melde('Diese Karte lässt sich gerade nicht öffnen.'); return; }
const d = await a.json();
stufen = d.stufen || [];
$('liste-block').hidden = true;
$('mich-block').hidden = true;
$('karte-block').hidden = false;
$('karte-name').textContent = `${d.person.name} · ${d.person.rolle_name}`;
$('karte-satz').textContent = d.satz;
/* ==== DER DRITTE AUSGANG ========================================
Nicht „in Ordnung“ und nicht „nicht in Ordnung“, sondern:
KONNTE NOCH NICHT NACHSEHEN — mit Grund. Ein Diagramm über
einen einzigen Zeitpunkt wäre kein Diagramm, sondern ein
Balken, und er sähe aus wie eine Aussage.
============================================================= */
const bBlock = $('bewegung-block'), sBlock = $('streifen-block');
if (!d.verlauf_moeglich) {
bBlock.hidden = false; sBlock.hidden = true;
$('bewegung').replaceChildren();
$('bewegung-satz').textContent = d.gesetzt === 0
? 'Zu dieser Person ist noch nichts eingetragen. Sobald die erste '
+ 'Einschätzung steht, beginnt hier der Verlauf.'
: `Es gibt bisher ${d.monate_mit_inhalt === 1 ? 'einen Monat' : d.monate_mit_inhalt + ' Monate'} `
+ `mit Einschätzungen. Ein Verlauf braucht mindestens ${d.verlauf_ab} – `
+ 'vorher stünde hier eine Linie, die nichts bedeutet.';
} else {
bBlock.hidden = false; sBlock.hidden = false;
$('bewegung-satz').textContent =
`Die letzten ${d.spalten.length} Monate. Nach oben: weitergekommen, `
+ 'geklärt oder zum ersten Mal beantwortet. Nach unten: zurückgesetzt '
+ 'oder wieder offen. Beides ist eine Auskunft, keines ist eine Note.';
$('bewegung').replaceChildren(bewegungBauen(d.bewegung, d.spalten));
$('streifen-satz').textContent =
'Was in einem Monat galt, gilt auch im nächsten, solange niemand etwas '
+ `ändert – deshalb sind die Zellen durchgezogen. Blasse Zellen sind älter `
+ `als ${d.verblasst_nach_tagen} Tage und zählen nicht mehr mit.`;
$('streifen').replaceChildren(streifenBauen(d.streifen, d.spalten));
}
const tBlock = $('texte-block');
if (d.texte?.length) {
tBlock.hidden = false;
$('texte').replaceChildren(texteBauen(d.texte));
} else {
tBlock.hidden = true;
}
/* Die Adresse merkt sich, welche Karte offen ist — damit „Zurück“
auf die Liste führt und nicht aus der Seite heraus. */
history.replaceState(null, '', `?p=${id}`);
window.scrollTo({ top: 0, behavior: 'instant' });
}
/* =====================================================================
DIE LISTE
KEINE SORTIERUNG NACH AKTIVITÄT. Eine Liste, die die mit der
meisten Bewegung nach oben stellt, ist eine Rangliste mit einem
anderen Namen. Der Server sortiert nach Namen, und dabei bleibt es.
===================================================================== */
function listeBauen(d) {
const wurzel = el('div', 'w-liste__gitter');
for (const p of d.liste) {
const karte = el('button', 'w-karte');
karte.type = 'button';
karte.append(el('span', 'w-karte__name', p.name));
karte.append(el('span', 'w-karte__rolle', p.rolle_name));
const zeilen = el('span', 'w-karte__zeilen');
zeilen.append(el('span', 'w-karte__zeile',
p.gesetzt === 0
? 'Noch nichts eingetragen'
: `${p.gesetzt} von ${p.gesamt} Punkten beantwortet`));
zeilen.append(el('span', 'w-karte__zeile',
p.bewegt === 0
? `In ${d.monate} Monaten keine Änderung`
: `${p.bewegt} Änderung${p.bewegt === 1 ? '' : 'en'} in ${d.monate} Monaten`
+ (p.vor ? ` · ${p.vor} nach vorn` : '')));
if (p.zuletzt_tage != null) {
const z = el('span', 'w-karte__zeile',
p.zuletzt_tage === 0 ? 'Zuletzt heute angesehen'
: `Zuletzt vor ${p.zuletzt_tage} Tag${p.zuletzt_tage === 1 ? '' : 'en'} angesehen`);
/* DIE EINZIGE FARBE IN DER LISTE, und sie sagt etwas über UNS:
Wer seit über drei Monaten nicht hingesehen hat, hat keine
Auskunft über diesen Menschen, sondern über sich. */
if (p.zuletzt_tage >= 90) z.dataset.alt = 'ja';
zeilen.append(z);
}
karte.append(zeilen);
karte.addEventListener('click', () => karteZeigen(p.id).catch(() => {}));
wurzel.append(karte);
}
return wurzel;
}
async function listeZeigen() {
const a = await hole('/workspace/api/werdegang/liste');
if (a.status === 404) return false; /* kein Leitungszugang */
if (!a.ok) { melde('Die Karten lassen sich gerade nicht laden.'); return true; }
const d = await a.json();
$('karte-block').hidden = true;
$('mich-block').hidden = true;
$('liste-block').hidden = false;
$('liste').removeAttribute('aria-busy');
$('liste-hinweis').textContent = d.hinweis;
if (!d.liste.length) {
$('liste').replaceChildren(el('p', 'leise',
'Es gibt noch niemanden mit einer Karte. Sobald jemand ins Team kommt, '
+ 'steht er hier.'));
} else {
$('liste').replaceChildren(listeBauen(d));
}
history.replaceState(null, '', location.pathname);
return true;
}
/* =====================================================================
DIE EIGENE SICHT
OHNE NAMEN, OHNE ANLASS, OHNE STREIFEN. Sie sieht, DASS sich etwas
bewegt hat und wann — nicht, wer was gesetzt hat und was
danebensteht. Der Anlass ist für ein Gespräch aufgeschrieben, nicht
zum Lesen ohne den Menschen dazu.
===================================================================== */
async function michZeigen() {
const a = await hole('/workspace/api/werdegang/mich');
if (!a.ok) { melde('Das lässt sich gerade nicht laden.'); return; }
const d = await a.json();
$('liste-block').hidden = true;
$('karte-block').hidden = true;
$('mich-block').hidden = false;
$('titel').textContent = 'Deine Entwicklung';
$('unterzeile').textContent =
'Was sich über die Zeit bewegt hat. Keine Note, keine Punktzahl.';
if (!d.hat_karte) {
$('mich-satz').textContent =
'Für deine Rolle wird keine Entwicklungskarte geführt. Das ist keine '
+ 'Lücke – es gibt schlicht nichts, was hier stehen müsste.';
$('mich-hinweis').textContent = '';
return;
}
stufen = d.stufen || stufen;
$('mich-satz').textContent = d.satz;
$('mich-hinweis').textContent = d.hinweis;
/* DREI DINGE, DIE HIER GEFEHLT HABEN (19.09.2026).
Beim Durchgehen aus der Sicht dessen, dem diese Seite gehört,
fiel auf: Er bekam Balken ohne die Erklärung, die die Leitung
daneben bekommt („Nach oben: weitergekommen"). Er bekam eine
Aufforderung ohne Adressaten („Frag danach" — wen?). Und wenn
sich nichts bewegt hatte, bekam er ein leeres Element, also
nichts: eine Seite, die einlädt und dann schweigt.
Alle drei sind hier behoben. Es ist derselbe Gedanke wie überall
im Haus: Der leere Zustand ist kein Sonderfall, er ist der
häufigste Fall — vor allem am Anfang. */
const etwas = d.bewegung?.some((m) => m.vor || m.zurueck || m.neu || m.geklaert);
if (etwas) {
$('mich-bewegung').replaceChildren(bewegungBauen(d.bewegung, d.spalten));
$('mich-lesehilfe').textContent =
'Nach oben: ein Punkt ist weitergekommen. Nach unten: er wird '
+ 'gerade anders gesehen als vorher – das ist kein Rückschritt, '
+ 'sondern meist nur ein genauerer Blick.';
$('mich-lesehilfe').hidden = false;
} else {
/* KEIN DIAGRAMM ÜBER NICHTS. Ein Raster aus leeren Spalten sieht
aus, als sei etwas kaputt. Ein Satz ist eine Auskunft. */
$('mich-bewegung').replaceChildren(leerKasten(
'Hier ist noch nichts zu sehen',
'Sobald jemand aus der Leitung eine Einschätzung setzt oder '
+ 'ändert, erscheint sie hier als Bewegung. Am Anfang ist diese '
+ 'Seite leer – das ist der Normalfall, kein Fehler.'));
$('mich-lesehilfe').hidden = true;
}
/* DER WEG WEITER. „Frag danach" ohne einen Ort ist eine Sackgasse
mit freundlichem Ton. */
const wege = $('mich-wege');
wege.replaceChildren();
const k1 = el('a', 'knopf knopf--klein', 'Deine Aufgaben ansehen');
k1.href = 'entwicklung.html';
const k2 = el('a', 'knopf knopf--klein w-weg--leise', 'Jemanden fragen');
k2.href = 'chat.html';
wege.append(k1, k2);
wege.hidden = false;
}
/** Ein Kasten für „hier ist noch nichts" — Überschrift plus Satz.
*
* ALS EIGENE FUNKTION, weil dieser Fall im Haus überall vorkommt und
* überall gleich aussehen soll. Ein leerer Zustand, der jedes Mal
* anders gebaut ist, liest sich wie ein Fehler. */
function leerKasten(titel, satz) {
const k = el('div', 'w-leer');
k.append(el('p', 'w-leer__titel', titel));
k.append(el('p', 'w-leer__satz', satz));
return k;
}
/* ---- Start ---------------------------------------------------------- */
$('zur-liste').addEventListener('click', () => { listeZeigen().catch(() => {}); });
(async () => {
try {
/* ERST DIE LISTE FRAGEN. Antwortet der Server mit 404, führt diese
Person das Team nicht — dann ist die eigene Sicht die richtige.
Die Rolle hier selbst zu prüfen hieße, dieselbe Entscheidung an
zwei Stellen zu treffen, und die zweite ist immer die, die
irgendwann etwas durchlässt. */
const leitung = await listeZeigen();
if (!leitung) { await michZeigen(); return; }
const p = new URLSearchParams(location.search).get('p');
if (p) await karteZeigen(Number(p));
} catch (f) {
if (String(f?.message || '') !== 'abgemeldet') {
melde('Die Seite lässt sich gerade nicht laden.');
}
}
})();
})();