Files
dogfather-universe/workspace/assets/js/rundgang.js
T
DogFatherGitandClaude Opus 5 f09a0c3456 Rundgang: drei Schritte fuer die Community, fuenf fuers Team
Bauplan Kapitel 10 verlangt "Community drei Schritte, Team-Rollen
fuenf Schritte (zusaetzlich 'Deine Aufgaben' und die Gruppen ...)".
Gemessen hatte die Community bisher FUENF -- sie bekam auch die beiden
Arbeitslisten gezeigt, "Was ist dran" und "Deine Aufgaben". Auf ihrer
Zentrale stehen beide Kaesten zwar, aber leer; eine Station, die vor
einem leeren Kasten erklaert, was dort sonst steht, ist schlechter als
keine.

WO DIE ENTSCHEIDUNG LIEGT, UND WARUM NICHT IN rundgang.js:
Der Server schickt mit der Einweisung jetzt `team: true/false`
(abgeleitet aus AUSSEN_ROLLEN, nicht aus einer zweiten Liste von
Rollennamen). `rundgang.js` siebt damit und kennt weiterhin keinen
einzigen Rollennamen. Haette ich die Rollen dort hineingeschrieben,
waere jede spaeter dazukommende Rolle stillschweigend eine Teamrolle --
und niemand kaeme auf die Idee, in einer Datei ueber Begruessungen
nach Rechten zu suchen.

ZWEI SIEBE, UND BEIDE SIND NOETIG: `nurTeam` nimmt der Community die
Arbeitslisten, `querySelector` nimmt jedem das, was auf SEINER
Zentrale gar nicht steht. Ohne das zweite bliebe der Rundgang vor
einem Kasten stehen, den es nicht gibt.

Dazu zwei Kleinigkeiten, beide beim Nachmessen aufgefallen:
  * Die Begruessung sagte "In fuenf Minuten"; im Bauplan steht "In
    zwei Minuten". Fuenf Minuten sind eine Ankuendigung, die abschreckt
    -- und bei drei Schritten auch nicht wahr.
  * Ohne Namen stand dort "Willkommen im Creator Workspace". Dieselbe
    Datei laeuft auf beiden Adressen; bei Team Dogi war das das
    falsche Haus. Jetzt nur "Willkommen!" -- kein Haus zu nennen ist
    besser als das falsche.

GEPRUEFT (pruef-anleitung 137 -> 148):
Die Zaehlung wird nicht nachgebildet, sondern AUSGEFUEHRT: Die
Stationsliste und der Siebausdruck werden aus der echten Datei geholt
und laufen zweimal, mit team=false und team=true. Ein Muster ("steht
da istTeam?") waere gruen, sobald das Wort in einem Kommentar steht --
genau der Fall, der am 04.10. bei pruef-anruf-klingelt aufgefallen ist.
Gegenprobe gefahren: Sieb entfernt -> drei FEHL, Anzahl bleibt 148
(kein stilles Ueberspringen). Dazu eine Zeile, die festhaelt, dass in
rundgang.js kein Rollenname steht, auf dem Code ohne Kommentare.

Im Browser nachgemessen (mess-anleitung-crew, echte Anmeldung, 390 px,
HTTPS-Vorbau wegen HSTS): Community 3 Stationen, Rechte Hand 5, beide
mit der neuen Begruessung.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-06 16:52:51 +02:00

295 lines
12 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.
/* =====================================================================
rundgang.js — Begrüßung und Rundgang beim ersten Anmelden (Etappe 6)
Bauplan Kapitel 10. Ein Rundgang über die Zentrale — für die
Community drei Schritte, für die Teamrollen fünf (zusätzlich die
Arbeitslisten). Im letzten leuchten nur die Muss-Kacheln, am Ende
steht man auf der Anleitung bei den Ersten Schritten.
Welche der beiden Längen gilt, sagt der SERVER (`team: true/false`)
— hier steht absichtlich kein einziger Rollenname. Sonst wäre jede
neue Rolle, die irgendwann dazukommt, stillschweigend eine
Teamrolle, und niemand käme auf die Idee, in dieser Datei
nachzusehen.
---------------------------------------------------------------------
ER DARF DIE SEITE UNTER KEINEN UMSTÄNDEN BLOCKIEREN
Das ist die wichtigste Eigenschaft dieser Datei, und sie ist der
Grund, warum sie eine eigene ist. Sie läuft auf `start.html` —
der meistbenutzten Seite des Hauses, für jede Rolle, bei jedem
Aufruf. Geht hier etwas schief, darf davon nichts anderes betroffen
sein.
Deshalb:
* eigene Datei, eigenes `defer` — kein Platz in start.js, wo ein
Fehler die Kacheln mitnähme;
* alles in `try`, und der Fehlerfall ist „nichts tun";
* der Server antwortet IMMER mit 200 (notfalls `zeigen: false`),
damit in der Konsole nichts Rotes steht;
* wird ein Zielbereich nicht gefunden, wird der Schritt
übersprungen statt auf ihn zu warten. Eine Einweisung, die auf
ein Element wartet, das es nicht gibt, ist genau die Sorte
Stillstand, die schlimmer ist als ein Fehlschlag.
---------------------------------------------------------------------
ER STARTET GENAU EINMAL JE PERSON UND ROLLE
Gemerkt wird das serverseitig (`anl_einweisung`), nicht im Browser:
Wer sich am Telefon anmeldet und später am Rechner, hat sie schon
gesehen. Und weil der Schlüssel (Person, Rolle) ist, startet sie
nach einem Rollenwechsel von selbst noch einmal — ohne dass jemand
daran denken muss.
===================================================================== */
(() => {
'use strict';
const BASIS = '/workspace/api/anleitung';
const el = (tag, klasse, text) => {
const k = document.createElement(tag);
if (klasse) k.className = klasse;
if (text !== undefined) k.textContent = text;
return k;
};
/* Die fünf Stationen. Wird ein Ziel nicht gefunden, fällt der
Schritt weg — die Zentrale sieht je nach Rolle anders aus, und
eine Einweisung, die auf einem fehlenden Kasten stehen bleibt,
wäre schlimmer als eine um einen Schritt kürzere. */
const STATIONEN = [
{ wo: '.willkommen', titel: 'Deine Zentrale',
text: 'Hier siehst du auf einen Blick, wie dein Tag steht: der Ring, '
+ 'der nächste Termin und was offen ist.' },
/* NUR FUERS TEAM (Bauplan Kapitel 10). Die Community bekommt drei
Stationen, die Teamrollen fuenf -- "zusaetzlich Deine Aufgaben"
und die Arbeitslisten. Wer nur mitliest, hat hier nichts
stehen, und eine Station vor einem leeren Kasten erklaert
nichts. */
{ wo: '#dran-seite', nurTeam: true, titel: 'Was ist dran',
text: 'Die wichtigste Liste des Hauses. Was hier steht, will etwas von dir – '
+ 'und es verschwindet von selbst, sobald es erledigt ist.' },
{ wo: '#heute-seite', titel: 'Heute',
text: 'Deine Termine für heute. Steht nichts da, steht wirklich nichts an.' },
{ wo: '#zahlen-block', nurTeam: true, titel: 'Deine Aufgaben',
text: 'Offen, in Arbeit, zur Freigabe, erledigt. Ein Tippen bringt dich '
+ 'ins Aufgabenbrett.' },
{ wo: '#bereiche', titel: 'Mit diesen fängst du an',
text: 'Alles andere kannst du erst einmal liegen lassen. Die Anleitung '
+ 'sagt dir zu jeder Kachel, wofür sie da ist.', nurMuss: true },
];
let schritt = 0;
let stationen = [];
let muss = [];
/* Teamrolle oder Community? Kommt vom Server als Ja/Nein -- ein
Rollenname stuende hier falsch (siehe den Kopf dieser Datei). */
let istTeam = true;
let schale = null;
/* ---------- Das Hervorheben ---------------------------------------
Kein Loch in einer Abdeckung (das müsste bei jedem Scrollen neu
gerechnet werden), sondern ein Rahmen um das Ziel plus ein
Merkmal am Element. Die Abdunklung macht CSS über
`body[data-rundgang]` — damit bleibt die Seite benutzbar, falls
dieses Skript mittendrin stirbt: Ein Attribut am body ist weg,
sobald die Seite neu lädt. */
function hervorheben(ziel) {
for (const e of document.querySelectorAll('[data-rundgang-ziel]')) {
e.removeAttribute('data-rundgang-ziel');
}
for (const e of document.querySelectorAll('[data-rundgang-muss]')) {
e.removeAttribute('data-rundgang-muss');
}
if (ziel) ziel.setAttribute('data-rundgang-ziel', 'ja');
}
/* Nur die Muss-Kacheln leuchten lassen. Der Schlüssel kommt aus
derselben Funktion, mit der auch die Anleitung ihre Karten
verbindet — eine zweite Rechnung hier wäre die, die beim nächsten
Fragezeichen in einer Adresse danebenliegt. */
function mussMarkieren() {
const B = window.Bereiche;
if (!B || typeof B.schluesselVonZiel !== 'function') return 0;
let n = 0;
for (const a of document.querySelectorAll('#bereiche .kachel__link')) {
const s = B.schluesselVonZiel(a.getAttribute('href') || '');
if (muss.includes(s)) {
a.closest('.kachel')?.setAttribute('data-rundgang-muss', 'ja');
n++;
}
}
return n;
}
function zeichnen() {
const st = stationen[schritt];
if (!st) { beenden(true); return; }
const ziel = document.querySelector(st.wo);
hervorheben(ziel);
if (st.nurMuss) mussMarkieren();
schale.querySelector('.rg__zaehler').textContent =
`Schritt ${schritt + 1} von ${stationen.length}`;
schale.querySelector('.rg__titel').textContent = st.titel;
schale.querySelector('.rg__text').textContent = st.text;
schale.querySelector('.rg__weiter').textContent =
schritt === stationen.length - 1 ? 'Zur Anleitung' : 'Weiter';
/* Sanft hinscrollen, aber nur, wenn jemand das möchte. */
try {
ziel?.scrollIntoView({
behavior: matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth',
block: 'center',
});
} catch { /* ältere Browser: dann eben ohne */ }
}
async function gesehenMerken() {
try {
await fetch(`${BASIS}/einweisung/gesehen`, {
method: 'POST', credentials: 'same-origin',
});
} catch { /* Beim nächsten Mal eben noch einmal -- kein Schaden. */ }
}
function beenden(zurAnleitung) {
document.body.removeAttribute('data-rundgang');
hervorheben(null);
schale?.remove();
schale = null;
gesehenMerken().finally(() => {
if (zurAnleitung) location.assign('anleitung.html#schritte');
});
}
function rundgangStarten() {
/* ZWEI SIEBE, UND BEIDE SIND NOETIG:
`nurTeam` nimmt der Community die Arbeitslisten (Bauplan
Kapitel 10), `querySelector` nimmt jedem das, was auf SEINER
Zentrale gar nicht steht. Ohne das zweite bliebe die Einweisung
vor einem Kasten stehen, den es nicht gibt. */
stationen = STATIONEN
.filter((s) => (s.nurTeam ? istTeam : true))
.filter((s) => document.querySelector(s.wo));
if (!stationen.length) { beenden(false); return; }
schritt = 0;
document.body.dataset.rundgang = 'ja';
schale = el('div', 'rg');
schale.setAttribute('role', 'dialog');
schale.setAttribute('aria-modal', 'false');
schale.setAttribute('aria-label', 'Rundgang');
const kasten = el('div', 'rg__kasten');
kasten.append(el('p', 'rg__zaehler'));
kasten.append(el('h2', 'rg__titel'));
kasten.append(el('p', 'rg__text'));
const knoepfe = el('div', 'rg__knoepfe');
const abbruch = el('button', 'rg__still', 'Abbrechen');
abbruch.type = 'button';
abbruch.addEventListener('click', () => beenden(false));
const weiter = el('button', 'rg__weiter knopf', 'Weiter');
weiter.type = 'button';
weiter.addEventListener('click', () => { schritt++; zeichnen(); });
knoepfe.append(abbruch, weiter);
kasten.append(knoepfe);
schale.append(kasten);
document.body.append(schale);
/* Escape beendet -- wie bei jedem Fenster im Haus. */
document.addEventListener('keydown', function esc(e) {
if (e.key !== 'Escape' || !schale) return;
document.removeEventListener('keydown', esc);
beenden(false);
});
zeichnen();
weiter.focus();
}
/* ---------- Die Begrüßung ------------------------------------------ */
function begruessen(d) {
const fenster = el('dialog', 'dialog rg-gruss');
const rumpf = el('div', 'rg-gruss__rumpf');
rumpf.append(el('h2', 'dialog__titel',
/* OHNE NAMEN BEWUSST OHNE HAUSNAMEN: Dieselbe Datei läuft auf
beiden Adressen. „Willkommen im Creator Workspace" wäre bei
Team Dogi das falsche Haus, und ein Haus zu nennen, das man
nicht kennt, ist schlimmer als keins zu nennen. */
d.name ? `Willkommen, ${d.name}!` : 'Willkommen!'));
rumpf.append(el('p', 'rg-gruss__text',
'Schön, dass du da bist. In zwei Minuten zeige ich dir, '
+ 'was hier für dich wichtig ist.'));
const knoepfe = el('div', 'dialog__knoepfe');
const spaeter = el('button', 'knopf-still', 'Später');
spaeter.type = 'button';
const los = el('button', 'knopf', 'Los geht’s');
los.type = 'button';
knoepfe.append(spaeter, los);
rumpf.append(knoepfe);
fenster.append(rumpf);
document.body.append(fenster);
/* „SPÄTER" IST ERLAUBT und zählt trotzdem als gezeigt. Ein
Fenster, das so lange wiederkommt, bis man das Richtige
anklickt, ist keine Einladung. Dass noch etwas offen ist, sagt
danach der Hinweis „Erste Schritte noch offen" — der ist leiser
und steht da, bis er erledigt ist. */
spaeter.addEventListener('click', () => {
fenster.close();
fenster.remove();
gesehenMerken();
});
los.addEventListener('click', () => {
fenster.close();
fenster.remove();
rundgangStarten();
});
/* GEMERKT WIRD BEIM ZEIGEN, NICHT BEIM BEENDEN (06.10.2026).
Zuerst stand das nur in `beenden()`. Gemessen mit
mess-anleitung: Wer "Los geht's" drueckt und dann weggeht --
Seite gewechselt, Telefon weggelegt, Fenster zu --, hat den
Rundgang nie beendet. Beim naechsten Aufruf kam die Begruessung
wieder. Und beim uebernaechsten. Fuer immer.
Die Tatsache, die gespeichert werden soll, ist "sie wurde
gezeigt" -- und die steht in dem Moment fest, in dem das
Fenster aufgeht. Alles andere ist eine Bedingung, die der
Mensch erfuellen muss, damit ihn die Software in Ruhe laesst. */
gesehenMerken();
fenster.showModal();
los.focus();
}
/* ---------- Start --------------------------------------------------- */
async function pruefen() {
let a;
try { a = await fetch(`${BASIS}/einweisung`, { credentials: 'same-origin' }); }
catch { return; }
if (!a.ok) return;
let d;
try { d = await a.json(); } catch { return; }
muss = Array.isArray(d.muss) ? d.muss : [];
istTeam = d.team !== false;
/* Der Knopf „Rundgang noch einmal starten" steht auf der
Anleitung; von dort kommt man mit `#rundgang` zurück. Das geht
auch, wenn die Einweisung längst gezeigt wurde — sonst wäre
der Knopf dort einer, der nichts tut. */
if (location.hash === '#rundgang') {
history.replaceState(null, '', location.pathname);
rundgangStarten();
return;
}
if (d.zeigen) begruessen(d);
}
/* Erst wenn die Kacheln stehen: Der letzte Schritt leuchtet sie an,
und vorher gibt es sie nicht. Zwei Sekunden sind gemessen reichlich
(die Kacheln stehen nach rund 300 ms) -- und falls doch nicht,
fällt nur die Markierung weg, nicht der Rundgang. */
if (document.readyState === 'complete') setTimeout(pruefen, 1200);
else window.addEventListener('load', () => setTimeout(pruefen, 1200));
})();