VIER DINGE, und das erste ist eine Meldung aus dem Support.
1. MISS KAM AUS "AUFGABE BEARBEITEN" NICHT HERAUS.
„Ich konnte da wieder nicht zurück gehen, musste die App schließen
damit ich wieder auf die Hauptseite kam."
Gemessen (mess-dialog-ausgang.mjs), vier Größen:
412x915 App 782 px Inhalt in 784 px -- knapp ja
412x780 Browser 782 px Inhalt in 742 px -- SACKGASSE
360x640 klein 794 px Inhalt in 602 px -- SACKGASSE
412x430 Tastatur 794 px Inhalt in 392 px -- SACKGASSE
`.dialog` hatte `overflow: hidden`, eine Scroll-Höhe gab es NUR für
`.dialog--breit`. Alles unterhalb des Rands wurde abgeschnitten --
samt "Abbrechen". Jetzt rollt JEDES Fenster, und Kopf wie Knopfzeile
bleiben stehen (`position: sticky`), damit man den Ausgang SIEHT,
ohne erst durch acht Felder zu scrollen. Alle vier Größen: ja.
2. DIE VORLAGENKACHEL DREHT SICH.
„wenn ich drauf drücke dreht sich die kachel und dan seh ich wer es
gemacht hat, und immer noch die option es nochmal zu verteilen falls
neue leute ins team zustoßen."
Vorne bleibt die Kurzfassung ("liegt bei 3 von 4"), hinten stehen
die Namen mit ihrem Stand und zwei Knöpfe: "Nachholen – 1 fehlt"
(oder "Nochmal an alle", wenn wirklich alle sie haben) und "Zurück".
Nach dem Verteilen dreht sie sich von selbst; wer nur nachsehen
will, drückt "Wer hat sie?".
3. DER EINGANG SIEHT AUS WIE DAS HAUS.
Fase und Leuchtschiene statt flachem Kasten, die Schiene in der
Farbe des Stands. Die drei Zahlen werden drei Felder -- und die
"0 neu" leuchtet nicht mehr rot: Eine Warnung, die immer kommt, ist
keine Warnung. Ab 760 px steht das Bild neben dem Text statt
darunter; die Karte war dadurch dreimal so hoch wie nötig.
4. DER CREATOR-KATALOG IST AUF DER TEAM-SEITE WEG.
„es gibt keine creator auf dieser seite" -- dort stand "Wähle oben
einen Creator", eine Aufforderung zu etwas Unmöglichem. Gefragt wird
jetzt nach den Daten (gibt es jemanden, dem ich das geben kann?),
nicht nach der Adresse.
DAZU FERTIG GEMACHT, WAS VON GESTERN OFFEN WAR:
* Die zwei Serien ohne Haus ("Community-Call", "Schulung-Agentur").
Ursache war meine eigene Abschrift: Bei den Terminen frage ich die
Teilnehmerliste, bei den Serien hatte ich sie vergessen. Auf einer
Kopie der echten Datenbank: 0 offene Zeilen.
* Sieben Schreibwege setzen jetzt `haus` (Aufgaben, Einträge,
Dateien, Material, Wissen, Video-Titelbild). Dabei gefunden:
`material` verwaltet seine Spalten SELBST -- meine Spalte stand in
der falschen Liste und fehlte auf einer frischen Datenbank
(78 Fehlschläge in pruef-material, jetzt 159/0).
* unterstuetzen.html lud meldung.js gar nicht -- dort stand das
Maschinenwort des Servers statt eines Satzes (pruef-meldungen 8/0).
DREI VERALTETE PRÜFUNGEN NACHGEZOGEN, jede STRENGER als vorher:
* "der Modi legt eine Aufgabe an (201)" -- seit dem 22.09. ist das
403 und gewollt. Geprüft wird jetzt auch das WORT.
* "calls.html ist verboten" -- Filipe hat die Kachel selbst verlangt
("jeder der einen kalender hat"). Mit Gegenprobe ersetzt.
* "Review" heißt seit dem 20.09. "Zur Freigabe". Der Name wird jetzt
aus STATUS_NAME GELESEN statt abgeschrieben.
GEPRÜFT: modi-katalog 150/0 (war 144), modi-verborgen 85/0 (war 80/2),
haus-trennung 97/0, material 159/0, meldungen 8/0, abbrechen-optik 0
Fehler. Dazu grün: an-alle, vorlagen, support, css-klassen,
aufgabenbrett, aufgaben-vorlagen, unterstuetzung, formulare, loeschen,
nachfrage, kalender, chat, leerzustand.
OFFEN UND NICHT ANGEFASST: pruef-breiten meldet auf report.html ein
Berührziel von 27x18 px. Der Link (`class="zurueck"`) ist auf 30
Seiten derselbe und hat gar keinen eigenen Stil; beanstandet wird nur
diese eine Seite, weil dort hinter ihm nur "· Review" steht und die
Prüfung Fließtext-Links erst ab 12 Zeichen Umgebung ausnimmt. Eine
Klasse auf 30 Seiten ohne Prüflauf zu ändern wäre geraten.
Co-Authored-By: Claude Opus 5 <[email protected]>
265 lines
12 KiB
JavaScript
265 lines
12 KiB
JavaScript
/* =====================================================================
|
|
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. Deshalb `mess-` und nicht `pruef-`: Die
|
|
Portvergabe leitet sich aus den `pruef-`-Namen ab, und eine
|
|
Messdatei darf die Nummern der Pruefungen nicht verschieben.
|
|
|
|
---------------------------------------------------------------------
|
|
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";
|
|
|
|
const PORT = 5397, HP = 5398;
|
|
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";
|
|
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");
|
|
execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes", "-keyout", schl,
|
|
"-out", zert, "-days", "2", "-subj", `/CN=${CREW}`, "-addext", `subjectAltName=DNS:${CREW}`],
|
|
{ stdio: "ignore" });
|
|
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 } });
|
|
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);
|
|
}
|