diff --git a/server/pruef-schritt.mjs b/server/pruef-schritt.mjs index daf00a8b..b733fc58 100644 --- a/server/pruef-schritt.mjs +++ b/server/pruef-schritt.mjs @@ -356,7 +356,8 @@ melde("=== 8. Im Browser ==="); offen: b.filter((x) => x.dataset.offen === "ja").length, schalter: document.querySelectorAll(".e-block__schalter").length, staende: [...document.querySelectorAll(".e-block__stand")].map((x) => x.textContent.trim()), - hakt: [...document.querySelectorAll(".e-block__hakt")].map((x) => x.textContent.trim()), + hakt: [...document.querySelectorAll(".e-block__hakt")] + .filter((x) => !x.hidden).map((x) => x.textContent.trim()), versteckt: b.filter((x) => x.querySelector(".e-block__koerper")?.hidden).length, alleKnopf: document.querySelector(".e-bloecke__alle")?.textContent.trim() || "", }; @@ -372,8 +373,11 @@ melde("=== 8. Im Browser ==="); ok(bloecke.staende.length === bloecke.anzahl && bloecke.staende.every((x) => x.includes(' / ')), `jeder Kopf sagt, wie weit er ist (${bloecke.staende.join(", ")})`); - ok(bloecke.hakt.some((x) => /hakt/.test(x)), - `und wo etwas hakt, steht es im Kopf (${bloecke.hakt.join(", ") || "nichts"})`); + /* SICHTBAR, nicht bloß vorhanden. Seit dem 15.09. steht der Hinweis + immer im Baum (damit er nachgezogen werden kann, ohne den Kopf neu + zu bauen) -- „es gibt ihn“ wäre damit über vier versteckten grün. */ + ok(bloecke.hakt.length === 1 && /hakt/.test(bloecke.hakt[0]), + `und NUR wo etwas hakt, steht es im Kopf (${bloecke.hakt.join(", ") || "nirgends"})`); ok(/Alle aufklappen/.test(bloecke.alleKnopf), `daneben einer für alle auf einmal („${bloecke.alleKnopf}“)`); @@ -434,6 +438,96 @@ melde("=== 8. Im Browser ==="); `das Brett sagt, was es zeigt: „${sprung.leiste}“`); ok(sprung.marke === "1", `und es ist genau eine (${sprung.marke})`); + /* ===================================================================== + ZWEI FEHLER, DIE FILIPE GEMELDET HAT (15.09.2026) + + Ein Fehler, der nur behoben ist, ist beim naechsten Umbau wieder da. + Beide werden deshalb nachgestellt, nicht beschrieben. + ===================================================================== */ + + /* ZURÜCK AUF DIE ENTWICKLUNGSSEITE. Der Abschnitt davor ist zum + Aufgabenbrett gesprungen -- mein erster Entwurf maß danach auf dem + Brett weiter und fand dort weder Punkte noch eine Karte. Gemeldet + hat das die Prüfung selbst: „Kachel und Karte nennen dieselbe Zahl + („“ / „“)" -- zwei leere Texte sind gleich und beweisen nichts. + Genau dafür steht die Zahl in der Bedingung daneben. */ + await seite.goto(`http://127.0.0.1:${PORT}/workspace/entwicklung.html`, + { waitUntil: "networkidle" }); + await seite.waitForTimeout(800); + await seite.click('.e-person:has-text("Frida")'); + await seite.waitForTimeout(1000); + await seite.click(".e-bloecke__alle"); + await seite.waitForTimeout(400); + + /* (1) und (2) IN EINEM DURCHGANG -- sie gehören zusammen. + + Filipe hat beides am selben Klick gemerkt: Die Seite sprang hoch, + UND die Zahl auf der Kachel blieb stehen. Also wird auch beides am + selben Klick gemessen: Vorher die Zahl und die Höhe merken, EINMAL + drücken, beides nachsehen. */ + await seite.evaluate(() => window.scrollTo({ top: 600, behavior: "instant" })); + await seite.waitForTimeout(250); + const vorDemKlick = await seite.evaluate(() => { + const name = document.querySelector("#karte .e-karte__name")?.textContent.trim() || ""; + const kachel = [...document.querySelectorAll(".e-person")] + .find((x) => x.querySelector(".e-person__name")?.textContent.trim() === name); + return { + name, + y: Math.round(window.scrollY), + kachel: kachel?.querySelector(".e-person__zahl")?.textContent.trim() || "", + kopf: document.querySelector("#karte .e-karte__stand")?.textContent.trim() || "", + }; + }); + ok(vorDemKlick.name === "Frida", `Fridas Karte ist offen (${vorDemKlick.name})`); + const zahlAus = (t) => Number((t.match(/^(\d+) von/) || [])[1] ?? -1); + ok(zahlAus(vorDemKlick.kachel) >= 0, + `vorher steht auf ihrer Kachel „${vorDemKlick.kachel}“`); + + /* Ein Punkt, der noch NICHT gesetzt ist -- genau der löst den + Neuaufbau aus, und genau da sprang die Seite. */ + const gedrueckt = await seite.evaluate(() => { + const p = [...document.querySelectorAll(".e-punkt")] + .find((x) => !x.dataset.stufe && x.offsetParent); + if (!p) return false; + p.querySelector('.e-wahl__knopf[data-stufe="laeuft"]')?.click(); + return true; + }); + ok(gedrueckt, "ein noch nicht gesetzter Punkt wird angeklickt"); + await seite.waitForTimeout(1400); + + const nachDemKlick = await seite.evaluate(() => { + const name = document.querySelector("#karte .e-karte__name")?.textContent.trim() || ""; + const kachel = [...document.querySelectorAll(".e-person")] + .find((x) => x.querySelector(".e-person__name")?.textContent.trim() === name); + return { + y: Math.round(window.scrollY), + kachel: kachel?.querySelector(".e-person__zahl")?.textContent.trim() || "", + kopf: document.querySelector("#karte .e-karte__stand")?.textContent.trim() || "", + }; + }); + + /* (1) „immer wenn ich auf was drücke dan geht das fenster wieder hoch." */ + ok(Math.abs(nachDemKlick.y - vorDemKlick.y) <= 4, + `die Seite bleibt stehen, wo sie stand (${vorDemKlick.y} -> ${nachDemKlick.y})`); + + /* (2) „wieso steht bei vanvan 0 von 28 obwohl ich alle durch habe." */ + ok(zahlAus(nachDemKlick.kachel) === zahlAus(vorDemKlick.kachel) + 1, + `die Kachel zählt sofort mit (${vorDemKlick.kachel} -> ${nachDemKlick.kachel})`); + ok(nachDemKlick.kachel === nachDemKlick.kopf, + `und nennt dieselbe Zahl wie die Karte („${nachDemKlick.kopf}“)`); + ok(zahlAus(nachDemKlick.kachel) > 0, + "es ist eine echte Zahl, keine Null — sonst wäre die Gleichheit wertlos"); + + /* GEGENPROBE ZUM SPRUNG: Auf eine PERSON zu drücken DARF springen -- + dort will man zur Karte. Ohne diese Zeile hieße „springt nicht“ + vielleicht nur, dass gar nichts mehr scrollt. */ + await seite.evaluate(() => window.scrollTo({ top: 900, behavior: "instant" })); + await seite.waitForTimeout(200); + await seite.evaluate(() => document.querySelector(".e-person")?.click()); + await seite.waitForTimeout(1400); + const beiPerson = await seite.evaluate(() => Math.round(window.scrollY)); + ok(beiPerson !== 900, `beim Öffnen einer Person springt sie sehr wohl (${beiPerson})`); + ok(konsole.length === 0, konsole.length ? "Fehler im Browser: " + konsole[0].slice(0, 90) : "keine Fehler im Browser"); await browser.close(); diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 452ab53b..93581974 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609151438", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609151721", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609151438", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609151721", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609151438", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609151721", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609151438", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609151721", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/entwicklung.css b/workspace/assets/css/entwicklung.css index df2c6293..883fb184 100644 --- a/workspace/assets/css/entwicklung.css +++ b/workspace/assets/css/entwicklung.css @@ -214,6 +214,10 @@ font-variant-numeric: tabular-nums; } .e-block__hakt-wort { font-size: .78rem; } +/* Die Zahl steht in einem eigenen Feld, damit sie nachgezogen + werden kann, ohne den ganzen Kopf neu zu bauen -- ein neu + gebauter Kopf waere wieder eine Bewegung auf der Seite. */ +.e-block__hakt-zahl { font-size: .78rem; font-variant-numeric: tabular-nums; } .e-block__koerper { padding: 0 15px 13px; } .e-block__titel { margin: 0 0 4px; font-size: 1.02rem; font-weight: 650; color: var(--text); } diff --git a/workspace/assets/js/entwicklung.js b/workspace/assets/js/entwicklung.js index 8f98464b..0630ecb0 100644 --- a/workspace/assets/js/entwicklung.js +++ b/workspace/assets/js/entwicklung.js @@ -69,6 +69,62 @@ }; let offen = null; // die gerade geöffnete Person + /* DIE ZAHL AUF DER PERSONENKACHEL LEBT WOANDERS ALS DIE IN DER KARTE + (15.09.2026). + + Filipe: „und wieso steht bei vanvan 0 von 28 obwohl ich alle durch + habe." + + Weil die Kachel aus /entwicklung/lage kommt -- EINMAL, beim Laden + der Seite. Die Karte holt sich ihren Stand bei jedem Öffnen neu. + Wer 28 Punkte durchklickt, sieht in der Karte 28 von 28 und auf der + Kachel darüber weiterhin die Zahl von vorhin. Zwei Wahrheiten auf + einem Bildschirm, und die falsche steht oben. + + Die Liste deshalb NICHT neu zu holen ist Absicht: Jeder Neuaufbau + bewegt die Seite, und genau darüber hat Filipe sich im selben + Atemzug beschwert. Stattdessen wandert die Zahl mit. */ + const personZahlen = new Map(); // Personennummer -> Element mit „x von 28" + let letzteKarte = null; // { id, daten } der gerade offenen Karte + + /** Alle sichtbaren Zähler auf den Stand bringen, den die Karte hat. + * Ohne Neuladen, ohne Sprung. */ + function zaehlerNachziehen() { + if (!letzteKarte) return; + const r = letzteKarte.daten; + const alle = r.bloecke.flatMap((b) => b.punkte); + const gesetzt = alle.filter((pt) => pt.meins).length; + const satz = `${gesetzt} von ${r.gesamt} angesehen`; + + const kopf = document.querySelector('#karte .e-karte__stand'); + if (kopf) kopf.textContent = satz; + const kachel = personZahlen.get(letzteKarte.id); + if (kachel) kachel.textContent = satz; + + /* Und die Köpfe der Kategorien -- sonst steht zugeklappt „0 / 7“ + über sieben beantworteten Fragen. */ + for (const b of r.bloecke) { + const abschnitt = document.querySelector(`.e-block[data-block="${b.schluessel}"]`); + if (!abschnitt) continue; + const n = b.punkte.filter((pt) => pt.meins).length; + const haktHier = b.punkte.filter((pt) => pt.meins?.stufe === 'hakt').length; + const stand = abschnitt.querySelector('.e-block__stand'); + if (stand) { + stand.textContent = `${n} / ${b.punkte.length}`; + if (n === b.punkte.length) stand.dataset.fertig = 'ja'; + else delete stand.dataset.fertig; + } + if (n === b.punkte.length) abschnitt.dataset.fertig = 'ja'; + else delete abschnitt.dataset.fertig; + const hakt = abschnitt.querySelector('.e-block__hakt'); + if (hakt) { + hakt.hidden = !haktHier; + const zahl = hakt.querySelector('.e-block__hakt-zahl'); + if (zahl) zahl.textContent = `${haktHier} ×`; + } + } + } + /* ---- Die vier Knöpfe ------------------------------------------------ Sie stehen immer in derselben Reihenfolge, auch wenn einer gesetzt ist. Ein Knopfsatz, der sich umsortiert, sobald man ihn benutzt, @@ -115,8 +171,12 @@ k.append(el('span', 'e-person__rolle', p.rolle_name)); const zahlen = el('span', 'e-person__zahlen'); - zahlen.append(el('span', 'e-person__zahl', - `${p.gesetzt} von ${lage.gesamt_fremd} angesehen`)); + const wieViele = el('span', 'e-person__zahl', + `${p.gesetzt} von ${lage.gesamt_fremd} angesehen`); + /* Die Kachel merkt sich ihr Zahlenfeld -- daran zieht + zaehlerNachziehen() sie nach, ohne die Liste neu zu holen. */ + personZahlen.set(p.id, wieViele); + zahlen.append(wieViele); /* DIE ABWEICHUNG STEHT VORNE UND NICHT IM DETAIL. Sie ist das Ergebnis dieser Seite: Wo zwei Leute dasselbe sehen, gibt es nichts zu besprechen. */ @@ -133,23 +193,45 @@ zahlen.append(z); } k.append(zahlen); - k.addEventListener('click', () => karte(p.id)); + /* NUR HIER WIRD GESPRUNGEN: Wer eine Person anklickt, will zu ihrer + Karte. Überall sonst bleibt die Seite stehen, wo sie steht. */ + k.addEventListener('click', () => karte(p.id, true)); return k; } /* ---- Die Karte einer Person ----------------------------------------- */ - async function karte(id) { + /* DIE SEITE BEWEGT SICH NUR NOCH, WENN MAN ES WILL (15.09.2026). + + Filipe: „immer wenn ich auf was drücke dan geht das fenster wieder + hoch. die seite soll sich nicht immer wieder bewegen wenn ich auf + was drücke." + + Hier stand ein `scrollIntoView` ohne Bedingung -- und diese Funktion + wird an VIER Stellen gerufen: beim Öffnen einer Person (da ist es + richtig), nach dem ersten Klick auf einen Punkt, beim Aufklappen + aller Kategorien und nach dem Anlegen eines Schritts. Dreimal von + vier war es falsch: Man tippt unten auf „Läuft“ und steht wieder + ganz oben. + + `springen` ist deshalb VORGABE NEIN. Wer springen will, sagt es -- + und das tut genau eine Stelle. Und selbst ohne Sprung wird die + Höhe gehalten: Der Neuaufbau kann die Karte kürzer oder länger + machen (eine Zeile „Schritt läuft“ kommt dazu), und dann rutscht + alles darunter weg. */ + async function karte(id, springen = false) { offen = id; const ziel = $('karte'); + const vorherY = window.scrollY; ziel.hidden = false; ziel.textContent = ''; ziel.append(el('p', 'leise', 'wird geladen …')); - ziel.scrollIntoView({ behavior: 'smooth', block: 'start' }); + if (springen) ziel.scrollIntoView({ behavior: 'smooth', block: 'start' }); const a = await hole(`/workspace/api/entwicklung/person/${id}`); if (!a.ok) { melde('Die Karte ließ sich nicht laden.'); return; } const r = await a.json(); stufen = r.stufen; + letzteKarte = { id, daten: r }; ziel.textContent = ''; const kopf = el('div', 'e-karte__kopf'); @@ -215,10 +297,18 @@ alleKnopf.textContent = alleZu() ? 'Alle zuklappen' : 'Alle aufklappen'; }; alleBeschriften(); + /* Jede Kategorie legt hier ihren eigenen Umschalter ab. */ + const umschalter = []; alleKnopf.addEventListener('click', () => { - if (alleZu()) offeneBloecke.clear(); + /* AN ORT UND STELLE, NICHT ÜBER EINEN NEUAUFBAU. Vorher rief + dieser Knopf karte() -- die Karte wurde neu geholt, neu + gezeichnet, und die Seite sprang. Aufklappen ist eine Sache der + Anzeige; dafür braucht es den Server nicht. */ + const zu = alleZu(); + if (zu) offeneBloecke.clear(); else for (const b of r.bloecke) offeneBloecke.add(b.schluessel); - karte(id); + for (const u of umschalter) u(!zu); + alleBeschriften(); }); leiste.append(alleKnopf); /* HIER STAND KURZ „2 von 28 angesehen" -- dieselbe Zahl, die zwei @@ -234,6 +324,9 @@ const haktHier = b.punkte.filter((pt) => pt.meins?.stufe === 'hakt').length; const abschnitt = el('section', 'e-block'); + /* Der Schlüssel am Element -- daran findet zaehlerNachziehen() + den richtigen Kopf wieder, ohne eine zweite Liste zu führen. */ + abschnitt.dataset.block = b.schluessel; abschnitt.dataset.offen = offen ? 'ja' : 'nein'; if (gesetzt === b.punkte.length) abschnitt.dataset.fertig = 'ja'; @@ -254,22 +347,29 @@ const stand = el('span', 'e-block__stand', `${gesetzt} / ${b.punkte.length}`); if (gesetzt === b.punkte.length) stand.dataset.fertig = 'ja'; schalter.append(stand); - if (haktHier) { - const h = el('span', 'e-block__hakt', `${haktHier} ×`); - h.title = haktHier === 1 ? 'Hier hakt ein Punkt.' : `Hier haken ${haktHier} Punkte.`; - h.append(el('span', 'e-block__hakt-wort', 'hakt')); - schalter.append(h); - } + /* DER HAKT-HINWEIS STEHT IMMER DA, nur meistens verborgen: So + kann er nachgezogen werden, ohne den Kopf neu zu bauen -- und + ein neu gebauter Kopf wäre wieder eine Bewegung. */ + const h = el('span', 'e-block__hakt'); + h.hidden = !haktHier; + h.append(el('span', 'e-block__hakt-zahl', `${haktHier} ×`)); + h.append(el('span', 'e-block__hakt-wort', 'hakt')); + h.title = 'Punkte, bei denen du „Da hakt es“ gesetzt hast.'; + schalter.append(h); + + const umschalten = (auf) => { + abschnitt.dataset.offen = auf ? 'ja' : 'nein'; + koerper.hidden = !auf; + schalter.setAttribute('aria-expanded', String(auf)); + }; + umschalter.push(umschalten); schalter.addEventListener('click', () => { if (offeneBloecke.has(b.schluessel)) offeneBloecke.delete(b.schluessel); else offeneBloecke.add(b.schluessel); /* Nur DIESEN Abschnitt umschalten statt die Karte neu zu holen: Ein Neuaufbau bei jedem Klick ließe die Seite unter den Fingern springen -- und bei 28 Punkten sieht man das. */ - const jetztOffen = offeneBloecke.has(b.schluessel); - abschnitt.dataset.offen = jetztOffen ? 'ja' : 'nein'; - koerper.hidden = !jetztOffen; - schalter.setAttribute('aria-expanded', String(jetztOffen)); + umschalten(offeneBloecke.has(b.schluessel)); alleBeschriften(); }); titel.append(schalter); @@ -282,6 +382,14 @@ abschnitt.append(koerper); ziel.append(abschnitt); } + + /* Die Zahl auf der Personenkachel mitziehen -- sie kommt aus einer + Liste, die nur beim Laden der Seite geholt wird. */ + zaehlerNachziehen(); + /* UND DIE HÖHE HALTEN. Ohne diese Zeile springt die Seite auch ohne + scrollIntoView: Der Neuaufbau ändert die Höhe der Karte, und der + Browser behält nur die Pixelzahl, nicht die Stelle. */ + if (!springen) window.scrollTo({ top: vorherY, behavior: 'instant' }); } function punktZeile(personId, pt, erwartung) { @@ -340,10 +448,18 @@ return; } melde(''); - /* Neu laden, sobald der erste Klick auf einem Punkt sitzt: Erst - danach darf der Stand des anderen überhaupt ausgeliefert - werden, und dann soll er auch sichtbar werden. */ - if (!pt.meins) await karte(personId); + /* DER STAND WANDERT SOFORT MIT -- ohne den Server noch einmal zu + fragen. Daran hängen drei sichtbare Zahlen: der Kopf der Karte, + der Kopf der Kategorie und die Personenkachel oben, die sonst + bei „0 von 28" stehen bliebe. */ + const warNeu = !pt.meins; + pt.meins = { ...(pt.meins || {}), stufe, anlass: feld.value }; + zaehlerNachziehen(); + /* Neu laden NUR beim ersten Klick auf einem Punkt: Erst danach + darf der Stand des anderen überhaupt ausgeliefert werden, und + dann soll er auch sichtbar werden. Ohne Sprung -- karte() + hält seit heute die Höhe. */ + if (warNeu) await karte(personId); })); z.append(feld); diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 94c4fa30..5e7ba09d 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -12,19 +12,19 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + - +
@@ -282,10 +282,10 @@ - - - - - + + + + +