Die Personenkachel wird eine Standkarte -- mit den Aufgaben darauf
Filipe: "mach das bitte viel krasser und viel geiler. die aufgaben die
wir verteilen sollen da auch zu sehen sein und so. viel
uebersichtlicher, viel moderner. nicht so banal und einfach."
VORHER: Name, Rolle, ein flacher Balken und drei Zahlenkaesten. Auf
seinem Bildschirmfoto standen in fuenf von sechs Kacheln exakt
dieselben drei Zahlen (0 / 68 / 0) -- achtzehn Kaesten fuer eine
einzige Auskunft. Und die Aufgaben, die er eine Handbreit darunter
verteilt, kamen auf der Kachel derselben Person gar nicht vor.
JETZT drei Zonen in der Reihenfolge, in der man fragt:
KOPF Ring mit dem Anteil in der Mitte, Name, Rolle.
AUFTRAG Wie viele offen, wie viele ueberfaellig, und die naechste
mit Namen und Frist ("seit gestern ueberfaellig").
FUSS "46 von 68 angesehen" -- und "verschieden gesehen" nur
dann, wenn es dort etwas gibt.
Der Ring rechnet mit stroke-dasharray auf einem Kreis vom Umfang 100:
der Anteil in Prozent ist damit buchstaeblich die Strichlaenge. Keine
Ampelfarbe -- was "genug" ist, haengt davon ab, wie lange jemand dabei
ist. Gewarnt wird nur an einem Massstab, der nicht geraten ist: einer
ueberschrittenen Frist.
Die Aufgabenzeile wird aus derselben Liste gerechnet wie das
Verteil-Band und der Katalog (alleAufgabenV) und in aufgabenHolen()
nachgezogen -- an EINER Stelle, damit es keine gibt, die es vergisst.
GITTER STATT FLIESSREIHE: Bei sieben Leuten stand die letzte Kachel
allein in ihrer Zeile und wuchs auf 1160 px neben 379 px der anderen.
Eine Person sah dreimal so wichtig aus, weil die Teamgroesse ungerade
ist.
Drei Funde nebenbei, alle durch die neuen Messungen:
- pruef-schritt verlangte seit dem 23.09. einen Sprung, der an dem
Tag ABSICHTLICH entfernt wurde. Sie war seither rot, ohne dass
etwas kaputt war. Neu gefasst auf den Sprung, den es noch gibt:
den Weg von aussen ueber "?zeigen=person-...".
- Und der war kaputt. Gesprungen wurde, waehrend in der Karte nur
"wird geladen ..." stand -- die Seite war zu kurz zum Scrollen,
der Browser klemmte bei 0 ab. Wer der Talentseite folgte, landete
oben auf der Liste. Gesprungen wird jetzt, wenn die Karte steht.
- Auf demselben Weg rief karte() das Vorlagenbrett, bevor es
eingerichtet war ("zeigen() vor einrichten()"). Das Einrichten ist
vorgezogen. Ueber "?zeigen=" ging vorher KEINE Pruefung.
Gemessen: pruef-entwicklung-kacheln 31 (vorher 16), pruef-schritt 68
(vorher 65), pruef-entwicklung 48, pruef-modi-katalog 133,
pruef-bewerbung-aufgaben 101, pruef-zuteilung, pruef-css-klassen --
alle gruen. Schriftgroessen unter 11,5 px: 40 statt 42.
Neu: server/mess-entwicklung-kacheln.mjs. Die Pruefung braucht einen
kargen Bestand und zeigt deshalb den Sonderfall (alles auf null); diese
Datei legt sieben Leute mit verschiedenen Staenden und Aufgaben an und
macht Bilder fuer 1280 und 390 px. Sie prueft nichts.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
/* =====================================================================
|
||||
WIE SIEHT DIE NEUE PERSONENKACHEL AUS? (24.09.2026)
|
||||
|
||||
Filipe: "mach das bitte viel krasser und viel geiler. die aufgaben
|
||||
die wir verteilen sollen da auch zu sehen sein und so. viel
|
||||
uebersichtlicher, viel moderner."
|
||||
|
||||
---------------------------------------------------------------------
|
||||
WARUM ES DIESE DATEI GIBT, OBWOHL pruef-entwicklung-kacheln EXISTIERT
|
||||
|
||||
Die Pruefung nebenan misst, ob die Kachel STIMMT -- ob Ring, Ziffer,
|
||||
Zeile und Vorleseschild dieselbe Zahl sagen. Dafuer braucht sie
|
||||
einen kargen Bestand: zwei Leute, alles auf null.
|
||||
|
||||
Genau dadurch zeigt ihr Bildschirmfoto den Sonderfall, nicht den
|
||||
Alltag: zwei leere Ringe nebeneinander. Ob ein Ring bei 68 Prozent
|
||||
gut aussieht, ob eine Kachel mit drei ueberfaelligen Aufgaben noch
|
||||
ruhig wirkt, ob sechs Kacheln am Handy uebersichtlich bleiben --
|
||||
davon steht dort nichts.
|
||||
|
||||
Diese Datei legt deshalb einen Bestand an, der wie der echte
|
||||
aussieht: sechs Leute, jeder anders weit, jeder mit anderen
|
||||
Aufgaben. Sie prueft NICHTS und meldet keinen Fehlschlag. Sie macht
|
||||
Bilder, an denen man entscheiden kann.
|
||||
|
||||
Sie heisst deshalb `mess-` und nicht `pruef-`: Die Portvergabe
|
||||
leitet sich aus den `pruef-`-Namen ab, und eine Messdatei darf die
|
||||
Nummern der Pruefungen nicht verschieben.
|
||||
|
||||
Aufrufen mit: node server/mess-entwicklung-kacheln.mjs
|
||||
Bilder: server/mess-entwicklung-kacheln-breit.png
|
||||
server/mess-entwicklung-kacheln-handy.png
|
||||
===================================================================== */
|
||||
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const PORT = 5399;
|
||||
const ordner = mkdtempSync(join(tmpdir(), "ws-messkachel-"));
|
||||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||||
process.env.PORT = String(PORT);
|
||||
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");
|
||||
notbremse(180_000, "mess-entwicklung-kacheln");
|
||||
await new Promise((r) => setTimeout(r, 700));
|
||||
const BASIS = `http://127.0.0.1:${PORT}`;
|
||||
|
||||
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 { ENTWICKLUNG_FREMD } = await import("./workspace-entwicklungs-punkte.js");
|
||||
|
||||
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
const jetzt = new Date().toISOString();
|
||||
const schluessel = randomBytes(32).toString("hex");
|
||||
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
|
||||
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
||||
.run("code_kennung_schluessel", schluessel, jetzt);
|
||||
|
||||
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");
|
||||
d.prepare("INSERT INTO personen"
|
||||
+ " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
|
||||
+ " VALUES (?,?,?,?,?,?,1,?)")
|
||||
.run(name, rolle, hash, salt, 32768,
|
||||
createHmac("sha256", schluessel).update(code).digest("hex"), jetzt);
|
||||
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||||
}
|
||||
|
||||
const ich = anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
||||
const leute = [
|
||||
{ name: "VanVan", rolle: "hand", anteil: 1.00, aufgaben: 0, spaet: 0 },
|
||||
{ name: "Kessi", rolle: "linke", anteil: 0.68, aufgaben: 3, spaet: 1 },
|
||||
{ name: "Rieke", rolle: "modi", anteil: 0.22, aufgaben: 5, spaet: 2 },
|
||||
{ name: "Nala", rolle: "modi", anteil: 0.00, aufgaben: 1, spaet: 0 },
|
||||
{ name: "Juli", rolle: "modi", anteil: 0.47, aufgaben: 0, spaet: 0 },
|
||||
{ name: "Momo", rolle: "modi", anteil: 0.91, aufgaben: 2, spaet: 0 },
|
||||
];
|
||||
for (const p of leute) p.id = anlegen(p.name, p.rolle, `CODE-${p.name.toUpperCase()}-1`);
|
||||
|
||||
/* Eine zweite Stimme -- nur damit "verschieden gesehen" ueberhaupt
|
||||
entstehen kann. Sie braucht keine Kachel. */
|
||||
const zweite = anlegen("Mira", "linke", "CODE-MIRA-0001");
|
||||
|
||||
/* ---- Der Stand: wie weit ist Filipe bei wem? -----------------------
|
||||
|
||||
ERST ANMELDEN, DANN SCHREIBEN. `entwicklung_stand` entsteht nicht
|
||||
beim Start, sondern beim ersten Aufruf der Entwicklungsseite
|
||||
(tabellen() in workspace-entwicklung.js). Wer vorher hineinschreibt,
|
||||
bekommt "no such table" -- genau der Fehler, den die Pruefung dort
|
||||
schon einmal gefunden hat. */
|
||||
d.close();
|
||||
const vorlauf = await fetch(BASIS + "/workspace/api/anmelden", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ rolle: "admin", code: "CODE-DOGI-0001" }),
|
||||
});
|
||||
const vorKeks = (vorlauf.headers.getSetCookie?.() || [])
|
||||
.map((z) => z.split(";")[0]).join("; ");
|
||||
await fetch(BASIS + "/workspace/api/entwicklung/lage", { headers: { Cookie: vorKeks } });
|
||||
const d2 = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
|
||||
const setze = d2.prepare(
|
||||
"INSERT OR REPLACE INTO entwicklung_stand (person_id, schluessel, von_id, stufe, anlass, am)"
|
||||
+ " VALUES (?,?,?,?,NULL,?)");
|
||||
const alt = new Date(Date.now() - 90 * 86400000).toISOString();
|
||||
for (const p of leute) {
|
||||
const wie_viele = Math.round(ENTWICKLUNG_FREMD.length * p.anteil);
|
||||
ENTWICKLUNG_FREMD.slice(0, wie_viele).forEach((punkt, i) => {
|
||||
/* Ein paar alte Eintraege bei Kessi -- so steht auf ihrer Kachel
|
||||
auch die Zeile "x aelter als 60 Tage". */
|
||||
const wann = (p.name === "Kessi" && i < 4) ? alt : jetzt;
|
||||
setze.run(p.id, punkt.schluessel, ich, "laeuft", wann);
|
||||
});
|
||||
/* Zwei Punkte, die Mira anders sieht -- daraus wird "verschieden
|
||||
gesehen" auf Riekes Kachel. */
|
||||
if (p.name === "Rieke") {
|
||||
ENTWICKLUNG_FREMD.slice(0, 2).forEach((punkt) =>
|
||||
setze.run(p.id, punkt.schluessel, zweite, "hakt", jetzt));
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Die Aufgaben -------------------------------------------------- */
|
||||
const tagPlus = (n) => {
|
||||
const t = new Date(); t.setHours(12, 0, 0, 0); t.setDate(t.getDate() + n);
|
||||
return `${t.getFullYear()}-${String(t.getMonth() + 1).padStart(2, "0")}`
|
||||
+ `-${String(t.getDate()).padStart(2, "0")}`;
|
||||
};
|
||||
const TITEL = [
|
||||
"Clips vom Samstag schneiden", "Plan für nächste Woche", "Ideen sammeln",
|
||||
"Rückmeldung an die Neue", "Termine für Oktober eintragen",
|
||||
];
|
||||
const neueAufgabe = d2.prepare(
|
||||
"INSERT INTO aufgaben (titel, status, prioritaet, creator_id, verantwortlich_id, frist,"
|
||||
+ " erstellt, erstellt_von) VALUES (?,?,'mittel',?,?,?,?,?)");
|
||||
for (const p of leute) {
|
||||
for (let i = 0; i < p.aufgaben; i++) {
|
||||
const frist = i < p.spaet ? tagPlus(-(i + 1)) : (i === p.spaet ? tagPlus(i + 1) : null);
|
||||
neueAufgabe.run(TITEL[i % TITEL.length], "offen", ich, p.id, frist, jetzt, ich);
|
||||
}
|
||||
/* Eine erledigte je Person -- sie darf nicht mitzaehlen. */
|
||||
neueAufgabe.run("Längst abgehakt", "erledigt", ich, p.id, tagPlus(-9), jetzt, ich);
|
||||
}
|
||||
d2.close();
|
||||
|
||||
const an = await fetch(BASIS + "/workspace/api/anmelden", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ rolle: "admin", code: "CODE-DOGI-0001" }),
|
||||
});
|
||||
const keks = (an.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; ");
|
||||
|
||||
const { chromium } = await import(
|
||||
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
||||
const browser = await chromium.launch();
|
||||
try {
|
||||
for (const [wie, breite, hoehe] of [["breit", 1280, 1000], ["handy", 390, 1100]]) {
|
||||
const kontext = await browser.newContext({ viewport: { width: breite, height: hoehe } });
|
||||
await kontext.addCookies(keks.split("; ").map((k) => {
|
||||
const [name, ...rest] = k.split("=");
|
||||
return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" };
|
||||
}));
|
||||
const seite = await kontext.newPage();
|
||||
const konsole = [];
|
||||
seite.on("console", (e) => { if (e.type() === "error") konsole.push(e.text()); });
|
||||
await seite.goto(`${BASIS}/workspace/entwicklung.html`, { waitUntil: "networkidle" });
|
||||
await seite.waitForSelector("#personen .e-person", { timeout: 15_000 });
|
||||
/* Warten, bis die Aufgabenzeilen nachgezogen sind -- sie kommen aus
|
||||
einem zweiten Abruf, der nach dem Zeichnen laeuft. */
|
||||
await seite.waitForFunction(() =>
|
||||
[...document.querySelectorAll(".e-person__auftrag")].some((z) => z.textContent.trim()),
|
||||
{ timeout: 15_000 });
|
||||
await seite.waitForTimeout(600);
|
||||
|
||||
const block = await seite.$("#personen-block");
|
||||
await block.screenshot({ path: `server/mess-entwicklung-kacheln-${wie}.png` });
|
||||
console.log(` -- Bild (${breite} px): server/mess-entwicklung-kacheln-${wie}.png`);
|
||||
|
||||
const zahlen = await seite.evaluate(() =>
|
||||
[...document.querySelectorAll("#personen .e-person")].map((k) => ({
|
||||
name: k.querySelector(".e-person__name")?.textContent,
|
||||
ring: k.querySelector(".e-ring__zahl")?.textContent,
|
||||
bogen: k.querySelector(".e-ring__bogen")?.getAttribute("stroke-dasharray"),
|
||||
auftrag: (k.querySelector(".e-person__auftrag")?.textContent || "").trim(),
|
||||
fuss: (k.querySelector(".e-person__fuss")?.textContent || "").trim(),
|
||||
hoch: Math.round(k.getBoundingClientRect().height),
|
||||
breit: Math.round(k.getBoundingClientRect().width),
|
||||
})));
|
||||
if (wie === "breit") {
|
||||
console.log("");
|
||||
for (const z of zahlen) {
|
||||
console.log(` ${z.name.padEnd(8)} ${String(z.ring + "%").padStart(5)} `
|
||||
+ `[${z.bogen}] ${z.breit}x${z.hoch}px`);
|
||||
console.log(` ${z.auftrag.replace(/\s+/g, " ")}`);
|
||||
console.log(` ${z.fuss.replace(/\s+/g, " ")}`);
|
||||
}
|
||||
console.log("");
|
||||
} else {
|
||||
const proZeile = {};
|
||||
for (const z of zahlen) proZeile[z.breit] = (proZeile[z.breit] || 0) + 1;
|
||||
console.log(` -- Handy: Kachelbreiten ${Object.keys(proZeile).join(", ")} px, `
|
||||
+ `Hoehen ${[...new Set(zahlen.map((z) => z.hoch))].join(", ")} px`);
|
||||
}
|
||||
if (konsole.length) console.log(" !! Konsole: " + konsole[0]);
|
||||
await kontext.close();
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
||||
process.exit(0);
|
||||
}
|
||||
@@ -86,9 +86,44 @@ function anlegen(name, rolle, code) {
|
||||
createHmac("sha256", schluessel).update(code).digest("hex"), jetzt);
|
||||
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||||
}
|
||||
anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
||||
const idFilipe = anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
||||
anlegen("VanVan", "hand", "CODE-HAND-0001");
|
||||
anlegen("Diene", "modi", "CODE-MODI-0001");
|
||||
const idDiene = anlegen("Diene", "modi", "CODE-MODI-0001");
|
||||
|
||||
/* =====================================================================
|
||||
AUFGABEN FUER DEN AUFTRAGSSTREIFEN (24.09.2026)
|
||||
=====================================================================
|
||||
|
||||
Seit dem 24.09. traegt jede Kachel, was bei der Person liegt --
|
||||
Filipe: "die aufgaben die wir verteilen sollen da auch zu sehen sein
|
||||
und so."
|
||||
|
||||
Das laesst sich nur messen, wenn es etwas zu zeigen gibt. Drei
|
||||
Aufgaben fuer Diene, KEINE offene fuer VanVan -- so wird beides
|
||||
geprueft: die Zahl UND der Fall "nichts offen", der auf der Kachel
|
||||
ausdruecklich stehenbleibt.
|
||||
|
||||
DIE ERLEDIGTE IST DIE GEGENPROBE. Zaehlte der Streifen sie mit,
|
||||
stuende bei VanVan "1 offene Aufgabe" ueber einer abgehakten -- und
|
||||
niemand wuerde es merken, weil die Zahl ja plausibel aussieht.
|
||||
|
||||
GERECHNET WIRD RELATIV ZU HEUTE, nie mit einem festen Datum: Eine
|
||||
Frist, die beim Schreiben in der Zukunft lag, holt der Kalender
|
||||
sonst ein, und die Pruefung wird rot, ohne dass etwas kaputt ist.
|
||||
Mittags gesetzt, damit die Zeitumstellung den Tag nicht kippt. */
|
||||
const tagPlus = (n) => {
|
||||
const t = new Date(); t.setHours(12, 0, 0, 0); t.setDate(t.getDate() + n);
|
||||
return `${t.getFullYear()}-${String(t.getMonth() + 1).padStart(2, "0")}-${String(t.getDate()).padStart(2, "0")}`;
|
||||
};
|
||||
const GESTERN = tagPlus(-1), IN_DREI = tagPlus(3);
|
||||
const neueAufgabe = (titel, wer, frist, status) => d.prepare(
|
||||
"INSERT INTO aufgaben (titel, status, prioritaet, creator_id, verantwortlich_id, frist,"
|
||||
+ " erstellt, erstellt_von) VALUES (?,?,'mittel',?,?,?,?,?)")
|
||||
.run(titel, status, idFilipe, wer, frist, jetzt, idFilipe);
|
||||
neueAufgabe("Clips vom Samstag schneiden", idDiene, GESTERN, "offen");
|
||||
neueAufgabe("Plan für nächste Woche", idDiene, IN_DREI, "arbeit");
|
||||
neueAufgabe("Ideen sammeln", idDiene, null, "offen");
|
||||
neueAufgabe("Längst abgehakt", idDiene, GESTERN, "erledigt");
|
||||
d.close();
|
||||
|
||||
const an = await fetch(BASIS + "/workspace/api/anmelden", {
|
||||
@@ -126,64 +161,182 @@ try {
|
||||
schild: reihe.getAttribute("aria-label") || "",
|
||||
anzahl: kacheln.length,
|
||||
namen: kacheln.map((k) => k.querySelector(".e-person__name")?.textContent || ""),
|
||||
/* Der Hausbaustein, nicht ein Nachbau. */
|
||||
bilanzJe: kacheln.map((k) => k.querySelectorAll(".bilanz-zahl").length),
|
||||
eigeneKlassen: kacheln.map((k) => k.querySelectorAll(
|
||||
"[class*='e-person__bilanz'] > :not(.bilanz-zahl)").length),
|
||||
ringe: kacheln.map((k) => k.querySelectorAll(".e-ring").length),
|
||||
boegen: kacheln.map((k) => k.querySelectorAll(".e-ring__bogen").length),
|
||||
streifen: kacheln.map((k) => k.querySelectorAll(".e-person__auftrag").length),
|
||||
/* GEGENPROBE AUF DEN UMBAU: Die drei Bilanz-Kaesten sind am
|
||||
24.09.2026 gewichen. Blieben irgendwo Reste stehen, stuenden
|
||||
zwei Aufbauten nebeneinander -- genau das, was hier seit dem
|
||||
17.09. verhindert werden soll. */
|
||||
reste: kacheln.map((k) => k.querySelectorAll(
|
||||
".bilanz-zahl, .e-fortschritt, .e-person__bilanz").length),
|
||||
/* Die Kachel ist ein <button>. Ein <div> darin ist ungueltiges
|
||||
HTML -- es wird angezeigt und faellt trotzdem irgendwann auf. */
|
||||
divs: kacheln.map((k) => k.querySelectorAll("div").length),
|
||||
};
|
||||
});
|
||||
ok(b.schild === "Person wählen", `die Reihe sagt, was sie ist ("${b.schild}")`);
|
||||
ok(b.anzahl === 2, `zwei Kacheln (${b.namen.join(", ")})`);
|
||||
/* DREI KAESTEN JE KACHEL -- dieselben drei wie auf der Checkliste. */
|
||||
ok(b.bilanzJe.length > 0 && b.bilanzJe.every((n) => n === 3),
|
||||
`jede Kachel traegt drei Bilanz-Kaesten (${b.bilanzJe.join(", ")})`);
|
||||
/* UND NICHTS DANEBEN: Ein eigener Nachbau neben dem Hausbaustein
|
||||
waere genau die zweite Wahrheit, die hier vermieden wird. */
|
||||
ok(b.eigeneKlassen.every((n) => n === 0),
|
||||
"und nichts Selbstgebautes daneben");
|
||||
ok(b.ringe.length > 0 && b.ringe.every((n) => n === 1),
|
||||
`jede Kachel traegt genau einen Ring (${b.ringe.join(", ")})`);
|
||||
ok(b.boegen.every((n) => n === 1),
|
||||
`und in jedem Ring genau einen Bogen (${b.boegen.join(", ")})`);
|
||||
ok(b.streifen.length > 0 && b.streifen.every((n) => n === 1),
|
||||
`jede Kachel traegt ihren Auftragsstreifen (${b.streifen.join(", ")})`);
|
||||
ok(b.reste.every((n) => n === 0),
|
||||
`kein Rest der alten Kachel (Balken/Bilanz-Kaesten: ${b.reste.join(", ")})`);
|
||||
ok(b.divs.every((n) => n === 0),
|
||||
`nichts Ungueltiges im Knopf (divs: ${b.divs.join(", ")})`);
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
2. STIMMEN DIE ZAHLEN?
|
||||
===================================================================== */
|
||||
2. STIMMT DER RING MIT DEM, WAS DANEBEN STEHT?
|
||||
=====================================================================
|
||||
|
||||
Bis zum 24.09.2026 stand hier die Rechnung "angesehen + noch
|
||||
offen = alle Punkte". Die beiden Kaesten gibt es nicht mehr; die
|
||||
Frage dahinter bleibt aber dieselbe und ist jetzt sogar schaerfer
|
||||
zu stellen, weil DREI Darstellungen derselben Zahl nebeneinander
|
||||
liegen:
|
||||
|
||||
- der Bogen des Rings (stroke-dasharray, "68 100"),
|
||||
- die Ziffer in seiner Mitte,
|
||||
- die Zeile darunter ("46 von 68 angesehen"),
|
||||
- und das aria-label, das ein Vorleseprogramm vorliest.
|
||||
|
||||
Laufen die auseinander, sieht man es NICHT: Ein Ring, der zu drei
|
||||
Vierteln gefuellt ist, waehrend "12 %" darin steht, faellt auf --
|
||||
ein Ring, dessen aria-label etwas anderes sagt als das Bild, nie.
|
||||
Deshalb wird hier alles gegen dieselbe Quelle geprueft: die Zahlen
|
||||
aus /entwicklung/lage. */
|
||||
melde("");
|
||||
melde("=== 2. Die Zahlen ===");
|
||||
melde("=== 2. Der Ring und seine Zahlen ===");
|
||||
{
|
||||
const lage = await seite.evaluate(async () => {
|
||||
const a = await fetch("/workspace/api/entwicklung/lage", { credentials: "same-origin" });
|
||||
return a.ok ? await a.json() : null;
|
||||
});
|
||||
ok(lage && lage.gesamt_fremd > 0,
|
||||
`die Seite kennt ${lage?.gesamt_fremd ?? "?"} Punkte insgesamt`);
|
||||
|
||||
const z = await seite.evaluate(() => [...document.querySelectorAll("#personen .e-person")]
|
||||
.map((k) => {
|
||||
const werte = [...k.querySelectorAll(".bilanz-zahl")].map((b) => ({
|
||||
n: Number(b.querySelector(".bilanz-zahl__n")?.textContent || "x"),
|
||||
text: b.querySelector(".bilanz-zahl__text")?.textContent || "",
|
||||
stufe: b.dataset.stufe || "",
|
||||
}));
|
||||
return { name: k.querySelector(".e-person__name")?.textContent || "", werte };
|
||||
const ring = k.querySelector(".e-ring");
|
||||
return {
|
||||
name: k.querySelector(".e-person__name")?.textContent || "",
|
||||
nr: Number(k.dataset.person),
|
||||
ringZahl: k.querySelector(".e-ring__zahl")?.textContent || "",
|
||||
bogen: k.querySelector(".e-ring__bogen")?.getAttribute("stroke-dasharray") || "",
|
||||
schild: ring?.getAttribute("aria-label") || "",
|
||||
rolle: ring?.getAttribute("role") || "",
|
||||
standZahl: k.querySelector(".e-person__stand-zahl")?.textContent || "",
|
||||
standRest: k.querySelector(".e-person__stand-rest")?.textContent || "",
|
||||
abw: k.querySelector(".e-person__abw")?.textContent || "",
|
||||
};
|
||||
}));
|
||||
|
||||
ok(z.every((k) => k.werte.every((w) => Number.isFinite(w.n))),
|
||||
"jede Ziffer ist eine Zahl");
|
||||
ok(z.every((k) => k.werte[0].text === "angesehen"
|
||||
&& k.werte[1].text === "noch offen"
|
||||
&& k.werte[2].text === "verschieden gesehen"),
|
||||
`die drei heissen ueberall gleich (${z[0]?.werte.map((w) => w.text).join(" · ")})`);
|
||||
const gesamt = lage?.gesamt_fremd || 0;
|
||||
const sollAnteil = (p) => (gesamt > 0
|
||||
? Math.round((Number(p?.gesetzt) || 0) / gesamt * 100) : 0);
|
||||
const nachNr = new Map((lage?.personen || []).map((p) => [Number(p.id), p]));
|
||||
|
||||
/* DIE ENTSCHEIDENDE RECHNUNG: Die ersten beiden teilen dasselbe
|
||||
Ganze. Gehen sie nicht auf, stehen zwei Zahlen nebeneinander, die
|
||||
einander widersprechen -- genau das, was auf der Startseite
|
||||
repariert wurde. Die Gesamtzahl kommt aus der Auskunft selbst,
|
||||
nicht aus einer festen Zahl hier. */
|
||||
const gesamt = await seite.evaluate(async () => {
|
||||
const a = await fetch("/workspace/api/entwicklung/lage", { credentials: "same-origin" });
|
||||
return a.ok ? (await a.json()).gesamt_fremd : -1;
|
||||
ok(z.length > 0 && z.every((k) => nachNr.has(k.nr)),
|
||||
`jede Kachel gehoert zu einer Person aus der Auskunft (${z.map((k) => k.nr).join(", ")})`);
|
||||
|
||||
const ringOk = z.every((k) => k.ringZahl === String(sollAnteil(nachNr.get(k.nr))));
|
||||
ok(ringOk, `die Ziffer im Ring ist der gerechnete Anteil `
|
||||
+ `(${z.map((k) => `${k.name}: ${k.ringZahl}%`).join(", ")})`);
|
||||
|
||||
/* Der BOGEN ist das, was man sieht. Umfang 100 -- die erste Zahl
|
||||
im stroke-dasharray ist damit buchstaeblich der Prozentwert. */
|
||||
const bogenOk = z.every((k) =>
|
||||
Number(String(k.bogen).split(" ")[0]) === sollAnteil(nachNr.get(k.nr))
|
||||
&& String(k.bogen).split(" ")[1] === "100");
|
||||
ok(bogenOk, `und der Bogen zeigt denselben Anteil (${z.map((k) => k.bogen).join(" | ")})`);
|
||||
|
||||
/* DIE ZEILE DARUNTER nennt beide Zahlen, aus denen der Anteil
|
||||
entsteht -- ohne sie waere "68 %" eine Behauptung ohne Beleg. */
|
||||
const standOk = z.every((k) => {
|
||||
const p = nachNr.get(k.nr);
|
||||
return k.standZahl === String(Number(p?.gesetzt) || 0)
|
||||
&& k.standRest === ` von ${gesamt} angesehen`;
|
||||
});
|
||||
ok(gesamt > 0, `die Seite kennt ${gesamt} Punkte insgesamt`);
|
||||
const summenOk = z.every((k) => k.werte[0].n + k.werte[1].n === gesamt);
|
||||
ok(summenOk,
|
||||
`angesehen + noch offen = ${gesamt} bei jeder Kachel `
|
||||
+ `(${z.map((k) => `${k.name}: ${k.werte[0].n}+${k.werte[1].n}`).join(", ")})`);
|
||||
ok(standOk, `"x von ${gesamt} angesehen" stimmt bei jeder Kachel `
|
||||
+ `(${z.map((k) => k.standZahl + k.standRest).join(", ")})`);
|
||||
|
||||
/* Der Ton gehoert zur Aussage: fertig ist gruen, halb ist es nicht. */
|
||||
ok(z.every((k) => k.werte[0].stufe === (k.werte[1].n === 0 ? "gut" : "offen")),
|
||||
"fertig Angesehenes ist gruen, Halbes nicht");
|
||||
/* UND DAS VORLESEPROGRAMM HOERT DASSELBE. Ein Ring ist ein Bild;
|
||||
ohne Schild ist er fuer ein Vorleseprogramm gar nicht da. */
|
||||
ok(z.every((k) => k.rolle === "img"), "der Ring gibt sich als Bild zu erkennen");
|
||||
const schildOk = z.every((k) => {
|
||||
const p = nachNr.get(k.nr);
|
||||
const soll = `${Number(p?.gesetzt) || 0} von ${gesamt} angesehen `
|
||||
+ `(${sollAnteil(p)} Prozent)`;
|
||||
return k.schild === soll;
|
||||
});
|
||||
ok(schildOk, `und sagt dasselbe wie das Bild ("${z[0]?.schild}")`);
|
||||
|
||||
/* EINE NULL IST KEINE WARNUNG: "verschieden gesehen" steht nur da,
|
||||
wenn es etwas gibt. Im Testbestand hat niemand etwas anders
|
||||
gesehen -- also darf die Angabe auf KEINER Kachel stehen. */
|
||||
ok(z.every((k) => k.abw === ""),
|
||||
"ohne Abweichung steht dort auch nichts");
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
2b. WAS LIEGT BEI DER PERSON? -- der Auftragsstreifen
|
||||
=====================================================================
|
||||
|
||||
Filipe, 24.09.2026: "die aufgaben die wir verteilen sollen da auch
|
||||
zu sehen sein und so."
|
||||
|
||||
Angelegt sind (siehe oben): fuer Diene drei offene -- eine
|
||||
ueberfaellig, eine in drei Tagen, eine ohne Frist -- und eine
|
||||
erledigte. Fuer VanVan nichts.
|
||||
|
||||
GEPRUEFT WIRD BEIDES: die Zahl UND der Fall "nichts offen". Der
|
||||
zweite ist der, den man beim Bauen vergisst, und er ist auf dieser
|
||||
Seite der haeufigste: Wer verteilt, sucht genau danach. */
|
||||
melde("");
|
||||
melde("=== 2b. Der Auftragsstreifen ===");
|
||||
{
|
||||
const a = await seite.evaluate(() => Object.fromEntries(
|
||||
[...document.querySelectorAll("#personen .e-person")].map((k) => {
|
||||
const s2 = k.querySelector(".e-person__auftrag");
|
||||
return [k.querySelector(".e-person__name")?.textContent || "?", {
|
||||
sichtbar: !!s2 && !s2.hidden,
|
||||
leer: s2?.dataset.leer || "",
|
||||
spaet: s2?.dataset.spaet || "",
|
||||
zahl: k.querySelector(".e-person__auftrag-zahl")?.textContent || "",
|
||||
wort: k.querySelector(".e-person__auftrag-wort")?.textContent || "",
|
||||
marke: k.querySelector(".e-person__auftrag-spaet")?.textContent || "",
|
||||
titel: k.querySelector(".e-person__auftrag-titel")?.textContent || "",
|
||||
frist: k.querySelector(".e-person__auftrag-frist")?.textContent || "",
|
||||
fristSpaet: k.querySelector(".e-person__auftrag-frist")?.dataset.spaet || "",
|
||||
leerText: k.querySelector(".e-person__auftrag-leer")?.textContent || "",
|
||||
}];
|
||||
})));
|
||||
|
||||
const dn = a.Diene || {}, vv = a.VanVan || {};
|
||||
ok(dn.sichtbar === true && vv.sichtbar === true,
|
||||
"der Streifen steht auf beiden Kacheln");
|
||||
/* DREI, NICHT VIER: Die erledigte zaehlt nicht mit. Ohne diese
|
||||
Gegenprobe wuerde eine Zahl, die alles zaehlt, nie auffallen. */
|
||||
ok(dn.zahl === "3", `Diene hat drei offene Aufgaben (steht da: "${dn.zahl}")`);
|
||||
ok(dn.wort === "offene Aufgaben", `und das Wort steht im Plural ("${dn.wort}")`);
|
||||
ok(dn.marke === "1 überfällig", `eine davon ist ueberfaellig ("${dn.marke}")`);
|
||||
ok(dn.spaet === "ja", "und der Streifen zeigt es an seiner Kante");
|
||||
/* DIE NAECHSTE MIT NAMEN -- und zwar die ueberfaellige, nicht
|
||||
irgendeine. Sortiert wird nach Frist; ohne Frist steht hinten. */
|
||||
ok(dn.titel === "Clips vom Samstag schneiden",
|
||||
`die ueberfaellige steht mit Namen da ("${dn.titel}")`);
|
||||
ok(dn.frist === "seit gestern überfällig",
|
||||
`mit einem Satz statt einem Datum ("${dn.frist}")`);
|
||||
ok(dn.fristSpaet === "ja", "und sie ist als ueberfaellig ausgezeichnet");
|
||||
|
||||
ok(vv.leer === "ja" && vv.leerText === "keine offene Aufgabe",
|
||||
`bei VanVan steht "nichts offen" statt einer Null ("${vv.leerText}")`);
|
||||
ok(vv.zahl === "" && vv.marke === "",
|
||||
"und keine erfundene Zahl daneben");
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
|
||||
+89
-23
@@ -485,14 +485,25 @@ melde("=== 8. Im Browser ===");
|
||||
return {
|
||||
name,
|
||||
y: Math.round(window.scrollY),
|
||||
/* DIE KACHEL ZAEHLT SEIT DEM 17.09.2026 IN KAESTEN.
|
||||
/* WO DIE ZAHL AUF DER KACHEL STEHT -- zum dritten Mal woanders.
|
||||
|
||||
Vorher stand dort ein Satz ("2 von 68 angesehen"), und dieser
|
||||
Selektor las ihn. Seit die Kachel dieselben Bilanz-Kaesten
|
||||
traegt wie das Brett daneben, ist `.e-person__zahl` nur noch
|
||||
die Zeile fuer veraltete Einschaetzungen -- die Pruefung las
|
||||
seitdem einen leeren String, ohne dass etwas kaputt war. */
|
||||
kachel: kachel?.querySelector(".bilanz-zahl__n")?.textContent.trim() || "",
|
||||
Bis 17.09.2026 ein Satz ("2 von 68 angesehen"), danach ein
|
||||
Bilanz-Kasten, seit dem 24.09.2026 die Ziffer vor "von 68
|
||||
angesehen" plus der Ring darueber.
|
||||
|
||||
Das ist kein Zufall und kein Pech: Wer eine Kachel umbaut,
|
||||
verschiebt die Zahl. Die Pruefung muss es merken -- und sie
|
||||
merkt es nur, weil sie die Zahl VERGLEICHT statt sie bloss zu
|
||||
lesen. Beim Umbau am 17.09. las genau dieser Selektor
|
||||
monatelang einen leeren String, ohne dass etwas kaputt war.
|
||||
|
||||
DER RING WIRD MITGELESEN. Er ist seit dem 24.09. die groessere
|
||||
der beiden Darstellungen; bewegte sich nur die Ziffer, stuende
|
||||
ein Ring bei 3 % ueber einer 46 -- und das faellt niemandem
|
||||
auf, weil man das eine sieht und das andere liest. */
|
||||
kachel: kachel?.querySelector(".e-person__stand-zahl")?.textContent.trim() || "",
|
||||
bogen: kachel?.querySelector(".e-ring__bogen")
|
||||
?.getAttribute("stroke-dasharray") || "",
|
||||
kopfZahl: (document.querySelector("#karte .e-karte__stand")?.textContent
|
||||
.match(/(\d+) von/) || [])[1] || "",
|
||||
kopf: document.querySelector("#karte .e-karte__stand")?.textContent.trim() || "",
|
||||
@@ -522,14 +533,25 @@ melde("=== 8. Im Browser ===");
|
||||
.find((x) => x.querySelector(".e-person__name")?.textContent.trim() === name);
|
||||
return {
|
||||
y: Math.round(window.scrollY),
|
||||
/* DIE KACHEL ZAEHLT SEIT DEM 17.09.2026 IN KAESTEN.
|
||||
/* WO DIE ZAHL AUF DER KACHEL STEHT -- zum dritten Mal woanders.
|
||||
|
||||
Vorher stand dort ein Satz ("2 von 68 angesehen"), und dieser
|
||||
Selektor las ihn. Seit die Kachel dieselben Bilanz-Kaesten
|
||||
traegt wie das Brett daneben, ist `.e-person__zahl` nur noch
|
||||
die Zeile fuer veraltete Einschaetzungen -- die Pruefung las
|
||||
seitdem einen leeren String, ohne dass etwas kaputt war. */
|
||||
kachel: kachel?.querySelector(".bilanz-zahl__n")?.textContent.trim() || "",
|
||||
Bis 17.09.2026 ein Satz ("2 von 68 angesehen"), danach ein
|
||||
Bilanz-Kasten, seit dem 24.09.2026 die Ziffer vor "von 68
|
||||
angesehen" plus der Ring darueber.
|
||||
|
||||
Das ist kein Zufall und kein Pech: Wer eine Kachel umbaut,
|
||||
verschiebt die Zahl. Die Pruefung muss es merken -- und sie
|
||||
merkt es nur, weil sie die Zahl VERGLEICHT statt sie bloss zu
|
||||
lesen. Beim Umbau am 17.09. las genau dieser Selektor
|
||||
monatelang einen leeren String, ohne dass etwas kaputt war.
|
||||
|
||||
DER RING WIRD MITGELESEN. Er ist seit dem 24.09. die groessere
|
||||
der beiden Darstellungen; bewegte sich nur die Ziffer, stuende
|
||||
ein Ring bei 3 % ueber einer 46 -- und das faellt niemandem
|
||||
auf, weil man das eine sieht und das andere liest. */
|
||||
kachel: kachel?.querySelector(".e-person__stand-zahl")?.textContent.trim() || "",
|
||||
bogen: kachel?.querySelector(".e-ring__bogen")
|
||||
?.getAttribute("stroke-dasharray") || "",
|
||||
kopfZahl: (document.querySelector("#karte .e-karte__stand")?.textContent
|
||||
.match(/(\d+) von/) || [])[1] || "",
|
||||
kopf: document.querySelector("#karte .e-karte__stand")?.textContent.trim() || "",
|
||||
@@ -545,18 +567,62 @@ melde("=== 8. Im Browser ===");
|
||||
`die Kachel zählt sofort mit (${vorDemKlick.kachel} -> ${nachDemKlick.kachel})`);
|
||||
ok(nachDemKlick.kachel === nachDemKlick.kopfZahl,
|
||||
`und nennt dieselbe Zahl wie die Karte (${nachDemKlick.kachel} / ${nachDemKlick.kopfZahl})`);
|
||||
/* UND DER RING WANDERT MIT. Seine Laenge ist der Anteil in Prozent
|
||||
(Umfang 100) -- er muss also groesser werden, wenn die Ziffer es
|
||||
wird. Ein Ring, der stehenbleibt, waere dieselbe zweite Wahrheit
|
||||
wie die Zahl von vorhin, nur als Bild statt als Text. */
|
||||
const bogenAus = (x) => Number(String(x).split(" ")[0]);
|
||||
ok(bogenAus(nachDemKlick.bogen) > bogenAus(vorDemKlick.bogen),
|
||||
`und der Ring wird voller (${vorDemKlick.bogen} -> ${nachDemKlick.bogen})`);
|
||||
ok(zahlAus(nachDemKlick.kachel) > 0,
|
||||
"es ist eine echte Zahl, keine Null — sonst wäre die Gleichheit wertlos");
|
||||
|
||||
/* GEGENPROBE ZUM SPRUNG: Auf eine PERSON zu drücken DARF springen --
|
||||
dort will man zur Karte. Ohne diese Zeile hieße „springt nicht“
|
||||
vielleicht nur, dass gar nichts mehr scrollt. */
|
||||
await seite.evaluate(() => window.scrollTo({ top: 900, behavior: "instant" }));
|
||||
await seite.waitForTimeout(200);
|
||||
await seite.evaluate(() => document.querySelector(".e-person")?.click());
|
||||
await seite.waitForTimeout(1400);
|
||||
const beiPerson = await seite.evaluate(() => Math.round(window.scrollY));
|
||||
ok(beiPerson !== 900, `beim Öffnen einer Person springt sie sehr wohl (${beiPerson})`);
|
||||
/* =====================================================================
|
||||
GEGENPROBE ZUM SPRUNG -- neu gefasst am 24.09.2026
|
||||
=====================================================================
|
||||
|
||||
Sie muss es geben: Ohne sie hiesse "die Seite bleibt stehen"
|
||||
vielleicht nur, dass ueberhaupt nichts mehr scrollt, und die
|
||||
Pruefung darueber waere wertlos.
|
||||
|
||||
HIER STAND BIS HEUTE DER FALSCHE FALL. Sie klickte eine Kachel an
|
||||
und verlangte einen Sprung zur Karte -- richtig bis zum
|
||||
23.09.2026. An dem Tag wurde die Seite nach dem Arbeitsablauf
|
||||
umgeraeumt: Zwischen Kachel und Karte liegen seither das
|
||||
Verteil-Formular, der Katalog und die Aufgabenliste. Ein Sprung
|
||||
zur Karte wuerde also ueber genau die drei Dinge hinwegspringen,
|
||||
die man nach der Wahl zuerst braucht. Der Sprung wurde deshalb
|
||||
ABSICHTLICH entfernt (siehe entwicklung.js, "KEIN SPRUNG MEHR").
|
||||
|
||||
Die Gegenprobe blieb stehen und war ab da rot -- nicht, weil
|
||||
etwas kaputt war, sondern weil sie eine Zusage einforderte, die
|
||||
zurueckgenommen worden ist. Gefunden beim Kachelumbau am
|
||||
24.09.2026, also einen Tag spaeter; waere sie gruen gewesen,
|
||||
haette es niemand bemerkt.
|
||||
|
||||
DER SPRUNG, DEN ES NOCH GIBT, ist der von aussen: Wer der
|
||||
Talentseite folgt, kommt mit "?zeigen=person-<nr>" und will genau
|
||||
diese eine Karte sehen. Daran laesst sich beweisen, dass die Seite
|
||||
sehr wohl springen KANN -- und zwar an der einzigen Stelle, an der
|
||||
sie es noch soll. */
|
||||
const werDenn = await seite.evaluate(() =>
|
||||
document.querySelector("#personen .e-person")?.dataset.person || "");
|
||||
ok(werDenn !== "", `eine Kachel zum Anspringen gefunden (Nr. ${werDenn})`);
|
||||
await seite.goto(`http://127.0.0.1:${PORT}/workspace/entwicklung.html?zeigen=person-${werDenn}`,
|
||||
{ waitUntil: "networkidle" });
|
||||
await seite.waitForSelector("#personen .e-person", { timeout: 15_000 });
|
||||
await seite.waitForTimeout(1600);
|
||||
const vonAussen = await seite.evaluate(() => ({
|
||||
y: Math.round(window.scrollY),
|
||||
karteOffen: !document.getElementById("karte")?.hidden,
|
||||
wer: document.querySelector('#personen .e-person[aria-pressed="true"]')
|
||||
?.dataset.person || "",
|
||||
}));
|
||||
ok(vonAussen.y > 0,
|
||||
`von aussen verlinkt springt sie sehr wohl (scrollY ${vonAussen.y})`);
|
||||
ok(vonAussen.karteOffen && vonAussen.wer === werDenn,
|
||||
`und zwar zu genau dieser Karte (offen: ${vonAussen.karteOffen}, `
|
||||
+ `gewaehlt: ${vonAussen.wer})`);
|
||||
|
||||
ok(konsole.length === 0,
|
||||
konsole.length ? "Fehler im Browser: " + konsole[0].slice(0, 90) : "keine Fehler im Browser");
|
||||
|
||||
Reference in New Issue
Block a user