Rund um das Team -- die Arbeitslage von Managern und Scouts
Filipe: "so eine kategorie wie ueber die creator will ich dass nur fuer
die spicy und dogfather rolle auch ueber manager und scouts gibt ...
ich will dass es so ultra krass gut ist dass die spicy und dogfather
rolle einen kompletten teil haben mit daten ueber die arbeit von den
manager und scout. keine geheimen sachen also termine, chats und
geheime dateien soll auch so bleiben dass keiner."
--- ZUERST DIE KOPFLEISTE ---
Filipe meldete, die Kopfleiste sei bei DogFather "nicht gemacht".
Nachgemessen auf ALLEN 18 Seiten, in allen fuenf Rollen, bei drei
Breiten: einzeilig, Spanne 4 px. Und die neuen Dateien liegen
nachweislich auf dem Server (a70bc4f, `abmelden__zeichen` in der
ausgelieferten kopf.js). Das Bild war vor dem Ausliefern entstanden.
Die Messung hat aber zwei echte Sachen gefunden, die vorher niemand
gesehen hatte -- beide bei 320 px auf UNTERseiten, wo links der
Zurueck-Knopf und rechts zusaetzlich die Glocke steht: 305 px
gebraucht, 294 verfuegbar. Eine Stufe kleiner (34 px je Knopf, 4 px
Abstand) macht 283 und passt; 34 px bleiben weit ueber den 24 px
Mindestmass fuer ein Beruehrziel.
Ausserdem die Auslieferung geordnet: HTML wird immer nachgefragt,
Dateien mit Versionsstempel duerfen ein Jahr liegenbleiben (vorher
bekam ALLES `no-cache`, also auch jede Stilvorlage bei jedem Aufruf).
sw.js ausgenommen -- er wird ohne Stempel geladen, ein Fehler darin
bliebe sonst ein Jahr stehen.
--- DIE NEUE SEITE ---
workspace/team.html, nur fuer `spicy` und `admin`. Aufbau:
DIE LUECKEN ZUERST. Creator ohne Betreuung, Scouts ohne Manager,
Leute ohne einen einzigen Creator -- mit NAMEN, nicht nur als Zahl.
Eine Kennzahl sagt, wie es laeuft; eine Luecke sagt, wo etwas fehlt,
und nur das Zweite kann man heute abstellen.
DANN DIE LAGE in fuenf Zahlen, dann JEDE PERSON EINZELN: betreute
Creator, laufende und ueberfaellige Aufgaben, in 30 Tagen erledigte,
Durchlaufzeit, Startcheck-Fortschritt der betreuten Creator,
LIVE-Tage und Diamanten. Bei Scouts zusaetzlich die Pipeline mit
Uebernahmequote und Zeit bis zur Uebergabe.
EIN MANAGER TRAEGT DIE CREATOR SEINER SCOUTS MIT. Ohne das saehe
einer mit fuenf Scouts aus wie jemand ohne Arbeit.
--- DREI ENTSCHEIDUNGEN, DIE ALLES TRAGEN ---
1. TERMINE, CHATS UND DATEIEN KOMMEN NICHT VOR -- weder Inhalte noch
Zaehlungen. Ausdruecklicher Wunsch, und der richtige: Ein Kalender
verraet, wann jemand nicht da war; ein Chatzaehler, mit wem jemand
oft spricht.
Das ist keine Zusicherung im Kommentar. pruef-team liest den
Quelltext von workspace-team.js und schlaegt an, wenn eine dieser
Tabellen darin auftaucht -- mit Gegenprobe, dass die Suche `aufgaben`
und `leads` auch wirklich findet. Der Weg ueber die Antwort allein
waere schwaecher: Ein leerer Testbestand kann ein Feld verstecken.
2. SEGMENTIEREN, NICHT MITTELN. Aus der Recherche zu
Arbeitslast-Dashboards: Ein Durchschnitt versteckt genau die Person,
bei der es klemmt. Markiert wird gegen den MEDIAN der eigenen Rolle
-- ein Manager traegt naturgemaess mehr als ein Scout, und ihn daran
zu messen waere unfair und nutzlos.
3. ES IST EINE ARBEITSLAGE, KEINE UEBERWACHUNG. Das steht so auf der
Seite, im Kopf, in einem eigenen Kasten. Wer das nicht dazuschreibt,
baut ein Kontrollwerkzeug, auch wenn er es nicht wollte. Deshalb
zeigen die Kennzahlen auf ZUSTAENDE (unbetreute Creator,
liegengebliebene Kontakte) und nicht auf Anwesenheit oder Fleiss.
--- WAS DIE MESSUNG UNTERWEGS GEFUNDEN HAT ---
* Die Lead-Status hiessen anders, als ich angenommen hatte: "kontakt"
gibt es nicht. Die CHECK-Bedingung der Datenbank hat es sofort
abgelehnt -- ohne sie waere "offen" still zu klein gewesen.
* Die Pruefung fand ihr eigenes Hinweisschild: Die Antwort traegt ein
Feld `ausgenommen: ["Termine","Chats","Dateien"]`, aus dem die Seite
den Satz baut. Es wird jetzt herausgenommen UND eigens geprueft --
ignorieren waere bequem gewesen und haette kuenftig jedes Feld unter
diesem Namen durchgelassen.
* Die Lektion vom Vorlagenbrett gleich mitgenommen: alle Karten haben
einen DECKENDEN Grund. Eine Karte mit sieben Prozent Farbe auf
durchsichtigem Grund laesst das Buehnenfoto durch -- dort waren es
3,61:1. Gemessen jetzt: 6,61 bis 14,80:1. Eine der Regeln hatte den
deckenden Grund selbst wieder aufgehoben (zwei Regeln, die spaetere
gewinnt) -- gefunden, bevor es jemand sehen musste.
* "1 Scouts" statt "1 Scout". Eine Kleinigkeit, und das Erste, was
auffaellt: Eine Seite, die ihre eigene Sprache nicht beherrscht, wird
auch bei den Zahlen nicht geglaubt.
--- Pruefung ---
server/pruef-team.mjs, neu, 51 Pruefungen, alle gruen. Darunter: alle
fuenf Rollen an Schnittstelle UND Seite (Manager, Scout und Creator
bekommen 404 bzw. eine Umleitung), Spicy sieht VanVan nicht (mit
Gegenprobe an der Personenliste), die Zahlen an einem gebauten
Bestand, acht Kontrastmessungen an der wirklichen Flaeche, Handy.
server/pruef-zwischenspeicher.mjs, neu, 15 Pruefungen: was liegenbleiben
darf und was nicht -- an echten Kopfzeilen gemessen, nicht am
Quelltext. Sie meldete zuerst drei Fehler, und das war sie selbst: Sie
fragte unangemeldet und bekam Umleitungen. Eine Pruefung braucht ihre
Voraussetzung, bevor sie misst.
Ausserdem gruen: pruef-handy (das Handy fand die 320-px-Sache),
pruef-workspace-seiten, pruef-alle-wege, pruef-sicht, pruef-css-klassen.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+401
-290
@@ -1,72 +1,72 @@
|
||||
/* Prueft zwei Wuensche vom 01.09.2026:
|
||||
/* DIE ARBEITSLAGE VON MANAGERN UND SCOUTS (09.09.2026)
|
||||
|
||||
SCREEN 2: "ich will, dass wir Manager, Scouts, DogFather auch die
|
||||
Fotos, Namen und so alles sehen, alles was die in ihrem Profil
|
||||
eintragen."
|
||||
Filipe: "so eine kategorie wie über die creator will ich dass nur für
|
||||
die spicy und dogfather rolle auch über manager und scouts gibt ...
|
||||
keine geheimen sachen also termine, chats und geheime dateien soll
|
||||
auch so bleiben dass keiner."
|
||||
|
||||
SCREEN 5: "wieso seh ich mein Bild da nicht, obwohl ich schon ein Foto
|
||||
drin hab?"
|
||||
---------------------------------------------------------------------
|
||||
DER WICHTIGSTE ABSCHNITT IST DER ERSTE, NICHT DER LETZTE
|
||||
|
||||
WAS AN DIESER PRUEFUNG WICHTIG IST.
|
||||
Diese Seite zeigt Zahlen über Kollegen. Zwei Dinge daran können
|
||||
schiefgehen, und beide wären schwer wieder gutzumachen:
|
||||
|
||||
Ein Profilbild zu pruefen, ohne ein echtes Bild zu haben, beweist
|
||||
nichts. Beim .wer__bild-Fehler im August benutzte die Pruefung ein
|
||||
1x1-Pixel-PNG -- damit kann kein Stil-Fehler sichtbar werden, weil
|
||||
ein Punkt nichts ueberdecken kann. Hier wird deshalb ein RICHTIGES
|
||||
Bild hochgeladen (600x600) und danach gemessen, ob es auch die
|
||||
Flaeche fuellt, die es fuellen soll -- und ob es rund beschnitten ist
|
||||
statt quadratisch ueber den Rahmen zu ragen.
|
||||
1. Sie zeigt sie den Falschen. Ein Manager, der die Auslastung
|
||||
seiner Kollegen sieht, hat eine Auskunft, die ihm nicht gehört.
|
||||
2. Sie zeigt etwas, das nicht hineingehört -- Termine, Chats,
|
||||
Dateien.
|
||||
|
||||
Und zu jeder Aussage die Gegenprobe: Wer KEIN Bild hat, muss den
|
||||
Buchstaben sehen. Sonst koennte die Pruefung gruen sein, weil
|
||||
ueberall Bilder erscheinen -- auch da, wo keine sind. */
|
||||
Punkt 2 wird hier auf ZWEI Wegen geprüft: an der Antwort (keine
|
||||
Felder mit diesen Namen) UND am Quelltext (die Datei nennt diese
|
||||
Tabellen nicht). Der zweite Weg ist der stärkere: Er hält auch dann,
|
||||
wenn jemand morgen ein Feld hinzufügt, das im Testbestand zufällig
|
||||
leer bleibt.
|
||||
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
DREI AUSGÄNGE: 0 in Ordnung · 1 Befunde · 2 konnte nicht arbeiten. */
|
||||
|
||||
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { deflateSync } from "node:zlib";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const ordner = mkdtempSync(join(tmpdir(), "ws-team-"));
|
||||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||||
/* DATEN_ORDNER wird aus dem Datenbankpfad abgeleitet (workspace.js) --
|
||||
die Profilbilder landen also von selbst im Testordner. */
|
||||
process.env.PORT = "4213";
|
||||
|
||||
import { notbremse } from "./helfer-notbremse.mjs";
|
||||
const { portMussFreiSein } = await import("./helfer-port.mjs");
|
||||
await portMussFreiSein(4345, "die Team-Pruefung");
|
||||
|
||||
process.env.PORT = "4345";
|
||||
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
||||
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
|
||||
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");
|
||||
/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort
|
||||
zwei Auffangnetze ein, die einen Absturz nur protokollieren --
|
||||
richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese
|
||||
Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der
|
||||
Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */
|
||||
notbremse(480_000, "pruef-team");
|
||||
await new Promise((r) => setTimeout(r, 700));
|
||||
const BASIS = "http://127.0.0.1:4213";
|
||||
setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 300_000).unref?.();
|
||||
notbremse(300_000, "pruef-team");
|
||||
await new Promise((r) => setTimeout(r, 900));
|
||||
const BASIS = "http://127.0.0.1:4345";
|
||||
|
||||
let fehler = 0;
|
||||
const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||||
let fehler = 0, geprueft = 0;
|
||||
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||||
|
||||
/* ---- Bestand ------------------------------------------------------------ */
|
||||
const { DatabaseSync } = await import("node:sqlite");
|
||||
const { scryptSync, randomBytes } = await import("node:crypto");
|
||||
await fetch(BASIS + "/workspace/api/anmelden", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
|
||||
|
||||
const { DatabaseSync } = await import("node:sqlite");
|
||||
const { scryptSync, randomBytes, createHash } = await import("node:crypto");
|
||||
for (let i = 0; i < 60; i++) {
|
||||
try {
|
||||
const p = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
try { const p = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); }
|
||||
} catch { await new Promise((r) => setTimeout(r, 250)); }
|
||||
}
|
||||
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
const jetzt = new Date().toISOString();
|
||||
const tagT = (v) => { const x = new Date(Date.now() + v * 86400000);
|
||||
return `${x.getFullYear()}-${String(x.getMonth() + 1).padStart(2, "0")}`
|
||||
+ `-${String(x.getDate()).padStart(2, "0")}`; };
|
||||
function anlegen(name, rolle, code) {
|
||||
const salt = randomBytes(16).toString("hex");
|
||||
const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
||||
@@ -75,267 +75,378 @@ function anlegen(name, rolle, code) {
|
||||
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||||
}
|
||||
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
||||
const idMgr = anlegen("Mara", "manager", "CODE-MANA-0001");
|
||||
const idScout = anlegen("Patrick", "scout", "CODE-SCOU-0001");
|
||||
const idSpicy = anlegen("Cigdem", "spicy", "CODE-SPIC-0001");
|
||||
const idVan = anlegen("VanVan", "admin", "CODE-VANV-0001"); /* der zweite Admin */
|
||||
const idMara = anlegen("Mara", "manager", "CODE-MANA-0001");
|
||||
const idPat = anlegen("Patrick", "scout", "CODE-SCOU-0001");
|
||||
const idBen = anlegen("Ben", "scout", "CODE-SCOB-0001");
|
||||
const idTili = anlegen("Tili", "creator", "CODE-ANNA-0001");
|
||||
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)")
|
||||
.run(idTili, idScout, jetzt);
|
||||
const idLuna = anlegen("Luna", "creator", "CODE-LUNA-0001");
|
||||
const idRoy = anlegen("Roy", "creator", "CODE-ROYY-0001");
|
||||
const idFrei = anlegen("Nio", "creator", "CODE-NIOO-0001"); /* ohne Betreuung */
|
||||
|
||||
/* Jeder traegt etwas in seinen Steckbrief ein -- sonst koennte die
|
||||
Pruefung nicht unterscheiden, ob "alles was die eintragen" wirklich
|
||||
ankommt oder ob nur Namen angezeigt werden. */
|
||||
d.prepare("UPDATE personen SET ueber_mich = ?, tiktok = ? WHERE id = ?")
|
||||
.run("Manager & CEO dieser Seite", "dogfather", idDogi);
|
||||
d.prepare("UPDATE personen SET ueber_mich = ?, instagram = ? WHERE id = ?")
|
||||
.run("Ich kuemmere mich um die Vertraege.", "mara.mgmt", idMgr);
|
||||
d.prepare("UPDATE personen SET ueber_mich = ? WHERE id = ?")
|
||||
.run("Ich finde neue Leute.", idScout);
|
||||
/* Tili bekommt BEWUSST nichts -- die Gegenprobe zum Platzhaltersatz. */
|
||||
/* Patrick betreut Tili und Luna, Ben betreut Roy, Mara fuehrt Patrick.
|
||||
Nio hat NIEMANDEN -- das ist die Luecke, die die Seite zeigen soll. */
|
||||
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idTili, idPat, jetzt);
|
||||
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idLuna, idPat, jetzt);
|
||||
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idRoy, idBen, jetzt);
|
||||
d.prepare("INSERT INTO scout_zuteilung (scout_id, manager_id, seit) VALUES (?,?,?)").run(idPat, idMara, jetzt);
|
||||
|
||||
const auf = (titel, creator, status, frist) =>
|
||||
d.prepare(`INSERT INTO aufgaben (titel, beschreibung, status, prioritaet, frist,
|
||||
creator_id, verantwortlich_id, erstellt, erstellt_von, erledigt_am)
|
||||
VALUES (?,?,?,'mittel',?,?,?,?,?,?)`)
|
||||
.run(titel, "", status, frist, creator, creator, jetzt, idDogi,
|
||||
status === "erledigt" ? jetzt : null);
|
||||
auf("T1", idTili, "offen", tagT(-3)); /* ueberfaellig */
|
||||
auf("T2", idTili, "arbeit", tagT(5));
|
||||
auf("T3", idLuna, "erledigt", tagT(-1));
|
||||
auf("T4", idRoy, "offen", tagT(2));
|
||||
|
||||
d.prepare(`INSERT INTO leads (name, plattform, handle, status, scout_id, erstellt, erstellt_von,
|
||||
naechster_followup, uebergeben_am) VALUES (?,?,?,?,?,?,?,?,?)`)
|
||||
.run("Kandidat A", "tiktok", "@a", "angesprochen", idPat, jetzt, idPat, tagT(-2), null);
|
||||
d.prepare(`INSERT INTO leads (name, plattform, handle, status, scout_id, erstellt, erstellt_von,
|
||||
naechster_followup, uebergeben_am) VALUES (?,?,?,?,?,?,?,?,?)`)
|
||||
.run("Kandidat B", "tiktok", "@b", "uebergeben", idPat, jetzt, idPat, null, jetzt);
|
||||
d.close();
|
||||
|
||||
/* ---- Ein echtes PNG, 600x600, einfarbig. Kein 1x1-Punkt: Der kann
|
||||
nichts ueberdecken und wuerde jeden Stil-Fehler verstecken. ---- */
|
||||
function pngBauen(breite, hoehe) {
|
||||
const roh = Buffer.alloc((breite * 3 + 1) * hoehe);
|
||||
for (let y = 0; y < hoehe; y++) {
|
||||
const z = y * (breite * 3 + 1);
|
||||
roh[z] = 0;
|
||||
for (let x = 0; x < breite; x++) {
|
||||
roh[z + 1 + x * 3] = 200; roh[z + 2 + x * 3] = 60; roh[z + 3 + x * 3] = 120;
|
||||
}
|
||||
const keks = async (rolle, code) => {
|
||||
const a = await fetch(BASIS + "/workspace/api/anmelden", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ rolle, code }) });
|
||||
return (a.headers.getSetCookie?.() || []).map((k) => k.split(";")[0]).join("; ");
|
||||
};
|
||||
const kDogi = await keks("admin", "CODE-DOGI-0001");
|
||||
const kSpicy = await keks("spicy", "CODE-SPIC-0001");
|
||||
const kMara = await keks("manager", "CODE-MANA-0001");
|
||||
const kPat = await keks("scout", "CODE-SCOU-0001");
|
||||
const kTili = await keks("creator", "CODE-ANNA-0001");
|
||||
const ruf = (pfad, k) => fetch(BASIS + pfad, { headers: { cookie: k }, redirect: "manual" });
|
||||
|
||||
/* =======================================================================
|
||||
1. WER DARF HINEIN -- und wer nicht
|
||||
======================================================================= */
|
||||
console.log("\n=== Wer darf hinein");
|
||||
{
|
||||
for (const [name, k, soll] of [
|
||||
["DogFather", kDogi, 200], ["Spicy Media", kSpicy, 200],
|
||||
["Manager", kMara, 404], ["Scout", kPat, 404], ["Creator", kTili, 404],
|
||||
]) {
|
||||
const a = await ruf("/workspace/api/team", k);
|
||||
ok(a.status === soll, `${name}: HTTP ${a.status} (erwartet ${soll})`);
|
||||
}
|
||||
const stueck = (art, daten) => {
|
||||
const l = Buffer.alloc(4); l.writeUInt32BE(daten.length);
|
||||
const k = Buffer.concat([Buffer.from(art), daten]);
|
||||
const c = Buffer.alloc(4);
|
||||
c.writeUInt32BE(crc32(k) >>> 0);
|
||||
return Buffer.concat([l, k, c]);
|
||||
};
|
||||
const ihdr = Buffer.alloc(13);
|
||||
ihdr.writeUInt32BE(breite, 0); ihdr.writeUInt32BE(hoehe, 4);
|
||||
ihdr[8] = 8; ihdr[9] = 2; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0;
|
||||
return Buffer.concat([
|
||||
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
||||
stueck("IHDR", ihdr), stueck("IDAT", deflateSync(roh)), stueck("IEND", Buffer.alloc(0)),
|
||||
]);
|
||||
}
|
||||
let tabelle = null;
|
||||
function crc32(buf) {
|
||||
if (!tabelle) {
|
||||
tabelle = new Int32Array(256);
|
||||
for (let n = 0; n < 256; n++) {
|
||||
let c = n;
|
||||
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
||||
tabelle[n] = c;
|
||||
}
|
||||
/* Ohne Anmeldung ueberhaupt nichts. */
|
||||
const ohne = await fetch(BASIS + "/workspace/api/team", { redirect: "manual" });
|
||||
ok(ohne.status === 401, `ohne Anmeldung: HTTP ${ohne.status}`);
|
||||
|
||||
/* Und die SEITE ebenso -- eine offene Seite mit gesperrter
|
||||
Schnittstelle waere eine leere Seite und eine offene Frage. */
|
||||
for (const [name, k, offen] of [
|
||||
["DogFather", kDogi, true], ["Spicy Media", kSpicy, true],
|
||||
["Manager", kMara, false], ["Scout", kPat, false], ["Creator", kTili, false],
|
||||
]) {
|
||||
const a = await ruf("/workspace/team.html", k);
|
||||
const drin = a.status === 200;
|
||||
ok(drin === offen, `team.html für ${name}: HTTP ${a.status}`);
|
||||
}
|
||||
let c = -1;
|
||||
for (const b of buf) c = tabelle[(c ^ b) & 0xff] ^ (c >>> 8);
|
||||
return c ^ -1;
|
||||
}
|
||||
const BILD = pngBauen(600, 600);
|
||||
|
||||
/* ---------------------------------------------------------------- */
|
||||
/* =======================================================================
|
||||
2. WAS NICHT HINEINGEHÖRT -- Termine, Chats, Dateien
|
||||
|
||||
const { chromium } = await import(
|
||||
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
||||
const browser = await chromium.launch();
|
||||
Auf zwei Wegen geprüft. Der zweite ist der stärkere.
|
||||
======================================================================= */
|
||||
console.log("\n=== Termine, Chats und Dateien bleiben draußen");
|
||||
{
|
||||
const daten = await (await ruf("/workspace/api/team", kDogi)).json();
|
||||
|
||||
async function anmelden(rolle, code) {
|
||||
const kontext = await browser.newContext({ viewport: { width: 1280, height: 1000 } });
|
||||
/* DAS HINWEISSCHILD WIRD AUSGENOMMEN -- und zwar ausdrücklich.
|
||||
|
||||
Die Antwort trägt ein Feld `ausgenommen: ["Termine","Chats",
|
||||
"Dateien"]`; daraus schreibt die Seite den Satz, der dem Betrachter
|
||||
sagt, was hier NICHT steht. Beim ersten Lauf hat die Wortsuche
|
||||
genau dieses Schild gefunden und Alarm geschlagen.
|
||||
|
||||
Es einfach zu ignorieren wäre die bequeme Lösung und eine schlechte:
|
||||
Dann könnte künftig jedes beliebige Feld unter diesem Namen
|
||||
durchrutschen. Stattdessen wird es HERAUSGENOMMEN und danach
|
||||
eigens geprüft -- was es enthalten darf, steht dann fest. */
|
||||
const { ausgenommen, ...ohneSchild } = daten;
|
||||
const text = JSON.stringify(ohneSchild).toLowerCase();
|
||||
const verboten = ["termin", "chat", "datei", "nachricht", "anhang"];
|
||||
const gefunden = verboten.filter((w) => text.includes(w));
|
||||
ok(gefunden.length === 0,
|
||||
gefunden.length ? `in der Antwort steht: ${gefunden.join(", ")}`
|
||||
: `kein einziges dieser Wörter in der Antwort (${verboten.join(", ")})`);
|
||||
ok(Array.isArray(ausgenommen) && ausgenommen.join(",") === "Termine,Chats,Dateien",
|
||||
`und das Schild nennt genau die drei (${(ausgenommen || []).join(", ")})`);
|
||||
|
||||
/* GEGENPROBE ZUR WORTSUCHE: Sie muss ein verbotenes Wort auch finden.
|
||||
Ohne sie hiesse "nichts gefunden" nur, dass nicht gesucht wurde. */
|
||||
ok(verboten.some((w) => JSON.stringify({ x: "Termine am Montag" }).toLowerCase().includes(w)),
|
||||
"Gegenprobe: die Wortsuche schlägt bei einem verbotenen Wort an");
|
||||
|
||||
/* DER STÄRKERE WEG: der Quelltext selbst. Ein leerer Testbestand
|
||||
kann ein Feld verstecken; eine Abfrage, die es gar nicht gibt,
|
||||
kann nichts liefern. */
|
||||
const HIER = dirname(fileURLToPath(import.meta.url));
|
||||
const quelle = readFileSync(join(HIER, "workspace-team.js"), "utf8");
|
||||
/* Nur der CODE, nicht die Kommentare -- dort steht ausdrücklich, was
|
||||
ausgenommen ist, und das soll dort auch stehen bleiben. */
|
||||
const nurCode = quelle.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
|
||||
const tabellen = ["termine", "termin_teilnehmer", "chat_nachrichten", "chat_raeume",
|
||||
"chat_teilnehmer", "dateien", "datei_personen"];
|
||||
const benutzt = tabellen.filter((t) => new RegExp(`\\b${t}\\b`).test(nurCode));
|
||||
ok(benutzt.length === 0,
|
||||
benutzt.length ? `der Quelltext fragt ab: ${benutzt.join(", ")}`
|
||||
: `keine dieser ${tabellen.length} Tabellen wird abgefragt`);
|
||||
|
||||
/* GEGENPROBE: Die Suche findet eine Tabelle, die WIRKLICH benutzt
|
||||
wird. Ohne sie hiesse "nichts gefunden" nur, dass die Suche nicht
|
||||
sucht. */
|
||||
ok(/\baufgaben\b/.test(nurCode) && /\bleads\b/.test(nurCode),
|
||||
"Gegenprobe: `aufgaben` und `leads` werden gefunden — die Suche sucht wirklich");
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
3. STIMMEN DIE ZAHLEN?
|
||||
======================================================================= */
|
||||
console.log("\n=== Die Zahlen");
|
||||
{
|
||||
const daten = await (await ruf("/workspace/api/team", kDogi)).json();
|
||||
const p = (name) => daten.leute.find((x) => x.name === name);
|
||||
|
||||
ok(daten.leute.length === 3,
|
||||
`drei Leute: Manager und zwei Scouts (${daten.leute.map((x) => x.name).join(", ")})`);
|
||||
ok(!daten.leute.some((x) => x.rolle === "creator" || x.rolle === "admin"),
|
||||
"keine Creator und keine Admins in der Liste — die Seite ist für Manager und Scouts");
|
||||
|
||||
const pat = p("Patrick");
|
||||
ok(pat?.creator.length === 2, `Patrick betreut zwei Creator (${pat?.creator.length})`);
|
||||
ok(pat?.aufgaben.ueberfaellig === 1,
|
||||
`eine überfällige Aufgabe bei seinen Creators (${pat?.aufgaben.ueberfaellig})`);
|
||||
ok(pat?.aufgaben.laufend === 2, `zwei laufende (${pat?.aufgaben.laufend})`);
|
||||
|
||||
/* DER MANAGER TRÄGT DIE CREATOR SEINER SCOUTS MIT.
|
||||
Ohne das sähe ein Manager mit fünf Scouts aus wie jemand ohne
|
||||
Arbeit -- und genau das wäre die falsche Auskunft. */
|
||||
const mara = p("Mara");
|
||||
ok(mara?.creator.length === 0 && mara?.creator_mittelbar === 2,
|
||||
`Mara betreut niemanden direkt, aber zwei über Patrick `
|
||||
+ `(${mara?.creator.length} / ${mara?.creator_mittelbar})`);
|
||||
ok(mara?.aufgaben.ueberfaellig === 1,
|
||||
`und sieht deren überfällige Aufgabe (${mara?.aufgaben.ueberfaellig})`);
|
||||
ok(mara?.scouts.includes("Patrick"), `mit Namen des Scouts (${mara?.scouts.join(", ")})`);
|
||||
|
||||
/* Die Pipeline gibt es nur bei Scouts. */
|
||||
ok(pat?.pipeline && pat.pipeline.gesamt === 2 && pat.pipeline.uebergeben === 1,
|
||||
`Patricks Pipeline: ${pat?.pipeline?.gesamt} Kontakte, ${pat?.pipeline?.uebergeben} übergeben`);
|
||||
ok(pat?.pipeline?.faellig === 1,
|
||||
`davon einer überfällig im Nachfassen (${pat?.pipeline?.faellig})`);
|
||||
ok(mara?.pipeline === null, "ein Manager hat keine Pipeline — und bekommt auch keine leere");
|
||||
|
||||
/* ---- DIE LÜCKEN ---- */
|
||||
ok(daten.luecken.creator_ohne_betreuung.some((c) => c.name === "Nio"),
|
||||
`Nio steht als Creator ohne Betreuung da `
|
||||
+ `(${daten.luecken.creator_ohne_betreuung.map((c) => c.name).join(", ")})`);
|
||||
ok(daten.luecken.scouts_ohne_manager.some((s) => s.name === "Ben"),
|
||||
`Ben steht als Scout ohne Manager da `
|
||||
+ `(${daten.luecken.scouts_ohne_manager.map((s) => s.name).join(", ")})`);
|
||||
/* GEGENPROBE: Wer versorgt ist, taucht NICHT auf. */
|
||||
ok(!daten.luecken.creator_ohne_betreuung.some((c) => c.name === "Tili")
|
||||
&& !daten.luecken.scouts_ohne_manager.some((s) => s.name === "Patrick"),
|
||||
"und die versorgten stehen nicht darin");
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
4. SPICY MEDIA SIEHT VANVAN NICHT
|
||||
|
||||
Die Hausregel gilt auch hier -- und zwar über dieselbe zentrale
|
||||
Funktion, nicht über eine eigene Fassung.
|
||||
======================================================================= */
|
||||
console.log("\n=== Niemand sieht VanVan außer DogFather");
|
||||
{
|
||||
const alsSpicy = await (await ruf("/workspace/api/team", kSpicy)).json();
|
||||
const text = JSON.stringify(alsSpicy);
|
||||
ok(!text.includes("VanVan"), "in der Spicy-Ansicht steht VanVan nirgends");
|
||||
/* Sie ist Admin und stünde ohnehin nicht in der Liste -- geprüft wird
|
||||
deshalb, dass die Regel überhaupt greift: DogFather sieht sie in
|
||||
der Personenliste, Spicy nicht. */
|
||||
const beiDogi = await (await ruf("/workspace/api/verwaltung/personen", kDogi)).json();
|
||||
const beiSpicy = await (await ruf("/workspace/api/verwaltung/personen", kSpicy)).json();
|
||||
const nDogi = (beiDogi.personen || []).some((x) => x.name === "VanVan");
|
||||
const nSpicy = (beiSpicy.personen || []).some((x) => x.name === "VanVan");
|
||||
ok(nDogi && !nSpicy,
|
||||
`Gegenprobe an der Personenliste: DogFather sieht sie (${nDogi}), Spicy nicht (${nSpicy})`);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
5. IM BROWSER
|
||||
======================================================================= */
|
||||
console.log("\n=== Auf der Seite");
|
||||
{
|
||||
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
||||
const browser = await pw.chromium.launch();
|
||||
const konsole = [];
|
||||
const seite = await kontext.newPage();
|
||||
seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); });
|
||||
seite.on("pageerror", (e) => konsole.push("pageerror: " + e.message));
|
||||
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
||||
await seite.click(`.rolle[data-rolle="${rolle}"]`);
|
||||
await seite.fill("#code", code);
|
||||
await seite.click("button[type=submit]");
|
||||
await seite.waitForURL("**/start.html", { timeout: 15000 });
|
||||
return { kontext, seite, konsole };
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 1200 } });
|
||||
const s = await ctx.newPage();
|
||||
s.on("pageerror", (f) => konsole.push("Absturz: " + f.message));
|
||||
s.on("console", (m) => { if (m.type() === "error") konsole.push("Konsole: " + m.text()); });
|
||||
await s.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
||||
await s.click('.rolle[data-rolle="admin"]');
|
||||
await s.fill("#code", "CODE-DOGI-0001");
|
||||
await s.click("button[type=submit]");
|
||||
await s.waitForURL("**/start.html", { timeout: 20000 });
|
||||
await s.waitForTimeout(1200);
|
||||
|
||||
/* Die Kachel steht auf der Startseite und führt hin. */
|
||||
const kachel = await s.evaluate(() => {
|
||||
const k = [...document.querySelectorAll(".kachel")]
|
||||
.find((x) => /Team-Lage/.test(x.textContent));
|
||||
return k ? { da: true, ziel: k.getAttribute("href") || k.dataset.ziel || "",
|
||||
gruppe: k.closest(".gruppe")?.textContent?.slice(0, 40) } : { da: false };
|
||||
});
|
||||
ok(kachel.da, `die Kachel steht auf der Startseite (Gruppe: ${kachel.gruppe})`);
|
||||
|
||||
await s.goto(BASIS + "/workspace/team.html", { waitUntil: "networkidle" });
|
||||
await s.waitForTimeout(1500);
|
||||
const m = await s.evaluate(() => ({
|
||||
leute: document.querySelectorAll(".tperson").length,
|
||||
zahlen: document.querySelectorAll(".tz").length,
|
||||
luecken: document.querySelectorAll(".tl").length,
|
||||
pipelines: document.querySelectorAll(".tpipe").length,
|
||||
grenze: document.querySelector(".t-kopf__grenze")?.textContent?.trim().slice(0, 60),
|
||||
warnungen: document.querySelectorAll('.tz[data-art="warn"]').length,
|
||||
ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
||||
}));
|
||||
ok(m.leute === 3, `drei Personenkarten (${m.leute})`);
|
||||
ok(m.pipelines === 2, `zwei Pipelines — nur die Scouts (${m.pipelines})`);
|
||||
ok(m.luecken >= 2, `die Lücken stehen oben (${m.luecken} Kästen)`);
|
||||
ok(m.warnungen > 0, `und was liegengeblieben ist, ist markiert (${m.warnungen})`);
|
||||
ok(/Termine, Chats und Dateien/.test(m.grenze || ""),
|
||||
`der Kopf sagt, was NICHT darin steht ("${m.grenze}…")`);
|
||||
ok(m.ueber <= 1, `nichts steht seitlich über (${m.ueber} px)`);
|
||||
if (process.env.BILD === "ja") {
|
||||
await s.screenshot({ path: "_team-gross.png", fullPage: true });
|
||||
}
|
||||
|
||||
/* Am Handy ebenfalls. */
|
||||
const ctx2 = await browser.newContext({ viewport: { width: 390, height: 900 } });
|
||||
const s2 = await ctx2.newPage();
|
||||
await s2.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
||||
await s2.click('.rolle[data-rolle="admin"]');
|
||||
await s2.fill("#code", "CODE-DOGI-0001");
|
||||
await s2.click("button[type=submit]");
|
||||
await s2.waitForURL("**/start.html", { timeout: 20000 });
|
||||
await s2.goto(BASIS + "/workspace/team.html", { waitUntil: "networkidle" });
|
||||
await s2.waitForTimeout(1400);
|
||||
const h = await s2.evaluate(() => ({
|
||||
ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
||||
leute: document.querySelectorAll(".tperson").length,
|
||||
breiter: [...document.querySelectorAll(".tperson, .tz, .tl")]
|
||||
.filter((e) => e.getBoundingClientRect().right > 391).length,
|
||||
}));
|
||||
ok(h.ueber <= 1 && h.breiter === 0,
|
||||
`am Handy nichts über (${h.ueber} px, ${h.breiter} zu breit) bei ${h.leute} Karten`);
|
||||
if (process.env.BILD === "ja") {
|
||||
await s2.screenshot({ path: "_team-handy.png", fullPage: true });
|
||||
}
|
||||
await ctx2.close();
|
||||
|
||||
/* ---- LESBAR, NICHT NUR DA (09.09.2026) --------------------------
|
||||
Die Lektion vom Vorlagenbrett: Eine Karte mit sieben Prozent Farbe
|
||||
auf durchsichtigem Grund laesst das Buehnenfoto durch, und dann
|
||||
haengt die Lesbarkeit davon ab, welche Stelle des Fotos gerade
|
||||
dahinterliegt. Dort waren es 3,61:1, wo 4,5 noetig sind.
|
||||
|
||||
Gemessen wird gegen die WIRKLICHE Flaeche: Der Text wird kurz
|
||||
unsichtbar gemacht und die Flaeche fotografiert. */
|
||||
{
|
||||
const leuchtkraft = (r, g, b) => {
|
||||
const f = (w) => { const v = w / 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); };
|
||||
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
|
||||
};
|
||||
const kontrast = (a, b) => {
|
||||
const l1 = leuchtkraft(...a), l2 = leuchtkraft(...b);
|
||||
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
||||
};
|
||||
async function messen(waehler) {
|
||||
const stelle = await s.evaluate((w) => {
|
||||
const e = document.querySelector(w);
|
||||
if (!e) return null;
|
||||
e.scrollIntoView({ block: "center" });
|
||||
const r = e.getBoundingClientRect();
|
||||
if (r.width < 8 || r.height < 6) return null;
|
||||
/* Die Farbe wird GEMALT und zurueckgelesen -- getComputedStyle
|
||||
liefert bei color-mix() "color(srgb ...)", und ein Muster
|
||||
ueber die Ziffern liest daraus Unsinn. */
|
||||
const c = document.createElement("canvas");
|
||||
c.width = 1; c.height = 1;
|
||||
const g = c.getContext("2d", { willReadFrequently: true });
|
||||
g.fillStyle = "#000000";
|
||||
g.fillStyle = getComputedStyle(e).color;
|
||||
g.fillRect(0, 0, 1, 1);
|
||||
const px = g.getImageData(0, 0, 1, 1).data;
|
||||
e.dataset.messen = "ja";
|
||||
e.style.color = "transparent";
|
||||
return { x: Math.round(r.x), y: Math.round(r.y),
|
||||
w: Math.round(r.width), h: Math.round(Math.min(r.height, 40)),
|
||||
farbe: [px[0], px[1], px[2]] };
|
||||
}, waehler);
|
||||
if (!stelle) return null;
|
||||
const bild = await s.screenshot({
|
||||
clip: { x: stelle.x, y: stelle.y, width: stelle.w, height: stelle.h } });
|
||||
const hinten = await s.evaluate(async (b64) => {
|
||||
const bild = new Image();
|
||||
await new Promise((r) => { bild.onload = r; bild.src = "data:image/png;base64," + b64; });
|
||||
const c = document.createElement("canvas");
|
||||
c.width = bild.width; c.height = bild.height;
|
||||
const g = c.getContext("2d", { willReadFrequently: true });
|
||||
g.drawImage(bild, 0, 0);
|
||||
const dd = g.getImageData(0, 0, c.width, c.height).data;
|
||||
let r = 0, gr = 0, bl = 0, n = 0;
|
||||
for (let i = 0; i < dd.length; i += 4) { r += dd[i]; gr += dd[i + 1]; bl += dd[i + 2]; n++; }
|
||||
return [Math.round(r / n), Math.round(gr / n), Math.round(bl / n)];
|
||||
}, bild.toString("base64"));
|
||||
await s.evaluate(() => {
|
||||
const e = document.querySelector('[data-messen="ja"]');
|
||||
if (e) { e.style.color = ""; delete e.dataset.messen; }
|
||||
});
|
||||
return { farbe: stelle.farbe, hinten, wert: kontrast(stelle.farbe, hinten) };
|
||||
}
|
||||
for (const [name, waehler] of [
|
||||
["Lücken-Titel", ".tl__titel"],
|
||||
["Lücken-Namen", ".tl__namen"],
|
||||
["Kennzahl", ".tz__wert"],
|
||||
["Beschriftung", ".tz__schild"],
|
||||
["Kennzahl (Warnung)", '.tz[data-art="warn"] .tz__wert'],
|
||||
["Name", ".tperson__name"],
|
||||
["Zuletzt da", ".tperson__zuletzt"],
|
||||
["Grenzsatz", ".t-kopf__grenze"],
|
||||
]) {
|
||||
const m = await messen(waehler);
|
||||
ok(m !== null && m.wert >= 4.5,
|
||||
`${name}: ${m ? m.wert.toFixed(2) + ":1 (" + m.farbe.join(",") + " auf " + m.hinten.join(",") + ")" : "nicht gefunden"}`);
|
||||
}
|
||||
/* GEGENPROBE: ein absichtlich zu dunkler Text MUSS durchfallen. */
|
||||
await s.evaluate(() => { document.querySelector(".tz__wert").style.color = "#131820"; });
|
||||
const schlecht = await messen(".tz__wert");
|
||||
ok(schlecht !== null && schlecht.wert < 4.5,
|
||||
`absichtlich zu dunkler Text fällt durch (${schlecht?.wert.toFixed(2)}:1)`);
|
||||
await s.evaluate(() => { document.querySelector(".tz__wert").style.color = ""; });
|
||||
}
|
||||
|
||||
ok(konsole.length === 0, konsole.length ? "Konsole: " + konsole[0] : "keine Fehler in der Konsole");
|
||||
/* GEGENPROBE zum Lauscher. */
|
||||
await s.evaluate(() => { setTimeout(() => { throw new Error("Absicht"); }, 0); });
|
||||
await s.waitForTimeout(400);
|
||||
ok(konsole.some((z) => /Absicht/.test(z)), "ein absichtlicher Fehler wird bemerkt");
|
||||
await ctx.close();
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
1. Ein echtes Bild hochladen -- und pruefen, dass es ueberall ankommt
|
||||
================================================================ */
|
||||
console.log("\n=== Das eigene Bild (Screen 5)");
|
||||
const a1 = await anmelden("admin", "CODE-DOGI-0001");
|
||||
|
||||
const hoch = await a1.seite.evaluate(async (b64) => {
|
||||
const roh = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
||||
const a = await fetch("/workspace/api/steckbrief/bild", {
|
||||
method: "POST", credentials: "same-origin",
|
||||
headers: { "Content-Type": "image/png" }, body: roh,
|
||||
});
|
||||
return { status: a.status, ...(await a.json().catch(() => ({}))) };
|
||||
}, BILD.toString("base64"));
|
||||
ok(hoch.status === 200 && !!hoch.bild, `ein echtes Bild (600x600) laesst sich hochladen (${hoch.status})`);
|
||||
|
||||
/* Kommt es auf der STARTSEITE in der Begruessung an? Das war der
|
||||
gemeldete Fehler: Dort stand der Buchstabe, obwohl ein Foto da war --
|
||||
und im Server steht seit jeher der Kommentar "es steht in der
|
||||
Kopfleiste JEDER Seite UND IN DER BEGRUESSUNG". Die Absicht war
|
||||
aufgeschrieben, die Haelfte nie gebaut. */
|
||||
await a1.seite.goto(BASIS + "/workspace/start.html", { waitUntil: "networkidle" });
|
||||
await a1.seite.waitForTimeout(1500);
|
||||
const gruss = await a1.seite.evaluate(() => {
|
||||
const p = document.getElementById("plakette");
|
||||
const b = p?.querySelector("img");
|
||||
if (!b) return { bild: false, text: p?.textContent || "" };
|
||||
const r = b.getBoundingClientRect();
|
||||
const pr = p.getBoundingClientRect();
|
||||
const st = getComputedStyle(b);
|
||||
const ps = getComputedStyle(p);
|
||||
return {
|
||||
bild: true,
|
||||
geladen: b.complete && b.naturalWidth > 0,
|
||||
/* Fuellt es die Plakette wirklich aus? Ein Bild, das nur zu einem
|
||||
Viertel drinsteht, sieht aus wie ein Fehler. */
|
||||
fuellt: r.width >= pr.width - 2 && r.height >= pr.height - 2,
|
||||
/* Rund beschnitten -- ohne overflow:hidden ragt ein quadratisches
|
||||
Bild aus dem runden Rahmen. */
|
||||
beschnitten: ps.overflow === "hidden",
|
||||
passung: st.objectFit,
|
||||
kein_text: (p.textContent || "").trim() === "",
|
||||
};
|
||||
});
|
||||
ok(gruss.bild === true, "die Begruessung zeigt das Bild statt des Buchstabens");
|
||||
ok(gruss.geladen === true, " und es ist wirklich geladen (nicht nur ein leerer Rahmen)");
|
||||
ok(gruss.fuellt === true, " es fuellt die Plakette aus");
|
||||
ok(gruss.beschnitten === true, " und ist rund beschnitten (overflow: hidden)");
|
||||
ok(gruss.passung === "cover", ` ohne zu verzerren (object-fit: ${gruss.passung})`);
|
||||
ok(gruss.kein_text === true, " der Buchstabe steht nicht mehr daneben");
|
||||
|
||||
/* ================================================================
|
||||
2. Das Team -- alle Rollen, mit allem, was sie eingetragen haben
|
||||
================================================================ */
|
||||
console.log("\n=== Das Team (Screen 2)");
|
||||
await a1.seite.goto(BASIS + "/workspace/steckbrief.html", { waitUntil: "networkidle" });
|
||||
await a1.seite.waitForTimeout(1600);
|
||||
|
||||
const team = await a1.seite.evaluate(() => {
|
||||
const block = document.getElementById("team-block");
|
||||
const karten = [...document.querySelectorAll(".team-karte")];
|
||||
const sichtbar = (e) => {
|
||||
const r = e.getBoundingClientRect();
|
||||
return r.width > 0 && r.height > 0;
|
||||
};
|
||||
return {
|
||||
da: block && !block.hidden,
|
||||
/* IST DER STIL ANGEKOMMEN? Die .block__*-Klassen stehen in
|
||||
automation.css und wissen.css -- diese Seite laedt beide nicht.
|
||||
Genau so sind hier schon zweimal Stile ins Leere gelaufen.
|
||||
Gemessen wird deshalb der tatsaechliche Stil, nicht die Klasse. */
|
||||
/* SEIT DEM 07.09.2026 IST DER RAND KEINE `border` MEHR, sondern
|
||||
eine Silhouette mit abgeschnittener Ecke aus module.css. Die
|
||||
alte Bedingung (border >= 1) wurde dadurch zwangsläufig rot,
|
||||
ohne dass ein Stil fehlte. Die FRAGE bleibt aber genau dieselbe
|
||||
und ist so wichtig wie zuvor: Ist der Stil auf DIESER Seite
|
||||
angekommen? Gemessen wird sie jetzt an der neuen Bauweise --
|
||||
Polsterung wie bisher, dazu die Silhouette, die es nur gibt,
|
||||
wenn module.css wirklich geladen wurde. */
|
||||
gestaltet: karten[0] ? (() => {
|
||||
const st = getComputedStyle(karten[0]);
|
||||
return parseFloat(st.paddingLeft) >= 8 && /polygon/.test(st.clipPath);
|
||||
})() : null,
|
||||
anzahl: karten.length,
|
||||
sichtbare: karten.filter(sichtbar).length,
|
||||
gruppen: [...document.querySelectorAll(".team-gruppe__name")].map((e) => e.textContent),
|
||||
leute: karten.map((k) => ({
|
||||
name: k.querySelector(".team-karte__name")?.textContent,
|
||||
rolle: k.dataset.rolle,
|
||||
rollenname: k.querySelector(".team-karte__rolle")?.textContent,
|
||||
ueber: k.querySelector(".team-karte__ueber")?.textContent || null,
|
||||
bild: !!k.querySelector(".team-karte__bild img"),
|
||||
buchstabe: k.querySelector(".team-karte__bild img")
|
||||
? null : (k.querySelector(".team-karte__bild")?.textContent || "").trim(),
|
||||
kanaele: [...k.querySelectorAll(".team-kanal")].map((a) => a.getAttribute("href")),
|
||||
ich: k.dataset.ich === "ja",
|
||||
})),
|
||||
ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
||||
};
|
||||
});
|
||||
|
||||
ok(team.da === true, "der Team-Block erscheint");
|
||||
ok(team.gestaltet === true, " und die Stile sind angekommen (Rand und Polsterung gemessen)");
|
||||
ok(team.anzahl === 4 && team.sichtbare === 4,
|
||||
`alle vier Personen stehen da (${team.anzahl}, davon ${team.sichtbare} sichtbar)`);
|
||||
ok(team.gruppen.join(", ") === "Management, Scouting, Creator",
|
||||
`nach Rollen gruppiert: ${team.gruppen.join(", ")}`);
|
||||
|
||||
const dogi = team.leute.find((l) => l.name === "Filipe");
|
||||
const mara = team.leute.find((l) => l.name === "Mara");
|
||||
const pat = team.leute.find((l) => l.name === "Patrick");
|
||||
const tili = team.leute.find((l) => l.name === "Tili");
|
||||
|
||||
ok(!!dogi && !!mara && !!pat && !!tili,
|
||||
"Manager, Scout, DogFather UND Creator sind dabei – nicht nur Creator");
|
||||
ok(dogi?.bild === true, "DogFather wird mit seinem Foto gezeigt");
|
||||
ok(dogi?.ueber === "Manager & CEO dieser Seite",
|
||||
` und mit dem, was er ueber sich geschrieben hat ("${dogi?.ueber}")`);
|
||||
ok(dogi?.ich === true, " seine eigene Karte ist als solche gekennzeichnet");
|
||||
ok(/tiktok\.com/.test(dogi?.kanaele?.[0] || ""),
|
||||
` sein Kanal fuehrt wirklich hin (${dogi?.kanaele?.[0]})`);
|
||||
ok(mara?.ueber === "Ich kuemmere mich um die Vertraege.", "der Manager-Text kommt an");
|
||||
ok(pat?.ueber === "Ich finde neue Leute.", "der Scout-Text kommt an");
|
||||
ok(mara?.rollenname === "Manager" && pat?.rollenname === "Scout",
|
||||
`die Rolle steht dabei (${mara?.rollenname}, ${pat?.rollenname})`);
|
||||
|
||||
/* GEGENPROBEN -- ohne sie waere oben alles auch dann gruen, wenn die
|
||||
Seite einfach ueberall Bilder und Texte hinschriebe. */
|
||||
ok(mara?.bild === false && mara?.buchstabe === "M",
|
||||
`Gegenprobe: wer kein Foto hat, bekommt den Buchstaben ("${mara?.buchstabe}")`);
|
||||
ok(tili?.ueber === null,
|
||||
"Gegenprobe: wer nichts ueber sich geschrieben hat, bekommt keinen Platzhaltersatz");
|
||||
ok(tili?.kanaele?.length === 0,
|
||||
"Gegenprobe: ohne Kanal steht dort auch keiner");
|
||||
ok(mara?.ich === false, "Gegenprobe: fremde Karten sind nicht als 'du' markiert");
|
||||
ok(team.ueber <= 0, `nichts steht seitlich ueber (${team.ueber}px)`);
|
||||
ok(a1.konsole.length === 0,
|
||||
`keine Fehler in der Konsole${a1.konsole.length ? " -> " + a1.konsole.join(" | ") : ""}`);
|
||||
|
||||
await a1.seite.screenshot({ path: "pruef-team.png", fullPage: false });
|
||||
await a1.kontext.close();
|
||||
|
||||
/* ================================================================
|
||||
3. Ein Creator sieht seine Betreuung -- aber nicht die anderen Creator
|
||||
================================================================ */
|
||||
console.log("\n=== Was ein Creator davon sieht");
|
||||
const a2 = await anmelden("creator", "CODE-ANNA-0001");
|
||||
await a2.seite.goto(BASIS + "/workspace/profil.html", { waitUntil: "networkidle" });
|
||||
await a2.seite.waitForTimeout(1600);
|
||||
const sicht = await a2.seite.evaluate(() => ({
|
||||
da: !document.getElementById("team-block")?.hidden,
|
||||
namen: [...document.querySelectorAll(".team-karte__name")].map((e) => e.textContent).sort(),
|
||||
}));
|
||||
ok(sicht.da === true, "ein Creator sieht das Team auf seiner Profilseite");
|
||||
ok(sicht.namen.includes("Patrick"), " seinen Scout");
|
||||
ok(sicht.namen.includes("Filipe") && sicht.namen.includes("Mara"),
|
||||
" und das Management");
|
||||
ok(a2.konsole.length === 0,
|
||||
`keine Fehler in der Konsole${a2.konsole.length ? " -> " + a2.konsole.join(" | ") : ""}`);
|
||||
await a2.kontext.close();
|
||||
|
||||
/* Ein zweiter Creator, um zu pruefen, dass Creator einander NICHT sehen.
|
||||
Das ist kein Schoenheitsfehler: Der Server entscheidet das, und wenn
|
||||
sich die Regel je aendert, muss es hier auffallen. */
|
||||
const d2 = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
const salt = randomBytes(16).toString("hex");
|
||||
const hash = scryptSync("CODE-LUNA-0001", salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
||||
d2.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
|
||||
.run("Luna", "creator", hash, salt, 32768, jetzt);
|
||||
d2.close();
|
||||
|
||||
const a3 = await anmelden("creator", "CODE-ANNA-0001");
|
||||
await a3.seite.goto(BASIS + "/workspace/profil.html", { waitUntil: "networkidle" });
|
||||
await a3.seite.waitForTimeout(1600);
|
||||
const fremd = await a3.seite.evaluate(() =>
|
||||
[...document.querySelectorAll(".team-karte__name")].map((e) => e.textContent));
|
||||
ok(!fremd.includes("Luna"),
|
||||
`Gegenprobe: ein Creator sieht einen fremden Creator NICHT (${fremd.join(", ")})`);
|
||||
await a3.kontext.close();
|
||||
|
||||
await browser.close();
|
||||
console.log(`\n${geprueft} Prüfungen`);
|
||||
console.log(fehler ? `${fehler} FEHLER` : "ALLES IN ORDNUNG");
|
||||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
||||
console.log(fehler ? `\n${fehler} Prüfung(en) fehlgeschlagen.` : "\nAlles in Ordnung.");
|
||||
process.exit(fehler ? 1 : 0);
|
||||
|
||||
Reference in New Issue
Block a user