diff --git a/server/pruef-handy-teamdogi.mjs b/server/pruef-handy-teamdogi.mjs
index cdfbeff8..c5a77c12 100644
--- a/server/pruef-handy-teamdogi.mjs
+++ b/server/pruef-handy-teamdogi.mjs
@@ -351,6 +351,32 @@ const messen = () => {
const hatText = (e.textContent || "").trim().length > 0;
const bedienbarHier = e.matches(BEDIENBAR) || !!e.closest(BEDIENBAR);
if (!hatText && !bedienbarHier) return false;
+
+ /* WAS IN EINEM ROLLENDEN KASTEN STEHT, LAEUFT NICHT UEBER
+ (geschaerft 22.09.2026).
+
+ Der Rundgang meldete drei Befunde, bei jeder Rolle dieselben:
+ `button.schnitt bis 481px` auf aufgaben.html und die
+ Rechtetafel `table bis 877px`. Nachgemessen bei 390 px:
+ Das Dokument ist exakt 390 px breit, NICHTS laeuft ueber --
+ beide stehen in einem Kasten mit `overflow-x: auto`, und der
+ rollt absichtlich waagerecht. Eine breite Tabelle auf einem
+ schmalen Schirm ist genau so geloest, wie man es loest.
+
+ Diese drei Meldungen kamen bei JEDEM Lauf. Und eine Warnung,
+ die immer kommt, ist keine Warnung mehr -- man klickt sie weg
+ und uebersieht ab da auch die echte. (Hausregel vom 03.09.2026,
+ damals zu einer Loeschwarnung, die bei jedem Aufruf ansprang.)
+
+ DIE REGEL IST SCHMAL: Es zaehlt nur, wenn ein Vorfahre
+ ausdruecklich `overflow-x: auto` oder `scroll` traegt. Ein
+ Element, das einfach so hinausragt, wird weiter gemeldet --
+ und `overflow: hidden` gilt NICHT, denn dort ist der Inhalt
+ wirklich weg und nicht erreichbar. */
+ for (let v = e.parentElement; v && v !== document.body; v = v.parentElement) {
+ const ox = getComputedStyle(v).overflowX;
+ if (ox === "auto" || ox === "scroll") return false;
+ }
return r.right > breite + 2;
}).slice(0, 4).map((e) => `${name(e)} bis ${Math.round(e.getBoundingClientRect().right)}px`);
@@ -427,6 +453,21 @@ const messen = () => {
/* ---- Zu kleine Tippziele ----------------------------------------- */
const klein = bedienbar.filter((e) => {
+ /* WAS KEINE KLICKS ANNIMMT, IST KEIN TIPPZIEL
+ (geschaerft 22.09.2026).
+
+ Gemeldet wurde `a.k-pille 8x8` auf kalender.html -- die Punkte,
+ die im Monatsraster am Handy fuer je einen Termin stehen. Sie
+ sind ABSICHTLICH 8 px klein und tragen `pointer-events: none`:
+ Der Tipp gehoert der ganzen Tageszelle, nicht dem Punkt. Ein
+ 8-px-Ziel waere ein Nadeloehr; die Zelle daneben ist 46 px und
+ oeffnet den Tag.
+
+ Wer keine Klicks annimmt, kann auch kein zu kleines Ziel sein.
+ Die Regel ist damit schaerfer, nicht laxer: Ein Knopf, der
+ Klicks annimmt und zu klein ist, wird weiter gemeldet -- die
+ Gegenprobe weiter unten weist genau das nach. */
+ if (getComputedStyle(e).pointerEvents === "none") return false;
const r = e.getBoundingClientRect();
/* 43,5 STATT 44: Ein Knopf von 43,98 px wird als „44x44"
angezeigt und waere trotzdem ein Fund -- ein halbes Pixel
@@ -553,6 +594,17 @@ for (const [breite, hoehe, geraet] of BREITEN) {
if (m.type() === "error") konsole.push(m.text().slice(0, 100));
});
seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message.slice(0, 100)));
+ /* WELCHE Datei fehlt -- die Konsole sagt es nicht (22.09.2026).
+ Der Rundgang meldete tagelang "404 (Not Found)" ohne Adresse.
+ Eine Pruefung, die einen Fehler findet, ihn aber nicht
+ auffindbar macht, kostet mehr Zeit als sie spart: Man weiss,
+ dass etwas kaputt ist, und muss es trotzdem von Hand suchen. */
+ seite.on("requestfailed", (r) => {
+ konsole.push(`Anfrage gescheitert: ${r.url().replace(BASIS, "")}`);
+ });
+ seite.on("response", (r) => {
+ if (r.status() === 404) konsole.push(`404: ${r.url().replace(BASIS, "")}`);
+ });
/* ---- Anmelden, wie ein Mensch es tut ---------------------------- */
let drin = false;
@@ -702,13 +754,47 @@ melde("\n --- Gegenprobe (beide Fehler MÜSSEN gefunden werden) ---");
winzig.textContent = "x";
winzig.style.cssText = "width:18px;height:18px";
document.body.append(winzig);
+
+ /* ---- UND DIE ZWEI AUSNAHMEN VOM 22.09.2026 -------------------
+ Beide Regeln machen die Messung ENGER. Eine engere Messung kann
+ auch zu eng sein -- dann fiele echter Schaden durch, und der
+ Lauf bliebe gruen. Deshalb hier vier Elemente statt zwei: zwei,
+ die gemeldet werden MUESSEN, und zwei, die es NICHT duerfen. */
+
+ /* (a) In einem rollenden Kasten -- darf NICHT gemeldet werden.
+ So steht die Rechtetafel da, und so gehoert eine breite
+ Tabelle auf einen schmalen Schirm. */
+ const roller = document.createElement("div");
+ roller.id = "gp-roller";
+ roller.style.cssText = "overflow-x:auto;width:200px";
+ const drin = document.createElement("div");
+ drin.textContent = "Steht in einem Kasten, der absichtlich rollt";
+ drin.style.cssText = "width:900px;height:20px;white-space:nowrap";
+ roller.append(drin);
+ document.body.append(roller);
+
+ /* (b) Winzig, aber nimmt keine Klicks an -- darf NICHT gemeldet
+ werden. So sind die Terminpunkte im Kalender gebaut. */
+ const stumm = document.createElement("button");
+ stumm.id = "gp-stumm";
+ stumm.textContent = "o";
+ stumm.style.cssText = "width:8px;height:8px;pointer-events:none";
+ document.body.append(stumm);
});
const g = await seite.evaluate(messen);
const ok1 = g.ueberstand.length > 0;
const ok2 = g.klein.length > 0;
+ /* Die zwei Ausnahmen duerfen NICHT in der Liste stehen. Gemessen
+ wird das an ihrer Groesse: 900 px waere der Roller-Inhalt, 8x8
+ der stumme Knopf. */
+ const ok3 = !g.ueberstand.some((x) => /9\d\dpx/.test(x));
+ const ok4 = !g.klein.some((x) => /\b8×8\b/.test(x));
melde(` ${ok1 ? "ok " : "FEHL"} ein zu breites Element wird gemeldet (${g.ueberstand.join(", ") || "nichts"})`);
melde(` ${ok2 ? "ok " : "FEHL"} ein zu kleiner Knopf wird gemeldet (${g.klein.join(", ") || "nichts"})`);
+ melde(` ${ok3 ? "ok " : "FEHL"} was in einem rollenden Kasten steht, NICHT`);
+ melde(` ${ok4 ? "ok " : "FEHL"} was keine Klicks annimmt, auch NICHT`);
if (!ok1 || !ok2) { fehler += 1; probleme.push("GEGENPROBE VERSAGT — die Messung findet nichts mehr"); }
+ if (!ok3 || !ok4) { fehler += 1; probleme.push("GEGENPROBE VERSAGT — die Messung meldet Absicht als Fehler"); }
await kontext.close();
}
diff --git a/server/pruef-nachfrage.mjs b/server/pruef-nachfrage.mjs
index 3eaa9d1e..fba04ae1 100644
--- a/server/pruef-nachfrage.mjs
+++ b/server/pruef-nachfrage.mjs
@@ -97,7 +97,27 @@ const nativ = [];
for (const f of skripte) {
if (f === DER_NOTNAGEL) continue;
const code = ohneKommentare(lies(join(JS_ORDNER, f)));
- for (const m of code.matchAll(/(?:^|[^.\w])(confirm|prompt)\s*\(/g)) {
+ /* DAS MUSTER HATTE EINE LUECKE, UND ZWAR DIE GROESSTE MOEGLICHE
+ (gefunden am 22.09.2026).
+
+ Vorher stand hier `(?:^|[^.\w])(confirm|prompt)\s*\(`. Das
+ `[^.\w]` sollte fremde Methoden ausschliessen -- etwas wie
+ `angebot.prompt()` (so heisst die Installations-Aufforderung des
+ Browsers) ist kein nativer Dialog und darf nicht als Fund zaehlen.
+
+ Nur trifft dieser Ausschluss ausgerechnet die HAEUFIGSTE
+ Schreibweise: `window.prompt(` hat einen Punkt davor. Die Pruefung
+ war damit gruen, waehrend in teamlage.js ZWEI native prompt()
+ standen -- einer davon seit Wochen.
+
+ Genau das Muster, vor dem dieses Haus warnt: eine Pruefung, die
+ laeuft, gruen ist und das Falsche prueft. Gefunden hat es nicht
+ sie, sondern eine Suche von Hand.
+
+ Jetzt zwei Faelle statt einem: mit `window.` davor -- oder ganz
+ ohne Punkt. Alles andere (`irgendwas.prompt()`) bleibt aussen
+ vor, und das ist weiterhin richtig. */
+ for (const m of code.matchAll(/(?:^|[^.\w])(?:window\s*\.\s*)?(confirm|prompt)\s*\(/g)) {
const zeile = code.slice(0, m.index).split("\n").length;
nativ.push(`${f}:${zeile} → ${m[1]}()`);
}
@@ -280,6 +300,15 @@ pruef("die Speicherwege melden der Wache, dass nichts mehr zu retten ist",
------------------------------------------------------------------ */
console.log("\n --- Gegenproben (jede MUSS anschlagen) ---");
+/* DIE GEGENPROBE PRUEFT JETZT BEIDE SCHREIBWEISEN. Haette sie das
+ vorher getan, waere die Luecke im Muster am selben Tag aufgefallen,
+ an dem es geschrieben wurde. */
+pruef("Gegenprobe: ein erfundenes window.prompt() würde gefunden",
+ /(?:^|[^.\w])(?:window\s*\.\s*)?(confirm|prompt)\s*\(/.test(
+ "const g = window.prompt('Warum nicht?');"));
+pruef("Gegenprobe: eine fremde Methode NICHT (angebot.prompt())",
+ !/(?:^|[^.\w])(?:window\s*\.\s*)?(confirm|prompt)\s*\(/.test(
+ "angebot.prompt();"));
pruef("Gegenprobe: ein erfundenes confirm() würde gefunden",
/(?:^|[^.\w])(confirm|prompt)\s*\(/.test(
ohneKommentare("function x(){ if (!confirm('weg?')) return; }")));
diff --git a/server/pruef-werdegang.mjs b/server/pruef-werdegang.mjs
index 3c3a3acb..36120d63 100644
--- a/server/pruef-werdegang.mjs
+++ b/server/pruef-werdegang.mjs
@@ -600,8 +600,58 @@ melde("=== 11. Im Browser ===");
{
const { chromium } = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
- const browser = await chromium.launch();
- const kontext = await browser.newContext({ viewport: { width: 1280, height: 1000 } });
+ /* AUF DER ECHTEN ADRESSE MESSEN (berichtigt 22.09.2026).
+ ==================================================================
+ Bis heute oeffnete dieser Abschnitt `http://127.0.0.1/...` -- also
+ die ADRESSE DER AGENTUR. Auf der ist `person.haus` nicht "crew",
+ und `bereicheFuer()` liefert eine andere Kachelliste. Die Kachel
+ zu dieser Seite steht dort nicht drin, also fand `kopf.js` keine
+ Farbe: `data-ton=null`.
+
+ Drei Pruefungen waren deshalb rot, und zwar dauerhaft -- an einer
+ Seite, die in Wirklichkeit in Ordnung ist. Nachgemessen: Alle vier
+ Rollen (admin, hand, linke, modi) HABEN die Kachel, sobald das
+ Haus stimmt.
+
+ Genau dieselbe Falle wie am 21.09.2026 bei pruef-rueckmeldung.
+ Dort ging es ueber `node:http` mit Host-Kopf; ein Browser kann das
+ nicht (Chromium verbietet den Host-Kopf). Der Weg im Haus ist ein
+ selbst unterschriebener Vorbau plus `--host-resolver-rules` --
+ derselbe wie in pruef-willkommen und pruef-zuteilung. */
+ const { createServer: httpsServer } = await import("node:https");
+ const { request: httpWeiter } = await import("node:http");
+ const { execFileSync } = await import("node:child_process");
+ const { mkdtempSync: tempOrdner, readFileSync: liesDatei } = await import("node:fs");
+ const { tmpdir: tempWurzel } = await import("node:os");
+ const { join: pfad } = await import("node:path");
+
+ const zOrdner = tempOrdner(pfad(tempWurzel(), "wg-zeugnis-"));
+ const zKey = pfad(zOrdner, "k.pem"), zCrt = pfad(zOrdner, "z.pem");
+ execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes",
+ "-keyout", zKey, "-out", zCrt, "-days", "2", "-subj", `/CN=${CREW}`,
+ "-addext", `subjectAltName=DNS:${CREW}`], { stdio: "ignore" });
+
+ const HTTPS_PORT = PORT + 1;
+ const vorbau = httpsServer(
+ { key: liesDatei(zKey), cert: liesDatei(zCrt) },
+ (von, an) => {
+ const weiter = httpWeiter({
+ host: "127.0.0.1", port: PORT, path: von.url, method: von.method,
+ headers: { ...von.headers, host: CREW },
+ }, (antwort) => { an.writeHead(antwort.statusCode, antwort.headers); antwort.pipe(an); });
+ weiter.on("error", () => { try { an.destroy(); } catch { /* Zugabe */ } });
+ von.on("error", () => {});
+ von.pipe(weiter);
+ });
+ await new Promise((r) => vorbau.listen(HTTPS_PORT, "127.0.0.1", r));
+ const SEITE = `https://${CREW}:${HTTPS_PORT}`;
+
+ const browser = await chromium.launch({
+ args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1`, "--ignore-certificate-errors"],
+ ignoreHTTPSErrors: true,
+ });
+ const kontext = await browser.newContext({
+ viewport: { width: 1280, height: 1000 }, ignoreHTTPSErrors: true });
const seite = await kontext.newPage();
const konsole = [];
seite.on("console", (m) => { if (m.type() === "error" && !/favicon|ERR_/i.test(m.text())) konsole.push(m.text()); });
@@ -609,10 +659,10 @@ melde("=== 11. Im Browser ===");
await kontext.addCookies(kDogi.split("; ").filter(Boolean).map((teil) => {
const i = teil.indexOf("=");
- return { name: teil.slice(0, i), value: teil.slice(i + 1), domain: "127.0.0.1", path: "/" };
+ return { name: teil.slice(0, i), value: teil.slice(i + 1), domain: CREW, path: "/" };
}));
- await seite.goto(`http://127.0.0.1:${PORT}/workspace/werdegang.html`, { waitUntil: "networkidle" });
+ await seite.goto(`${SEITE}/workspace/werdegang.html`, { waitUntil: "networkidle" });
await seite.waitForTimeout(900);
const liste = await seite.evaluate(() => ({
@@ -741,6 +791,7 @@ melde("=== 11. Im Browser ===");
ok(konsole.length === 0,
konsole.length ? "Fehler im Browser: " + konsole[0].slice(0, 120) : "keine Fehler im Browser");
await browser.close();
+ await new Promise((r) => vorbau.close(r));
}
/* =======================================================================
diff --git a/server/workspace.js b/server/workspace.js
index 11b707df..a3822f75 100644
--- a/server/workspace.js
+++ b/server/workspace.js
@@ -5723,6 +5723,20 @@ workspaceRouter.get("/workspace/api/ich", (req, res) => {
/* Damit die Oberflaeche den Knopf gar nicht erst anbietet -- ein
Knopf, der mit 403 antwortet, ist schlimmer als keiner. */
darf_aufgaben_anlegen: darfAufgabenAnlegen(person),
+ /* FUEHRT JEMAND TEAM DOGI? (22.09.2026)
+ Damit die Entwicklungsseite die Kartenliste GAR NICHT ERST
+ abfragt, wenn es sie fuer diese Person nicht gibt. Der Code
+ behandelte den 404 schon richtig -- aber der Browser
+ protokolliert ihn trotzdem, und im Handy-Rundgang stand
+ deshalb bei jedem Modi "404 (Not Found)" in der Konsole.
+ Genau derselbe Fall wie am 19.09.2026 bei /anruf/adressen.
+
+ EIGENES FELD, kein Stellvertreter: `darf_verteilen` sieht
+ aehnlich aus, ist aber nicht dasselbe -- ein Manager darf
+ verteilen und fuehrt Team Dogi nicht. Zwei Regeln, zwei
+ Felder; eines fuer beides waere die Stelle, an der es beim
+ naechsten Umbau auseinanderlaeuft. */
+ fuehrt_team: fuehrtTeamDogi(person),
/* DIE KACHELN, WENN SIE NICHT IM BROWSER STEHEN DUERFEN.
Fuer die fuenf bekannten Rollen steht hier `null`, und die
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index 065cc8f1..707cb9d7 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`. -->
-
-
+
+
-
+