From 5b244ab7ec4feaa8b3428e0299df1bea279ce868 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Thu, 24 Sep 2026 13:30:17 +0200 Subject: [PATCH] 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 --- .gitignore | 4 + server/mess-entwicklung-kacheln.mjs | 223 +++++++++++++ server/pruef-entwicklung-kacheln.mjs | 241 +++++++++++--- server/pruef-schritt.mjs | 112 +++++-- workspace/anruf-probe.html | 10 +- workspace/app.webmanifest | 8 +- workspace/assets/css/entwicklung.css | 448 ++++++++++++++++++-------- workspace/assets/js/entwicklung.js | 463 +++++++++++++++++++++------ workspace/aufgaben.html | 38 +-- workspace/automation.html | 32 +- workspace/befinden.html | 34 +- workspace/bereich.html | 44 +-- workspace/bewerben.html | 38 +-- workspace/bewerbungen.html | 36 +-- workspace/calls.html | 34 +- workspace/chat.html | 38 +-- workspace/content.html | 34 +- workspace/crew-index.html | 12 +- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 34 +- workspace/entwicklung.html | 36 +-- workspace/hilfe.html | 30 +- workspace/index.html | 10 +- workspace/kalender.html | 34 +- workspace/leistung.html | 32 +- workspace/material.html | 32 +- workspace/personen.html | 32 +- workspace/profil.html | 36 +-- workspace/rechte.html | 34 +- workspace/report.html | 36 +-- workspace/scouting.html | 32 +- workspace/start.html | 32 +- workspace/startcheck.html | 34 +- workspace/steckbrief.html | 36 +-- workspace/support.html | 28 +- workspace/talente.html | 36 +-- workspace/team.html | 32 +- workspace/teamlage.html | 34 +- workspace/teilen.html | 30 +- workspace/treff-moderation.html | 32 +- workspace/treff-regeln.html | 32 +- workspace/uebersicht.html | 32 +- workspace/unsere-seiten.html | 30 +- workspace/werdegang.html | 32 +- workspace/willkommen.html | 30 +- workspace/wissen.html | 34 +- 46 files changed, 1795 insertions(+), 924 deletions(-) create mode 100644 server/mess-entwicklung-kacheln.mjs diff --git a/.gitignore b/.gitignore index 3b6450c1..5d3cb8e3 100644 --- a/.gitignore +++ b/.gitignore @@ -80,6 +80,10 @@ pruef-*.png server/pruef-*.png abnahme-*.png server/abnahme-*.png +# Dasselbe fuer die Messlaeufe (server/mess-*.mjs): Bilder eines +# Laufs, kein Quelltext. +mess-*.png +server/mess-*.png # Messlaeufe von tools/mess-rueckgabewerte.sh -- Ergebnis eines Laufs, # kein Quelltext. Gehoert nicht in die Geschichte. diff --git a/server/mess-entwicklung-kacheln.mjs b/server/mess-entwicklung-kacheln.mjs new file mode 100644 index 00000000..9c478b9b --- /dev/null +++ b/server/mess-entwicklung-kacheln.mjs @@ -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); +} diff --git a/server/pruef-entwicklung-kacheln.mjs b/server/pruef-entwicklung-kacheln.mjs index 50d5e3cc..b0345ccb 100644 --- a/server/pruef-entwicklung-kacheln.mjs +++ b/server/pruef-entwicklung-kacheln.mjs @@ -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