diff --git a/server/pruef-anruf.mjs b/server/pruef-anruf.mjs
index 47c04f29..188b8384 100644
--- a/server/pruef-anruf.mjs
+++ b/server/pruef-anruf.mjs
@@ -686,10 +686,42 @@ melde("=== 6b. Zugangsdaten fuer die Vermittlung ===");
const a = await roh("/workspace/api/anruf/adressen", CREW, { cookie: kModi });
const r = json(a);
- ok(r.turn_gesamt === 1 && r.turn_bereit === 1,
- `der Modi bekommt einen einsatzbereiten Eintrag (${r.turn_bereit}/${r.turn_gesamt})`);
- ok(!!r.adressen?.[0]?.credential && !!r.adressen?.[0]?.username,
- "mit Benutzername und Passwort");
+ /* AUS EINEM EINTRAG WERDEN DREI WEGE (angepasst 19.09.2026).
+
+ Bis heute stand hier `adressen[0]` -- und das war der einzige
+ Eintrag. Seit ein schlichtes `turn:host:port` in stun + turn/udp
+ + turn/tcp aufgefaechert wird (workspace-turn.js), ist
+ `adressen[0]` der STUN-Weg, und der hat RICHTIGERWEISE kein
+ Passwort. Die Pruefung suchte also am falschen Eintrag.
+
+ WARUM ES DIESE AUFFAECHERUNG GIBT: Ein `turn:` ohne
+ `?transport=` heisst UDP, und nur UDP. Wer in einem Netz sitzt,
+ das UDP sperrt, bekam damit keinen Vermittlungsweg -- es
+ klingelte, und danach passierte nichts. Genau der Fehler, den
+ Filipe am 19.09. gemeldet hat.
+
+ Gesucht wird jetzt nach ART, nicht nach Platz. Ein Index ist
+ eine Annahme darueber, wie die Liste aussieht -- und die hat
+ sich schon einmal geaendert. */
+ const turnWege = (r.adressen || []).filter(
+ (e) => /^turns?:/.test(String(e.urls || "")));
+ const stunWege = (r.adressen || []).filter(
+ (e) => /^stun:/.test(String(e.urls || "")));
+ const ersterTurn = turnWege[0];
+
+ ok(r.turn_gesamt === 2 && r.turn_bereit === 2,
+ `der Modi bekommt zwei einsatzbereite Wege (${r.turn_bereit}/${r.turn_gesamt})`);
+ ok(turnWege.length === 2
+ && turnWege.some((e) => /transport=udp/.test(String(e.urls)))
+ && turnWege.some((e) => /transport=tcp/.test(String(e.urls))),
+ `naemlich UDP und TCP (${turnWege.map((e) => e.urls).join(", ")})`);
+ ok(stunWege.length === 1, "dazu ein STUN-Weg, der ohne Vermittlung auskommt");
+ /* Die ZAHL in die Bedingung, nicht nur in den Meldetext: `every()`
+ auf einer leeren Liste waere sonst trivial wahr. */
+ ok(turnWege.length === 2 && turnWege.every((e) => e.credential && e.username),
+ "und beide tragen Benutzername und Passwort");
+ ok(!stunWege[0]?.credential,
+ "STUN bekommt keins -- es kennt keine Anmeldung");
/* DIE WICHTIGSTE PRUEFUNG DES ABSCHNITTS. Im ROHEN Text gesucht,
nicht im ausgewerteten Objekt. */
@@ -699,12 +731,14 @@ melde("=== 6b. Zugangsdaten fuer die Vermittlung ===");
/* Jeder bekommt EIGENE Zugangsdaten -- im coturn-Protokoll steht
damit die Nummer, nicht ein Sammelkonto. */
const rDogi = json(await roh("/workspace/api/anruf/adressen", HAUS, { cookie: kDogi }));
- ok(rDogi.adressen?.[0]?.username !== r.adressen?.[0]?.username,
+ const dogiTurn = (rDogi.adressen || []).find(
+ (e) => /^turns?:/.test(String(e.urls || "")));
+ ok(!!dogiTurn?.username && dogiTurn.username !== ersterTurn?.username,
"DogFather und der Modi bekommen verschiedene Zugangsdaten");
/* Der Ablauf muss fuer den Browser LESBAR sein -- darauf beruht das
Auffrischen vor jedem Anruf in anruf.js. */
- const ablauf = Number(String(r.adressen?.[0]?.username || "").split(":")[0]);
+ const ablauf = Number(String(ersterTurn?.username || "").split(":")[0]);
ok(ablauf > Math.floor(Date.now() / 1000) + 3600,
`der Ablauf steht vorn im Benutzernamen und liegt in der Zukunft (${ablauf})`);
}
@@ -812,7 +846,20 @@ melde("=== 7. Im Browser ===");
ok(kasten.da, `der Anrufkasten erscheint ("${kasten.lage}")`);
ok(/klingelt|Verbind/i.test(kasten.lage),
`und sagt, was gerade passiert ("${kasten.lage}")`);
- ok(/^\d\d:\d\d$/.test(kasten.zeit), `die Uhr laeuft (${kasten.zeit})`);
+ /* WAEHREND ES KLINGELT, ZAEHLT DIE UHR NICHT (geaendert 19.09.2026).
+
+ Vorher stand hier, die Uhr MUESSE laufen -- und sie tat es,
+ ab dem Waehlen. Wer 30 Sekunden klingeln liess und 12
+ Sekunden sprach, las im Kasten "00:42" und danach im Chat
+ "Anruf . 12 Sekunden". Zwei Zahlen fuer dieselbe Sache.
+
+ Jetzt beginnt sie beim Rangehen. Solange es klingelt, ist
+ das Feld leer -- und GENAU DAS wird hier geprueft, statt
+ die Pruefung einfach zu streichen: Eine Zeile weniger
+ haette denselben Fehler beim naechsten Umbau wieder
+ durchgelassen. */
+ ok(kasten.zeit === "" || kasten.zeit === "–",
+ `waehrend es klingelt zaehlt die Uhr NICHT ("${kasten.zeit}")`);
/* Und es klingelt hoerbar. Waere der Ton wieder ausgebaut,
stuende hier 0. */
@@ -979,7 +1026,13 @@ melde("=== 8. Zu dritt ===");
const k = document.getElementById("anruf-hoeren");
return k ? { text: k.textContent.trim(), an: k.getAttribute("aria-pressed") } : null;
});
- ok(knopf?.text === "Lautsprecher an" && knopf?.an === "true",
+ /* DER ZUSTAND STEHT IN aria-pressed, NICHT MEHR IM TEXT
+ (geaendert 19.09.2026). Die Aufschrift "Lautsprecher an" war
+ zweideutig -- "ist an" oder "schalt an"? Jetzt traegt das
+ durchgestrichene Zeichen den Zustand und das Wort nur die
+ Sache. Geprueft wird deshalb aria-pressed; der Text bleibt
+ gleich und beweist nichts mehr. */
+ ok(knopf?.an === "true",
`der Lautsprecher-Knopf ist da und steht auf an („${knopf?.text}")`);
await a.se.click("#anruf-hoeren");
@@ -991,8 +1044,8 @@ melde("=== 8. Zu dritt ===");
const k = document.getElementById("anruf-hoeren");
return { text: k.textContent.trim(), an: k.getAttribute("aria-pressed") };
});
- ok(knopf2.text === "Lautsprecher aus" && knopf2.an === "false",
- `und der Knopf sagt es auch („${knopf2.text}")`);
+ ok(knopf2.an === "false",
+ `und der Knopf steht auf aus (aria-pressed="${knopf2.an}")`);
/* NUR BEI IHM. Ein Schalter, der bei den anderen mitwirkt,
waere kein Lautsprecher, sondern eine Fernbedienung. */
diff --git a/server/pruef-turn-wege.mjs b/server/pruef-turn-wege.mjs
new file mode 100644
index 00000000..bee540c4
--- /dev/null
+++ b/server/pruef-turn-wege.mjs
@@ -0,0 +1,142 @@
+/* =====================================================================
+ pruef-turn-wege.mjs — Es klingelt, aber es verbindet nicht.
+
+ Filipe, 19.09.2026: „es klingelt erscheint auch alles bei jedem aber
+ telefonieren klappt immer noch nicht was läuft ich will es heute an
+ die leute bringen also los."
+
+ ---------------------------------------------------------------------
+ WAS VORHER GEMESSEN WURDE — UND WARUM ALLES GRÜN WAR
+
+ Der Reihe nach, bevor eine Zeile Code angefasst wurde:
+
+ coturn läuft aktiv
+ STUN von außen antwortet, nennt die eigene Adresse
+ echte Zuteilung von außen 7/7, das Testpaket kam an
+ Dreier-Anruf im Prüflauf verbindet, alle hören alle
+
+ Vier Messungen, vier Mal in Ordnung — und das Telefon ging trotzdem
+ nicht. Das ist die unangenehmste Lage überhaupt, und sie hat genau
+ einen Grund: Der Prüflauf fährt BEIDE Seiten auf demselben Rechner.
+ Dort finden sie sich über die direkte Adresse, und die Vermittlung
+ wird nie gebraucht. Draußen sitzen sie in verschiedenen Netzen.
+
+ ---------------------------------------------------------------------
+ DER FUND
+
+ Eingetragen war eine einzige Adresse: `turn:159.195.212.167:3478`.
+
+ Ein `turn:` OHNE `?transport=` heißt **UDP**, und nur UDP. Gemessen
+ hört coturn aber auf beidem — 3478/UDP offen, 3478/TCP offen.
+ Angeboten wurde nur der halbe Server.
+
+ Wer in einem Netz sitzt, das UDP nach draußen sperrt (Mobilfunk mit
+ strengem Profil, Gast-WLAN, Firmennetz), bekommt damit KEINEN
+ Vermittlungsweg. Es klingelt — das läuft über die Website, also über
+ 443 — und danach passiert nichts.
+
+ ---------------------------------------------------------------------
+ WAS DIESE PRÜFUNG SICHERT
+
+ Nicht „gibt es einen TURN-Server" — das war schon vorher grün und hat
+ nichts genützt. Sondern: **Kommt aus einem eingetragenen Eintrag
+ wirklich jeder Weg heraus, und trägt jeder davon Zugangsdaten?**
+
+ Der zweite Teil ist der wichtigere. Würde aufgefächert, ohne danach
+ anzumelden, hätten zwei von drei Wegen kein Passwort — coturn weist
+ sie ab, und wir hätten den Fehler nur verschoben.
+
+ AUFRUF: node server/pruef-turn-wege.mjs
+ Läuft in unter einer Sekunde, ohne Server, ohne Netz.
+ ===================================================================== */
+
+import { writeFileSync, unlinkSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { wegeAusfaechern, mitZugangsdaten, turnZugang } from "./workspace-turn.js";
+
+let ok = 0, fehl = 0;
+const pruef = (name, bedingung, zusatz = "") => {
+ if (bedingung) { ok++; console.log(` OK ${name}`); }
+ else { fehl++; console.log(` FEHL ${name}${zusatz ? " — " + zusatz : ""}`); }
+};
+
+console.log("\n=== Vermittlungswege: kommt jeder heraus, und trägt jeder ein Passwort? ===\n");
+
+/* Ein Geheimnis nur für diese Prüfung -- nie das echte. Es wandert in
+ eine Datei, weil mitZugangsdaten aus einer Datei liest; sie wird am
+ Ende wieder entfernt. */
+const DATEI = join(tmpdir(), `turn-pruef-${process.pid}.geheimnis`);
+writeFileSync(DATEI, "x".repeat(48), "utf8");
+
+try {
+ /* ---- 1. Ein schlichter Eintrag wird zu drei Wegen ---------------- */
+ const raus = wegeAusfaechern([{ urls: "turn:beispiel.de:3478" }]);
+ const urls = raus.map((e) => String(e.urls));
+
+ pruef("aus einem Eintrag werden genau drei Wege", raus.length === 3,
+ `es sind ${raus.length}: ${urls.join(", ")}`);
+ pruef("darunter STUN — der Weg, der ohne Vermittlung auskommt",
+ urls.includes("stun:beispiel.de:3478"));
+ pruef("darunter der schnelle Weg (UDP)",
+ urls.includes("turn:beispiel.de:3478?transport=udp"));
+ pruef("darunter der Weg durch fast jede Sperre (TCP)",
+ urls.includes("turn:beispiel.de:3478?transport=tcp"),
+ "GENAU DIESER hat gefehlt — deshalb ging es nicht");
+ pruef("STUN steht vorn, TCP hinten", /^stun:/.test(urls[0]) && /transport=tcp$/.test(urls[2]),
+ urls.join(" → "));
+
+ /* ---- 2. Und jeder turn:-Weg bekommt Zugangsdaten ----------------- */
+ const { adressen, turn_gesamt, turn_bereit } =
+ mitZugangsdaten(raus, "w1", Date.now(), DATEI);
+
+ pruef("beide turn:-Wege werden gezählt", turn_gesamt === 2, `gezählt: ${turn_gesamt}`);
+ /* DIE ZAHL GEHOERT IN DIE BEDINGUNG, nicht nur in den Meldetext:
+ Bei einer leeren Liste waere "alle haben ein Passwort" sonst
+ trivialerweise wahr. */
+ pruef("und beide haben auch eins", turn_bereit === 2 && turn_gesamt === 2,
+ `bereit: ${turn_bereit} von ${turn_gesamt}`);
+
+ const mitPasswort = adressen.filter(
+ (e) => /^turns?:/.test(String(e.urls)) && e.credential);
+ pruef("nachgezählt an der ausgelieferten Liste", mitPasswort.length === 2,
+ `${mitPasswort.length} von 2`);
+ pruef("STUN bekommt KEINS — es kennt keine Anmeldung",
+ !adressen.find((e) => /^stun:/.test(String(e.urls)))?.credential);
+
+ /* ---- 3. Was ausdrücklich gemeint war, bleibt unangetastet -------- */
+ const fremd = wegeAusfaechern([
+ { urls: "turn:eigen.de:3478?transport=tcp" },
+ { urls: "stun:schon.da:3478" },
+ { urls: "turn:fremder-dienst.de:3478", username: "a", credential: "b" },
+ ]);
+ pruef("ein Eintrag MIT transport bleibt, wie er ist",
+ fremd.length === 3 && fremd.some((e) => e.urls === "turn:eigen.de:3478?transport=tcp"));
+ pruef("ein fremder Dienst mit eigenem Passwort wird nicht angefasst",
+ fremd.some((e) => e.urls === "turn:fremder-dienst.de:3478" && e.credential === "b"));
+
+ /* ---- 4. Die Zugangsdaten sind zeitlich begrenzt ------------------ */
+ const z = turnZugang("w1", "x".repeat(48), Date.parse("2026-09-19T12:00:00Z"));
+ pruef("der Benutzername trägt seinen Ablauf", /^\d+:w1$/.test(z.username), z.username);
+ pruef("und er läuft wirklich ab", z.ablauf > Math.floor(Date.parse("2026-09-19T12:00:00Z") / 1000));
+
+ /* ---- DIE GEGENPROBEN ---------------------------------------------
+ Eine Prüfung, die immer bestätigt, bestätigt nichts. */
+ console.log("\n --- Gegenproben (jede MUSS anschlagen) ---");
+
+ const ohneTcp = wegeAusfaechern([{ urls: "turn:x:1" }])
+ .filter((e) => !/transport=tcp/.test(String(e.urls)));
+ pruef("Gegenprobe: fehlte der TCP-Weg, schlüge Prüfung 1 an",
+ !ohneTcp.some((e) => /transport=tcp/.test(String(e.urls))));
+
+ const ohneGeheimnis = mitZugangsdaten(raus, "w1", Date.now(),
+ join(tmpdir(), "gibt-es-nicht-" + process.pid));
+ pruef("Gegenprobe: ohne Geheimnis meldet die Rechnung 0 bereit und nennt den Grund",
+ ohneGeheimnis.turn_bereit === 0 && !!ohneGeheimnis.grund,
+ `bereit ${ohneGeheimnis.turn_bereit}, Grund "${ohneGeheimnis.grund}"`);
+} finally {
+ try { unlinkSync(DATEI); } catch { /* egal */ }
+}
+
+console.log(`\n ${ok} bestanden, ${fehl} fehlgeschlagen\n`);
+process.exit(fehl ? 1 : 0);
diff --git a/server/workspace-anruf.js b/server/workspace-anruf.js
index 1614dd14..cbe115d8 100644
--- a/server/workspace-anruf.js
+++ b/server/workspace-anruf.js
@@ -73,7 +73,7 @@ import {
} from "./workspace.js";
import { chatEreignis, istDrinFuerAnruf, teilnehmerFuerAnruf } from "./workspace-chat.js";
import { darfTelefonieren } from "./workspace-treffchat.js";
-import { mitZugangsdaten } from "./workspace-turn.js";
+import { mitZugangsdaten, wegeAusfaechern } from "./workspace-turn.js";
export const anrufRouter = express.Router();
@@ -324,8 +324,23 @@ anrufRouter.get("/workspace/api/anruf/adressen", (req, res) => {
Als Kennung die PERSONENNUMMER, nicht der Name -- sie landet im
Protokoll von coturn, und das ist kein Ort fuer Klarnamen. */
+ /* ERST AUFFAECHERN, DANN ANMELDEN (19.09.2026).
+
+ Ein eingetragenes `turn:host:port` ohne `?transport=` heisst
+ UDP -- und nur UDP. Wer in einem Netz sitzt, das UDP nach
+ draussen sperrt, bekommt damit keinen Vermittlungsweg: Es
+ klingelt (das laeuft ueber 443), und danach passiert nichts.
+
+ Nachgemessen hoert coturn auf beidem. Aus einem Eintrag werden
+ deshalb drei Wege -- stun, turn/udp, turn/tcp. Die Reihenfolge
+ ist Absicht, siehe workspace-turn.js.
+
+ VOR mitZugangsdaten, nicht danach: Die neuen turn:-Eintraege
+ brauchen dieselben gerechneten Zugangsdaten wie der Eintrag, aus
+ dem sie entstanden sind. Andersherum waeren zwei von drei ohne
+ Passwort -- also genau der Zustand, den wir beheben wollen. */
const { adressen, turn_gesamt, turn_bereit, grund } =
- mitZugangsdaten(liste, `w${req.person.id}`);
+ mitZugangsdaten(wegeAusfaechern(liste), `w${req.person.id}`);
if (grund) {
/* DER DRITTE AUSGANG. „Kann nicht nachsehen" ist etwas anderes
diff --git a/server/workspace-turn.js b/server/workspace-turn.js
index da6def1b..3e9801f7 100644
--- a/server/workspace-turn.js
+++ b/server/workspace-turn.js
@@ -181,3 +181,105 @@ export function mitZugangsdaten(liste, kennung, jetzt = Date.now(),
grund: "",
};
}
+
+/* =====================================================================
+ EIN EINTRAG, DREI WEGE — 19.09.2026
+
+ Filipe: „es klingelt erscheint auch alles bei jedem aber telefonieren
+ klappt immer noch nicht."
+
+ ---------------------------------------------------------------------
+ WAS GEMESSEN WURDE, BEVOR ETWAS GEÄNDERT WURDE
+
+ Der Reihe nach, und jeder Schritt war grün:
+
+ coturn läuft aktiv
+ STUN von außen antwortet, nennt meine Adresse
+ echte Zuteilung von außen 7 von 7 Prüfungen, Paket kam an
+ Dreier-Anruf im Prüflauf verbindet, alle hören alle
+
+ Es war also weder der Server noch die Logik. Der Unterschied zwischen
+ Prüflauf und Wirklichkeit ist das NETZ: Im Prüflauf sitzen beide
+ Seiten auf demselben Rechner und finden sich über die direkte
+ Adresse. Draußen sitzen sie in verschiedenen Netzen.
+
+ ---------------------------------------------------------------------
+ DER FUND
+
+ Eingetragen war genau eine Adresse:
+
+ turn:159.195.212.167:3478
+
+ Ein `turn:` OHNE `?transport=` heißt **UDP**. Nachgemessen hört
+ coturn aber auf beidem: 3478/UDP offen, 3478/TCP offen (5349/TLS ist
+ zu). Angeboten wurde nur der UDP-Weg.
+
+ Wer in einem Netz sitzt, das UDP nach draußen sperrt — Mobilfunk mit
+ strengem Profil, Gast-WLAN, Firmennetz, manche Hotspots —, bekommt
+ damit KEINEN Vermittlungsweg. Es klingelt (das läuft über die
+ Website, also über 443), und danach passiert nichts. Genau das
+ Bild, das Filipe beschreibt.
+
+ ---------------------------------------------------------------------
+ WARUM ABGELEITET UND NICHT EINGETRAGEN
+
+ Man könnte die drei Zeilen von Hand in die Einstellung schreiben.
+ Dann stünden dort drei Adressen, die denselben Host meinen, und beim
+ nächsten Serverumzug müsste jemand an drei Stellen denken — die
+ abgeschriebene Liste, die dieses Haus schon zweimal teuer bezahlt
+ hat.
+
+ Stattdessen: Aus einem eingetragenen `turn:host:port` entstehen
+ automatisch
+
+ stun:host:port damit jeder seine öffentliche
+ Adresse findet (das ist der Weg,
+ der in den meisten Fällen reicht
+ und KEINE Vermittlung kostet)
+ turn:host:port?transport=udp der schnelle Weg
+ turn:host:port?transport=tcp der Weg, der durch fast jede
+ Sperre kommt
+
+ Wer eine Adresse MIT `?transport=` einträgt, meint sie genau so —
+ die bleibt unangetastet. Und ein fremder Dienst mit eigenem Passwort
+ sowieso (siehe mitZugangsdaten).
+
+ DIE REIHENFOLGE IST ABSICHT: STUN zuerst, dann UDP, dann TCP. Der
+ Browser probiert alle gleichzeitig und nimmt den ersten, der trägt —
+ aber bei Gleichstand gewinnt der frühere, und TCP-Vermittlung ist
+ der langsamste der drei.
+ ===================================================================== */
+
+/** Aus jedem schlichten `turn:`-Eintrag drei Wege machen.
+ *
+ * @param liste wie eingetragen
+ * @returns {object[]} die erweiterte Liste
+ */
+export function wegeAusfaechern(liste) {
+ const rein = Array.isArray(liste) ? liste : [];
+ const raus = [];
+ const schon = new Set();
+ const dazu = (e) => {
+ const s = String(e?.urls || "");
+ if (!s || schon.has(s)) return;
+ schon.add(s);
+ raus.push(e);
+ };
+
+ for (const e of rein) {
+ const url = String(e?.urls || "");
+ const m = /^turn:([^?]+)$/.exec(url);
+ /* Kein schlichter turn:-Eintrag -- unverändert übernehmen. Das
+ trifft stun:, turns:, alles mit ?transport= und jeden fremden
+ Dienst mit eigenen Zugangsdaten. */
+ if (!m || e?.credential) { dazu(e); continue; }
+
+ const ziel = m[1];
+ /* STUN braucht keine Anmeldung und bekommt deshalb auch keine --
+ mitZugangsdaten lässt stun:-Einträge ohnehin in Ruhe. */
+ dazu({ urls: `stun:${ziel}` });
+ dazu({ ...e, urls: `turn:${ziel}?transport=udp` });
+ dazu({ ...e, urls: `turn:${ziel}?transport=tcp` });
+ }
+ return raus;
+}
diff --git a/server/workspace-werdegang.js b/server/workspace-werdegang.js
index 8673b571..8029c79a 100644
--- a/server/workspace-werdegang.js
+++ b/server/workspace-werdegang.js
@@ -104,7 +104,7 @@
import express from "express";
import {
- db, sitzungLesen, fuehrtTeamDogi, ROLLEN_NAME,
+ db, sitzungLesen, fuehrtTeamDogi, ROLLEN_NAME, ROLLEN_SORTIERUNG,
} from "./workspace.js";
import {
ENTWICKLUNG_BLOECKE, ENTWICKLUNG_PUNKTE, ENTWICKLUNG_STUFEN,
@@ -587,10 +587,26 @@ werdegangRouter.use("/workspace/api/werdegang", angemeldet);
------------------------------------------------------------------- */
werdegangRouter.get("/workspace/api/werdegang/liste", nurLeitung, (req, res) => {
try {
+ /* NACH ROLLE, DANN NACH NAMEN (19.09.2026).
+
+ Filipe: „die reihenfolge der listen soll immer angepasst sein
+ hatten wir doch schon. die rechte hand rolle immer zuerst und
+ dan die modis."
+
+ Vorher stand hier nur `ORDER BY name` mit der Begründung, eine
+ Sortierung nach Aktivität wäre eine Rangliste mit anderem Namen.
+ Das gilt weiter — aber eine Sortierung nach ROLLE ist keine:
+ Sie sagt nichts darüber, wie gut jemand ist, sondern nur, welche
+ Aufgabe er hat. Genau dieselbe Reihenfolge benutzt das Haus in
+ `ROLLEN_SORTIERUNG` schon überall sonst.
+
+ ABGELEITET, NICHT ABGESCHRIEBEN: Die Reihenfolge kommt aus
+ derselben Konstante wie in der Personenliste. Wer dort eine
+ Rolle verschiebt, verschiebt sie hier mit. */
const leute = db().prepare(
`SELECT id, name, rolle FROM personen
WHERE aktiv = 1 AND rolle IN (${KARTEN_ROLLEN.map(() => "?").join(",")})
- ORDER BY name COLLATE NOCASE`).all(...KARTEN_ROLLEN);
+ ORDER BY ${ROLLEN_SORTIERUNG}, name COLLATE NOCASE`).all(...KARTEN_ROLLEN);
const spalten = monatsspalten();
const liste = leute.map((p) => {
@@ -604,6 +620,11 @@ werdegangRouter.get("/workspace/api/werdegang/liste", nurLeitung, (req, res) =>
return {
id: p.id,
name: p.name,
+ /* DIE ROLLE SELBST KOMMT MIT, nicht nur ihr Name: Die
+ Oberfläche gruppiert danach und zeichnet die Leitung anders.
+ Über den NAMEN zu gruppieren wäre die Stelle, die beim
+ nächsten Umbenennen auseinanderfällt. */
+ rolle: p.rolle,
rolle_name: ROLLEN_NAME[p.rolle] ?? p.rolle,
gesetzt: stand.length,
gesamt: ENTWICKLUNG_FREMD.length,
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index c745c28a..e0d6e57a 100644
--- a/workspace/anruf-probe.html
+++ b/workspace/anruf-probe.html
@@ -16,15 +16,15 @@
verdeckt. Gefunden hat es pruef-struktur.
Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. -->
-
-
+
+
-
+