/* ===================================================================== WER HAT SIE, WER NICHT — die Einzelansicht bei „An alle" (24.09.2026) --------------------------------------------------------------------- WARUM ES DIESE DATEI GIBT Filipe: „wenn ich eine aufgabe an alle verteile will ich dass dogfather und die rechte hand individuel von jedem sehen wer es gemacht hat oder nicht. aufgabe an alle verteilen aber individuel sehen wir das." Gebaut ist das in `vorlagenbrett.js`. Ob es AUSSIEHT wie gedacht, beantwortet kein Quelltext: Die Marken stehen in einer Reihe, die umbricht, in einer Karte, die schmaler wird — und auf einem Handy ist die Karte 380 px breit, nicht 400. SIE PRUEFT NICHTS und meldet keinen Fehlschlag. Sie macht ein Bild, auf dem man nachsehen kann. Sie heisst `mess-` und nicht `pruef-`: Beide Sorten leiten ihre Portnummer aus der Stelle im Alphabet ab, aber jede aus IHRER eigenen Liste und in ihrem eigenen Bereich (Pruefungen ab 5000, Messungen ab 5900). So verschiebt eine neue Messdatei die Nummern der Pruefungen nicht -- und umgekehrt. --------------------------------------------------------------------- EIGENE WEGWERF-DATENBANK, NIE DIE ECHTE Es entstehen echte Aufgaben — vier Stueck, damit alle vier Zustaende im Bild sind. In einem laufenden System waeren das Testdaten, und die sind verboten. Hier liegt die Datenbank in einem `mktemp`-Ordner und wird am Ende geloescht. Aufrufen mit: node server/mess-alle-einzelsicht.mjs ===================================================================== */ import { mkdtempSync, rmSync, readFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { execFileSync } from "node:child_process"; import { request as httpAnfrage } from "node:http"; import { createServer as httpsServer } from "node:https"; import { eigenerPort } from "./helfer-port.mjs"; const PORT = await eigenerPort(import.meta); const HP = await eigenerPort(import.meta, "mess-alle-einzelsicht (HTTPS)", 1); const CREW = "crew.dogfather-universe.com"; const ordner = mkdtempSync(join(tmpdir(), "ws-alle-sicht-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); process.env.PORT = String(PORT); process.env.SITE_ACCESS_SECRET = "lokaler-test"; process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; const express = (await import("express")).default; const ec = express.response.cookie; express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; import { notbremse } from "./helfer-notbremse.mjs"; import { zertifikatBauen } from "./helfer-openssl.mjs"; await import("./index.js"); notbremse(240_000, "mess-alle-einzelsicht"); await new Promise((r) => setTimeout(r, 900)); process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); }); process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); }); const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); const d = new DatabaseSync(process.env.WORKSPACE_DB); const jetzt = new Date().toISOString(); const sch = randomBytes(32).toString("hex"); d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") .run("code_kennung_schluessel", sch, jetzt); const anlegen = (name, rolle, code) => { const s = randomBytes(16).toString("hex"); const h = scryptSync(code, s, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); return d.prepare("INSERT INTO personen" + " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)" + " VALUES (?,?,?,?,?,?,1,?) RETURNING id") .get(name, rolle, h, s, 32768, createHmac("sha256", sch).update(code).digest("hex"), jetzt).id; }; /* Vier Empfaenger und eine rechte Hand — genau die Besetzung, um die es geht: Die Haende sind getrennt (eine verteilt, eine bekommt). */ const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001"); const idHand = anlegen("VanVan", "hand", "CODE-HAND-0001"); const idLinke = anlegen("Kessi", "linke", "CODE-LINKE-0001"); const idM1 = anlegen("Diene", "modi", "CODE-MODI-0001"); const idM2 = anlegen("Marina", "modi", "CODE-MODI-0002"); const idM3 = anlegen("Ghost", "modi", "CODE-MODI-0003"); void idDogi; void idHand; d.close(); const anfrage = (pfad, art, koerper, keks) => new Promise((fertig, schief) => { const a = httpAnfrage({ host: "127.0.0.1", port: PORT, path: pfad, method: art, headers: { Host: CREW, "Content-Type": "application/json", ...(keks ? { Cookie: keks } : {}), }, }, (r) => { let t = ""; r.on("data", (x) => { t += x; }); r.on("end", () => fertig({ code: r.statusCode, kopf: r.headers, text: t })); }); a.on("error", schief); if (koerper) a.write(JSON.stringify(koerper)); a.end(); }); const an = await anfrage("/workspace/api/anmelden", "POST", { rolle: "admin", code: "CODE-DOGI-0001" }); if (an.code !== 200) { console.log(`ABBRUCH: DogFather ließ sich nicht anmelden (HTTP ${an.code}).`); process.exit(2); } const keks = (an.kopf["set-cookie"] || []).map((x) => x.split(";")[0]).join("; "); /* ---- Ein Stand, auf dem man ALLE VIER Zustaende sieht ---------------- Sonst zeigt das Bild nur „hat sie nicht", und man weiss nachher nicht, ob die Farben stimmen. */ /* Der Katalog steckt UNTER `katalog` in der Antwort -- die Route liefert daneben noch Bewerbungen und Rechte. */ const kat = JSON.parse((await anfrage("/workspace/api/vorlagen", "GET", null, keks)).text); const liste = kat?.katalog?.aufgaben; if (!Array.isArray(liste) || !liste.length) { console.log("ABBRUCH: kein Katalog in der Antwort --", JSON.stringify(kat).slice(0, 300)); process.exit(3); } const v0 = liste.map((v, i) => ({ ...v, nr: i }))[0]; const verteilt = await anfrage("/workspace/api/vorlagen/uebernehmen", "POST", { art: "katalog", nr: v0.nr, gruppe: v0.gruppe, person_id: "alle" }, keks); console.log(`Verteilt an alle: HTTP ${verteilt.code} ${verteilt.text.slice(0, 80)}`); const d2 = new DatabaseSync(process.env.WORKSPACE_DB); /* Einer hat sie erledigt, einer ist ueberfaellig, einer hat sie gar nicht — die Linke Hand behaelt ihr schlichtes „Offen". */ d2.prepare("UPDATE aufgaben SET status = 'erledigt' WHERE vorlage = ? AND verantwortlich_id = ?") .run(v0.schluessel, idM1); d2.prepare("UPDATE aufgaben SET frist = '2026-09-01' WHERE vorlage = ? AND verantwortlich_id = ?") .run(v0.schluessel, idM2); d2.prepare("DELETE FROM aufgaben WHERE vorlage = ? AND verantwortlich_id = ?") .run(v0.schluessel, idM3); const stand = d2.prepare( "SELECT verantwortlich_id AS w, status, frist FROM aufgaben WHERE vorlage = ?") .all(v0.schluessel); d2.close(); console.log("Stand:", JSON.stringify(stand), `\n (${idLinke}=Kessi/linke, ${idM1}=Diene, ${idM2}=Marina, ${idM3}=Ghost)`); const schl = join(ordner, "b.key"), zert = join(ordner, "b.crt"); zertifikatBauen(schl, zert, CREW); const vorbau = httpsServer({ key: readFileSync(schl), cert: readFileSync(zert) }, (von, nach) => { const w = httpAnfrage({ host: "127.0.0.1", port: PORT, path: von.url, method: von.method, headers: { ...von.headers, host: `${CREW}:${HP}` }, }, (a) => { nach.writeHead(a.statusCode || 502, a.headers); a.pipe(nach); }); von.pipe(w); }); await new Promise((r) => vorbau.listen(HP, "127.0.0.1", r)); 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 ${CREW} 127.0.0.1`, "--ignore-certificate-errors"], }); try { /* ZWEI BREITEN. 412 px ist die haeufigste Android-Breite; an ihr ist am 06.09. die Kopfleiste gescheitert. 1280 px ist der kleinste Rechner, an dem noch jemand arbeitet. */ for (const [wie, breite, hoehe] of [["handy", 412, 1400], ["computer", 1280, 1600]]) { const kontext = await browser.newContext({ viewport: { width: breite, height: hoehe }, hasTouch: breite <= 860 }); await kontext.addCookies(keks.split("; ").map((k) => { const [n, ...r] = k.split("="); return { name: n, value: r.join("="), domain: CREW, path: "/" }; })); const seite = await kontext.newPage(); const konsole = []; seite.on("console", (e) => { if (e.type() === "error" && !/favicon|ERR_CERT|net::ERR/i.test(e.text())) { konsole.push(e.text().slice(0, 140)); } }); await seite.goto(`https://${CREW}:${HP}/workspace/entwicklung.html`, { waitUntil: "networkidle" }); await seite.waitForTimeout(1200); /* Das Brett startet zugeklappt — aufklappen, sonst ist das Bild leer. */ await seite.evaluate(() => { const kopf = document.querySelector('.vorlagenbrett__kopf[aria-expanded="false"]'); if (kopf) kopf.click(); }); await seite.waitForTimeout(700); /* Den Alle-Knopf waehlen -- das ist der Fall, um den es geht. */ const traf = await seite.evaluate(() => { const k = document.querySelector('.stufenleiter--wer [data-alle="ja"]'); if (!k) return false; k.click(); return true; }); await seite.waitForTimeout(1400); const gesehen = await seite.evaluate(() => { const karte = document.querySelector('.vorlagenbrett__karte .vorlagenbrett__wer') ?.closest('.vorlagenbrett__karte'); const reihe = [...(karte?.querySelectorAll('.vorlagenbrett__wer-p') || [])] .map((m) => ({ wer: m.querySelector('.vorlagenbrett__wer-name')?.textContent, stand: m.querySelector('.vorlagenbrett__wer-stand')?.textContent, art: m.dataset.stand, })); return { wen: [...document.querySelectorAll('.stufenleiter--wer .stufenleiter__knopf')] .map((k) => k.textContent.trim().replace(/\s+/g, ' ')), titel: karte?.querySelector('.vorlagenbrett__titel')?.textContent, fuss: karte?.querySelector('.vorlagenbrett__schon')?.textContent, knopf: karte?.querySelector('.knopf')?.textContent, hinweis: karte?.querySelector('.knopf')?.title, reihe, mitReihe: document.querySelectorAll('.vorlagenbrett__wer').length, }; }); console.log(`\n=== ${wie} (${breite} px) ===`); console.log(" Alle-Knopf getroffen:", traf); console.log(" An wen:", JSON.stringify(gesehen.wen)); console.log(" Karte :", gesehen.titel); console.log(" Fuss :", gesehen.fuss, "| Knopf:", gesehen.knopf); console.log(" Hinweis:", gesehen.hinweis); console.log(" Karten mit Namensreihe:", gesehen.mitReihe); for (const r of gesehen.reihe) console.log(` ${r.wer} -> ${r.stand} [${r.art}]`); if (konsole.length) console.log(" FEHLER IN DER KONSOLE:", konsole); const karte = seite.locator('.vorlagenbrett__karte', { has: seite.locator('.vorlagenbrett__wer') }).first(); if (await karte.count()) { await karte.screenshot({ path: `server/mess-alle-${wie}.png` }); console.log(` Bild: server/mess-alle-${wie}.png`); /* UND DIE RUECKSEITE. Ohne sie waere nur bewiesen, dass die Namen von der Vorderseite verschwunden sind -- nicht, dass sie hinten ankommen. */ await karte.locator('.vorlagenbrett__drehknopf').click().catch(() => {}); await seite.waitForTimeout(600); const hinten = await seite.evaluate(() => { const k = document.querySelector('.vorlagenbrett__karte[data-gedreht="ja"]'); const r = k?.querySelector('.vorlagenbrett__seite--hinten'); return { gedreht: !!k, schild: r?.querySelector('.vorlagenbrett__wer-schild')?.textContent, knoepfe: [...(r?.querySelectorAll('button') || [])].map((b) => b.textContent), namen: [...(r?.querySelectorAll('.vorlagenbrett__wer-name') || [])] .map((x) => x.textContent), }; }); console.log(' Rueckseite:', JSON.stringify(hinten)); await karte.screenshot({ path: `server/mess-alle-${wie}-hinten.png` }).catch(() => {}); } await kontext.close(); } } finally { await browser.close(); vorbau.close(); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } process.exit(0); }