Files
dogfather-universe/workspace/assets/js/werdegang.js
T
DogFatherGitandClaude Opus 5 d6df590224 Telefonieren ging nur ueber UDP -- und die Seite sprang immer hoch
=== 1. WARUM TELEFONIEREN NICHT GING ===

Filipe: "es klingelt erscheint auch alles bei jedem aber telefonieren
klappt immer noch nicht."

GEMESSEN, BEVOR ETWAS GEAENDERT WURDE -- und alles war gruen:

  coturn laeuft                 aktiv
  STUN von aussen               antwortet, nennt meine oeffentliche Adresse
  echte Zuteilung von aussen    7/7, Testpaket kam an (Port 49183)
  Dreier-Anruf im Prueflauf     verbindet, alle hoeren alle

Vier Messungen, vier Mal in Ordnung -- und das Telefon ging trotzdem
nicht. Der Grund: Der Prueflauf faehrt BEIDE Seiten auf demselben
Rechner. Dort finden sie sich ueber die direkte Adresse, und die
Vermittlung wird nie gebraucht. Draussen sitzen sie in verschiedenen
Netzen.

DER FUND: Eingetragen war eine einzige Adresse --

    turn:159.195.212.167:3478

Ein `turn:` OHNE `?transport=` heisst UDP, und nur UDP. Nachgemessen
hoert coturn aber auf beidem: 3478/UDP offen, 3478/TCP offen (5349/TLS
ist zu). Angeboten wurde nur der halbe Server.

Wer in einem Netz sitzt, das UDP nach draussen sperrt -- Mobilfunk mit
strengem Profil, Gast-WLAN, Firmennetz --, bekam damit KEINEN
Vermittlungsweg. Es klingelt (das laeuft ueber die Website, also ueber
443), und danach passiert nichts. Genau das gemeldete Bild.

Aus einem Eintrag werden jetzt drei Wege:
  stun:host:port                die eigene Adresse finden, ohne Vermittlung
  turn:host:port?transport=udp  der schnelle Weg
  turn:host:port?transport=tcp  der Weg durch fast jede Sperre

ABGELEITET, NICHT EINGETRAGEN: Drei Zeilen von Hand waeren drei
Stellen, an die beim naechsten Serverumzug jemand denken muesste --
die abgeschriebene Liste, die hier schon zweimal teuer war. Ein
Eintrag MIT `?transport=` bleibt unangetastet, ein fremder Dienst mit
eigenem Passwort sowieso.

pruef-turn-wege (neu, 15/0) sichert beides: dass jeder Weg herauskommt
UND dass jeder turn:-Weg Zugangsdaten traegt. Das Zweite ist das
wichtigere -- auffaechern ohne anmelden haette den Fehler nur
verschoben.

=== 2. DIE SEITE SPRANG BEIM ZURUECKGEHEN IMMER HOCH ===

Filipe: "das nervt man muss dan immer wieder runter scrollen bis man
da ist wo man vorher war."

Der Browser versucht es sogar -- scrollRestoration steht ab Werk auf
"auto". Nur: Jede Seite hier kommt fast leer an und holt ihren Inhalt
danach per Abruf. In dem Moment, in dem der Browser die alte Position
wiederherstellen will, ist das Dokument ein paar hundert Pixel hoch.
Er kann nicht auf Zeile 900 springen, die es noch nicht gibt -- und er
versucht es kein zweites Mal.

Jetzt macht es kopf.js selbst (gilt damit auf allen 21 Seiten): Stelle
merken beim Verlassen, beim Oeffnen zurueckholen und jeden Bildaufbau
lang versuchen, bis das Dokument hoch genug ist. Nach drei Sekunden
wird aufgegeben.

Drei Dinge sind Absicht:
  - NUR beim Zurueckgehen, nicht bei jedem Oeffnen. Wer eine Kachel
    anklickt, will oben anfangen.
  - WER SELBST SCROLLT, GEWINNT. Rad, Wisch oder Taste beenden das
    Nachspringen sofort.
  - Nach einer Stunde vergessen -- auf Zeile 900 zu landen, weil man
    gestern dort war, ist keine Hilfe.

=== 3. DIE LISTE IST NACH ROLLE GETRENNT ===

Filipe: "die rechte hand rolle immer zuerst und dan die modis. die
sollen auch schoen getrennt sein und verschieden aussehen also die
rechte hand viel spezieller."

Die Reihenfolge macht der Server ueber ROLLEN_SORTIERUNG -- dieselbe
Konstante wie ueberall sonst im Haus, nicht eine zweite. Eine
Sortierung nach ROLLE ist ausdruecklich keine Rangliste: Sie sagt
nichts darueber, wie gut jemand ist, nur welche Aufgabe er hat.
Deshalb steht ueber jeder Gruppe ein Satz und keine Zahl.

"Spezieller" heisst hier Material, nicht Groesse: eigener Farbton als
Kante und Schimmer, hellere Flaeche, kraeftigerer Name. Beide Karten
sind GLEICH GROSS und tragen dieselben Zeilen -- zwei Sorten Aufgabe,
keine Rangfolge. Im Kontrastmodus traegt eine doppelte Kante die
Unterscheidung, weil dort keine Farbe mehr wirkt.

=== 4. DER ANRUFKASTEN ===

Vorher: vier gleich aussehende Pillen nebeneinander, darunter eine
Geraeteauswahl, die das breiteste Element war. Kein Name, keine
Ordnung, und der einzige Knopf mit Folgen sah aus wie die anderen.

  - MAN SAH NICHT, MIT WEM. Der wichtigste Satz eines Telefonats fehlte.
    Der Chat kennt den Namen und gibt ihn jetzt mit; wer rangeht, sieht
    den Anrufer.
  - "MIKRO AN" WAR ZWEIDEUTIG -- "ist an" oder "schalt an"? Wer falsch
    raet, sitzt stumm da. Jetzt traegt ein durchgestrichenes Zeichen
    den Zustand, das Wort nur die Sache. aria-pressed bleibt die
    Wahrheit, auch fuer Vorleseprogramme.
  - DIE GERAETEAUSWAHL liegt hinter einem kleinen Knopf, der nur
    erscheint, wenn es ueberhaupt etwas zu waehlen gibt.
  - AUFLEGEN ist als einziger Knopf farbig und steht immer rechts.
  - Ein ruhiger Puls atmet, solange die Verbindung aufbaut, und haelt
    an, sobald sie steht. Bei prefers-reduced-motion steht er still.

=== NEBENBEFUND, DEN DIE PRUEFUNG GEFUNDEN HAT ===

pruef-anruf wurde durch die Auffaecherung an vier Stellen rot: Sie
griff `adressen[0]` und erwartete dort Zugangsdaten. Das ist jetzt der
STUN-Weg, und der hat richtigerweise keine. Gesucht wird nicht mehr
nach PLATZ, sondern nach ART -- ein Index ist eine Annahme darueber,
wie die Liste aussieht, und die hat sich gerade geaendert.

Dazu angepasst: Die Pruefung "die Uhr laeuft" verlangte, dass die Uhr
schon beim Klingeln zaehlt. Seit heute frueh beginnt sie beim
Rangehen (sonst standen im Kasten und im Chat zwei verschiedene
Zahlen). Sie prueft jetzt das Gegenteil -- statt sie zu streichen,
denn eine Zeile weniger haette den Fehler beim naechsten Umbau wieder
durchgelassen.

GEMESSEN: pruef-anruf 114/0 (vorher 111 -- drei MEHR, weil die
Auffaecherung mitgeprueft wird), pruef-turn-wege 15/0,
pruef-meldungen 8/0, pruef-css-klassen ALLES IN ORDNUNG,
pruef-rechtetafel 19/0.

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

571 lines
24 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.
===================================================================== */
/* Was über einer Gruppe steht. Aus der Rolle abgeleitet, nicht
abgeschrieben -- der Rollenname kommt vom Server. */
const GRUPPENSATZ = {
hand: 'Koordiniert den Alltag und schaut mit dir zusammen auf das Team.',
modi: 'Die Leute, die im Chat und im Live stehen.',
};
function listeBauen(d) {
const wurzel = el('div', 'w-liste');
/* ---- NACH ROLLE GRUPPIERT (19.09.2026) -------------------------
Filipe: „die reihenfolge der listen soll immer angepasst sein
... die rechte hand rolle immer zuerst und dan die modis. die
sollen auch schoen getrennt sein und verschieden aussehen also
die rechte hand viel spezieller."
Die REIHENFOLGE macht der Server (ROLLEN_SORTIERUNG) -- hier
wird nur gruppiert, wo sie wechselt. Eine eigene Sortierung im
Browser waere die zweite Wahrheit, und die zweite ist die, die
beim naechsten Umbau abweicht.
DAS IST KEINE RANGLISTE. Eine Sortierung nach Rolle sagt nichts
darueber, wie gut jemand ist -- nur, welche Aufgabe er hat.
Deshalb steht ueber jeder Gruppe ein Satz, der genau das
erklaert, und keine Zahl. */
let letzteRolle = null;
let gitter = null;
for (const p of d.liste) {
if (p.rolle !== letzteRolle) {
letzteRolle = p.rolle;
const kopf = el('div', 'w-gruppe');
kopf.dataset.rolle = p.rolle;
kopf.append(el('h3', 'w-gruppe__name', p.rolle_name));
if (GRUPPENSATZ[p.rolle]) {
kopf.append(el('p', 'w-gruppe__satz', GRUPPENSATZ[p.rolle]));
}
wurzel.append(kopf);
gitter = el('div', 'w-liste__gitter');
gitter.dataset.rolle = p.rolle;
wurzel.append(gitter);
}
const karte = el('button', 'w-karte');
karte.type = 'button';
/* Die Rolle haengt an der Karte, nicht an einer Klasse: So kann
das CSS sie einfaerben, ohne dass hier eine zweite Liste von
Rollennamen entsteht. */
karte.dataset.rolle = p.rolle;
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(() => {}));
(gitter || 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.');
}
}
})();
})();