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]>
295 lines
12 KiB
JavaScript
295 lines
12 KiB
JavaScript
/* =====================================================================
|
||
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));
|
||
})();
|