diff --git a/server/pruef-zuteilung.mjs b/server/pruef-zuteilung.mjs
index 58929fdc..0f97748b 100644
--- a/server/pruef-zuteilung.mjs
+++ b/server/pruef-zuteilung.mjs
@@ -370,6 +370,197 @@ melde("\n=== Grenzen beim Verteilen ===");
`die Aufgabe liegt bei Anna selbst (${beiAnna?.verantwortlich_name})`);
}
+
+/* =====================================================================
+ 8. AM ECHTEN BILDSCHIRM
+ =====================================================================
+
+ Bis hierher ist bewiesen, dass der Server richtig entscheidet. Das
+ sagt nichts darüber, ob ein Mensch es bedienen kann. Geprüft wird
+ der Weg, den er wirklich geht: Seite öffnen, Band lesen, Knopf
+ drücken.
+
+ WARUM DAS NICHT DIE SCHNITTSTELLE NOCH EINMAL PRÜFT: Ein Knopf, den
+ es nicht gibt, ist genauso wirkungslos wie eine Route, die 403 sagt
+ — nur fällt er niemandem auf.
+ ===================================================================== */
+melde("\n=== Am echten Bildschirm ===");
+{
+ /* WARUM HIER EIN https-VORBAU STEHT -- gemessen, nicht vermutet.
+
+ `crew.dogfather-universe.com` steht in Chromiums eingebauter
+ HSTS-Liste. Der Browser schaltet von sich aus auf https um, noch
+ bevor eine Anfrage hinausgeht; der Testserver spricht http, und
+ das Ergebnis ist `chrome-error://chromewebdata/` -- die Seite
+ laedt gar nicht erst. Genau so ist dieser Abschnitt beim ersten
+ Lauf gescheitert.
+
+ Abschalten laesst sich das nicht (die Liste ist einkompiliert).
+ Also spricht die Pruefung selbst https: ein eigenes Zertifikat
+ auf diesen Namen, ein Vorbau, der alles unveraendert
+ weiterreicht -- BESONDERS die Host-Kopfzeile, denn an ihr
+ entscheidet das Haus, wer was sieht.
+
+ Derselbe Weg wie in pruef-handy-teamdogi. Eine Nachbildung waere
+ die zweite Fassung, die anders altert. */
+ const { execFileSync } = await import("node:child_process");
+ const { createServer: httpsServer } = await import("node:https");
+ const { request: httpAnfrage } = await import("node:http");
+ const { readFileSync } = await import("node:fs");
+
+ const schluessel = join(ordner, "pruef.key");
+ const zertifikat = join(ordner, "pruef.crt");
+ execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes",
+ "-keyout", schluessel, "-out", zertifikat, "-days", "2",
+ "-subj", `/CN=${HOST}`, "-addext", `subjectAltName=DNS:${HOST}`],
+ { stdio: "ignore" });
+
+ const HTTPS_PORT = PORT + 1;
+ const vorbau = httpsServer({
+ key: readFileSync(schluessel), cert: readFileSync(zertifikat),
+ }, (von, nach) => {
+ const weiter = httpAnfrage({
+ host: "127.0.0.1", port: PORT, path: von.url, method: von.method,
+ headers: { ...von.headers, host: `${HOST}:${HTTPS_PORT}` },
+ }, (antwort) => {
+ nach.writeHead(antwort.statusCode || 502, antwort.headers);
+ antwort.pipe(nach);
+ });
+ weiter.on("error", () => { try { nach.writeHead(502); nach.end(); } catch { /* egal */ } });
+ von.pipe(weiter);
+ });
+ await new Promise((r) => vorbau.listen(HTTPS_PORT, "127.0.0.1", r));
+ const SEITE = `https://${HOST}:${HTTPS_PORT}`;
+
+ const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
+ const browser = await pw.chromium.launch({
+ args: [`--host-resolver-rules=MAP ${HOST} 127.0.0.1`,
+ "--ignore-certificate-errors"],
+ });
+ const seitenFehler = [];
+ async function alsSeite(rolle, code) {
+ const ctx = await browser.newContext({ viewport: { width: 1280, height: 950 } });
+ const s2 = await ctx.newPage();
+ s2.on("pageerror", (e) => seitenFehler.push(`${rolle}: ${String(e).slice(0, 110)}`));
+ await s2.goto(`${SEITE}/workspace/`, { waitUntil: "networkidle" });
+ await s2.click(`.rolle[data-rolle="${rolle}"]`);
+ await s2.fill("#code", code);
+ await s2.click("button[type=submit]");
+ await s2.waitForURL("**/start.html", { timeout: 25000 });
+ await s2.goto(`${SEITE}/workspace/aufgaben.html`, { waitUntil: "networkidle" });
+ await s2.waitForTimeout(1500);
+ return { ctx, seite: s2 };
+ }
+
+ /* ---- Der Modi: sein eigenes Resümee und seine Knöpfe ---- */
+ const pAnna = await alsSeite("modi", "CODE-MOD1-0001");
+ try {
+ const band = await pAnna.seite.evaluate(() => {
+ const b = document.querySelector(".z-band");
+ if (!b) return null;
+ return {
+ titel: b.querySelector(".z-band__titel")?.textContent,
+ satz: b.querySelector(".z-band__satz")?.textContent,
+ zahlen: [...b.querySelectorAll(".zk")].map((z) =>
+ z.querySelector(".zk__zahl").textContent + " " + z.querySelector(".zk__wort").textContent),
+ istTeam: b.classList.contains("z-band--team"),
+ };
+ });
+ ok(!!band, "ein Modi sieht ein Band ueber dem Brett");
+ ok(band && band.titel === "Deine Aufgaben",
+ `und es ist SEIN Resuemee (${band?.titel})`);
+ ok(band && band.istTeam === false,
+ "nicht die Uebersicht ueber alle");
+ ok(band && band.zahlen.length >= 3,
+ `mit Zahlen (${(band?.zahlen || []).join(" · ")})`);
+
+ /* DER KERN DER OBERFLAECHE: Der Knopf, mit dem sie annimmt. */
+ const knopf = await pAnna.seite.evaluate(() => {
+ const k = [...document.querySelectorAll(".z-karte .knopf")]
+ .map((b) => b.textContent.trim());
+ return k;
+ });
+ ok(knopf.length > 0, `an ihren Karten stehen Knoepfe (${knopf.join(", ")})`);
+ ok(knopf.includes("Fertig") || knopf.includes("Ich fange an") || knopf.includes("Annehmen"),
+ "und darunter einer, der sie weiterbringt");
+
+ /* GEGENPROBE: Es darf KEIN Bewerten-Knopf dastehen. */
+ const bewerten = await pAnna.seite.evaluate(() =>
+ document.querySelectorAll(".z-bewerten").length);
+ ok(bewerten === 0, `ein Modi bekommt keine Bewerten-Knoepfe (${bewerten})`);
+ } finally {
+ await pAnna.ctx.close();
+ }
+
+ /* ---- DogFather: die Uebersicht ueber alle ---- */
+ const pChef = await alsSeite("admin", "CODE-DOGI-0001");
+ try {
+ const band = await pChef.seite.evaluate(() => {
+ const b = document.querySelector(".z-band");
+ if (!b) return null;
+ return {
+ titel: b.querySelector(".z-band__titel")?.textContent,
+ istTeam: b.classList.contains("z-band--team"),
+ leute: [...b.querySelectorAll(".z-person__name")].map((n) => n.textContent),
+ marken: [...b.querySelectorAll(".z-marke")].map((m) => m.textContent),
+ };
+ });
+ ok(!!band && band.istTeam === true,
+ `DogFather sieht die Uebersicht ueber alle (${band?.titel})`);
+ ok((band?.leute || []).includes("Anna") && (band?.leute || []).includes("Bea"),
+ `mit allen Namen (${(band?.leute || []).join(", ")})`);
+ ok((band?.marken || []).some((m) => /erledigt|offen|in Arbeit/.test(m)),
+ `und Marken, die etwas sagen (${(band?.marken || []).slice(0, 4).join(" · ")})`);
+
+ /* EINE PERSON ANTIPPEN FILTERT DAS BRETT. */
+ const vorher = await pChef.seite.evaluate(() =>
+ [...document.querySelectorAll(".karte[data-id]")].filter((k) => !k.hidden).length);
+ await pChef.seite.click('.z-person[data-person]');
+ await pChef.seite.waitForTimeout(400);
+ const nachher = await pChef.seite.evaluate(() =>
+ [...document.querySelectorAll(".karte[data-id]")].filter((k) => !k.hidden).length);
+ ok(vorher > 0, `vorher sind ${vorher} Karten zu sehen`);
+ ok(nachher < vorher, `nach dem Antippen weniger (${nachher})`);
+
+ /* UND NOCH EINMAL ANTIPPEN ZEIGT WIEDER ALLE -- sonst waere der
+ Filter eine Falle: Man kommt hinein und nicht mehr heraus. */
+ await pChef.seite.click('.z-person[data-person]');
+ await pChef.seite.waitForTimeout(400);
+ const zurueck = await pChef.seite.evaluate(() =>
+ [...document.querySelectorAll(".karte[data-id]")].filter((k) => !k.hidden).length);
+ ok(zurueck === vorher, `und noch einmal zeigt wieder alle (${zurueck})`);
+
+ /* DIE MEHRFACHAUSWAHL IM FORMULAR. */
+ await pChef.seite.click("#neu-oeffnen");
+ await pChef.seite.waitForTimeout(500);
+ const mehr = await pChef.seite.evaluate(() => {
+ const k = document.getElementById("feld-mehrere");
+ const auf = document.getElementById("mehrere-auf");
+ if (!k || k.hidden || !auf) return { da: false };
+ auf.click();
+ const leute = [...document.querySelectorAll("#mehrere-leute input[type=checkbox]")];
+ const artVorher = document.getElementById("mehrere-art").hidden;
+ leute.slice(0, 2).forEach((x) => { x.checked = true; x.dispatchEvent(new Event("change")); });
+ return { da: true, zahl: leute.length, artVorher,
+ artNachher: document.getElementById("mehrere-art").hidden };
+ });
+ ok(mehr.da, "DogFather kann mehrere dazunehmen");
+ ok(mehr.zahl >= 2, `es stehen Leute zur Auswahl (${mehr.zahl})`);
+ ok(mehr.artVorher === true,
+ "die Frage 'wie soll das laufen' steht erst nicht da");
+ ok(mehr.artNachher === false,
+ "und erscheint, sobald zwei gewaehlt sind");
+ } finally {
+ await pChef.ctx.close();
+ }
+
+ ok(seitenFehler.length === 0, seitenFehler.length === 0
+ ? "kein Skript ist unterwegs gestolpert"
+ : `Skriptfehler: ${seitenFehler.join(" | ")}`);
+ await browser.close();
+ await new Promise((r) => vorbau.close(r));
+}
+
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Zugabe */ }
melde(`\n ${fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`}`);
process.exit(fehler ? 1 : 0);
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index b9e87786..8a833e76 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`. -->
-
-
+
+
-
+