From b57b9cebb5de370a3b75d45339d63ba8b2661318 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Thu, 17 Sep 2026 02:21:12 +0200 Subject: [PATCH] Der Ring oben zeigt Arbeit statt der Uhrzeit Auf der Startseite von Creator, rechter Hand und Modis ist der Ring das groesste Element, ganz oben. Er rechnete: prozent = (aktuelle Stunde - 6) / 18 ...und nannte das "Tag geschafft". Um 9 Uhr also 17 %, um 23 Uhr 94 % -- unabhaengig davon, ob jemand etwas getan hatte. Aufgefallen auf einem Bildschirmfoto: Bei einer Modi stand "0 % TAG GESCHAFFT", waehrend sie ueberhaupt nichts offen hatte. Beide Angaben waren fuer sich richtig und zusammen Unsinn -- und es war das Erste, was sie sah. JETZT ZAEHLT ER ARBEIT. Nenner ist, was heute auf dem Tisch liegt: heute faellig ODER laenger offen. Die naheliegende Rechnung ueber "nur heute faellig" waere derselbe Fehler noch einmal gewesen -- bei drei ueberfaelligen Sachen haette der Ring "nichts faellig" gemeldet. Zaehler ist, was HEUTE fertig wurde (erledigt_am), nicht was irgendwann abgehakt wurde. Liegt nichts an: voller Ring, und der Titel sagt es in Worten statt nur eine 100 zu zeigen. Gemessen danach, dieselbe Seite: "33 % Heute geschafft", darunter "2 Sachen sind ueberfaellig -- das zuerst". Die Seite erzaehlt jetzt eine Geschichte statt zweier. NEU: pruef-zentrale-ring (10 Pruefungen) Drei Lagen, drei Menschen: etwas anliegend und teils fertig / nichts anliegend / drei ueberfaellige und nichts fertig. Dazu zwei Abgrenzungen (morgen faellig und vorgestern erledigt duerfen nicht mitzaehlen) und die Gegenprobe: zwischen zwei Abfragen wird eine Aufgabe fertig gemacht, der Ring MUSS springen -- er tut es, 0 % -> 33 %. Der letzte Abschnitt rechnet die alte Uhrzeit-Formel mit und meldet, falls der Ring je wieder auf ihren Wert faellt. NEU: mess-startseite -- ein Messwerkzeug, kein Pruefwerkzeug Es sagt nicht richtig/falsch, sondern wie viel da ist: Seitenlaenge in Handy-Bildschirmen, Bedienelemente, Woerter, Ring, je Rolle, mit echten Aufgaben statt im leeren Zustand. Gemessener Stand: admin 7,0 Schirme / 58 Bedienelemente hand 5,7 / 43 modi 5,0 / 37 manager 4,9 / 38 scout 4,5 / 34 creator 4,3 / 32 pruef-sicht, pruef-verborgen, pruef-modi-verborgen und pruef-haus-trennung laufen unveraendert durch -- die neue Abfrage folgt der fremden Sicht ueber dieselbe Variable wie der Rest. Co-Authored-By: Claude Opus 5 --- server/mess-startseite.mjs | 188 ++++++++++++++++++++++++++++ server/pruef-zentrale-ring.mjs | 220 +++++++++++++++++++++++++++++++++ server/workspace-zentrale.js | 36 +++++- 3 files changed, 441 insertions(+), 3 deletions(-) create mode 100644 server/mess-startseite.mjs create mode 100644 server/pruef-zentrale-ring.mjs diff --git a/server/mess-startseite.mjs b/server/mess-startseite.mjs new file mode 100644 index 00000000..1c3843e3 --- /dev/null +++ b/server/mess-startseite.mjs @@ -0,0 +1,188 @@ +/* ===================================================================== + WIE VIEL STEHT AUF DER STARTSEITE? (17.09.2026) + + Kein Pruefwerkzeug -- ein MESSWERKZEUG. Es sagt nicht "richtig" oder + "falsch", es sagt, wie viel da ist. Anlass ist Filipes Wunsch, dass + alles "so einfach wie moeglich" wird, und dafuer braucht es zuerst + eine Zahl statt eines Eindrucks. + + GEMESSEN WIRD IN HANDY-BILDSCHIRMEN, nicht in Pixeln. 2955 px sagt + niemandem etwas; "acht Bildschirme scrollen" sagt es sofort. + + UND DIE WICHTIGSTE ZAHL IST NICHT DIE LAENGE, sondern: + Wie weit muss jemand scrollen, bis er das Erste sieht, das er + ANFASSEN kann? Alles darueber ist Lage, nicht Handlung. Ein Werkzeug, + bei dem die Arbeit erst im zweiten Bildschirm anfaengt, ist kein + Werkzeug, sondern ein Bericht. + + MIT ECHTEN AUFGABEN, nicht im leeren Zustand -- sonst misst man den + ersten Tag und nicht den Alltag. Der leere Zustand wird zusaetzlich + gemessen, weil er der erste Eindruck ist. + + Aufrufen mit: node server/mess-startseite.mjs + ===================================================================== */ + +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { portMussFreiSein } from "./helfer-port.mjs"; + +const PORT = await portMussFreiSein(4471, "mess-startseite"); +const ordner = mkdtempSync(join(tmpdir(), "ws-mess-")); +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(240_000, "mess-startseite"); +await new Promise((r) => setTimeout(r, 700)); +const BASIS = `http://127.0.0.1:${PORT}`; + +/* ---------- Personen anlegen ---------------------------------------- */ +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); +const 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 leute = [ + { rolle: "admin", kachel: "admin", name: "Filipe", code: "CODE-DOGI-0001" }, + { rolle: "manager", kachel: "manager", name: "Schulle", code: "CODE-MAN-0001" }, + { rolle: "scout", kachel: "scout", name: "Nane", code: "CODE-SCT-0001" }, + { rolle: "creator", kachel: "creator", name: "Tili", code: "CODE-CRE-0001" }, + { rolle: "hand", kachel: "creator", name: "Rieke", code: "CODE-HAND-0001" }, + { rolle: "modi", kachel: "creator", name: "Marina", code: "CODE-MODI-0001" }, +]; +for (const p of leute) p.id = anlegen(p.name, p.rolle, p.code); + +/* ---------- Aufgaben, damit der Alltag gemessen wird ---------------- */ +const tag = (v) => { + const x = new Date(Date.now() + v * 86400_000); + const z = (n) => String(n).padStart(2, "0"); + return `${x.getFullYear()}-${z(x.getMonth() + 1)}-${z(x.getDate())}`; +}; +const spalten = d.prepare("PRAGMA table_info(aufgaben)").all().map((s) => s.name); +function aufgabe(personId, titel, status, frist) { + const feld = { titel, status, verantwortlich_id: personId, erstellt: jetzt }; + if (status === "erledigt" && spalten.includes("erledigt_am")) feld.erledigt_am = jetzt; + if (spalten.includes("frist")) feld.frist = frist; + if (spalten.includes("erstellt_von")) feld.erstellt_von = personId; + const namen = Object.keys(feld).filter((n) => spalten.includes(n)); + d.prepare(`INSERT INTO aufgaben (${namen.join(",")}) VALUES (${namen.map(() => "?").join(",")})`) + .run(...namen.map((n) => feld[n])); +} +const modi = leute.find((p) => p.rolle === "modi"); +for (const p of leute) { + aufgabe(p.id, "Regeln des Treffs lesen", "offen", tag(-2)); + aufgabe(p.id, "Chat vor der Sendung pruefen", "offen", tag(0)); + aufgabe(p.id, "Drei Zeilen nach der Schicht", "offen", tag(1)); + aufgabe(p.id, "Clip zuschneiden", "arbeit", tag(2)); + aufgabe(p.id, "Gefilterte Kommentare durchsehen", "erledigt", tag(-1)); +} +d.close(); + +/* ---------- Anmelden ------------------------------------------------- */ +async function anmelden(kachel, code) { + const a = await fetch(BASIS + "/workspace/api/anmelden", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rolle: kachel, code }), + }); + return (a.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; "); +} + +/* ---------- Messen ---------------------------------------------------- */ +const { chromium } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); +const browser = await chromium.launch(); +const BREITE = 390, HOEHE = 844; +const bildOrdner = fileURLToPath(new URL("./", import.meta.url)); + +const ergebnisse = []; +try { + for (const p of leute) { + const keks = await anmelden(p.kachel, p.code); + if (!keks) { console.log(` !! ${p.rolle}: Anmeldung fehlgeschlagen`); continue; } + 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(); + await seite.goto(`${BASIS}/workspace/start.html`, { waitUntil: "networkidle" }); + await seite.waitForTimeout(900); + + const m = await seite.evaluate(() => { + const hoehe = document.documentElement.scrollHeight; + /* WAS KANN MAN ANFASSEN? Knoepfe, Verweise, Felder -- aber nur, + was sichtbar ist und im Inhalt steht, nicht die Kopfleiste: + Die ist auf jeder Seite gleich und zaehlt nicht zur Seite. */ + const inhalt = document.querySelector("main") || document.body; + const anfassbar = [...inhalt.querySelectorAll("a[href], button, input, select, textarea")] + .filter((z) => z.offsetParent !== null + && !z.closest(".kopfleiste") && !z.closest("header")); + const erstes = anfassbar[0]; + const yErstes = erstes + ? Math.round(erstes.getBoundingClientRect().top + window.scrollY) : -1; + /* Zahlen ohne Handlung: die grossen Zaehler-Kacheln. */ + const zaehler = inhalt.querySelectorAll( + "[class*='zaehler'], [class*='kennzahl'], [class*='statistik'], [class*='stapel__wert']").length; + const woerter = (inhalt.innerText || "").trim().split(/\s+/).filter(Boolean).length; + const ring = (document.getElementById("zring-prozent") || {}).textContent || ""; + const ringUnter = (document.getElementById("zring-unter") || {}).textContent || ""; + return { hoehe, anfassbar: anfassbar.length, yErstes, zaehler, woerter, ring, ringUnter, + erstesWort: erstes ? (erstes.innerText || erstes.value || "").trim().slice(0, 30) : "" }; + }); + + await seite.screenshot({ path: join(bildOrdner, `mess-start-${p.rolle}.png`), fullPage: true }); + await kontext.close(); + ergebnisse.push({ ...p, ...m }); + } +} finally { + await browser.close(); +} + +/* ---------- Ausgabe --------------------------------------------------- */ +console.log(""); +console.log(`Handy ${BREITE} x ${HOEHE} px, je 5 Aufgaben (1 ueberfaellig, 1 heute)`); +console.log(""); +console.log("Rolle".padEnd(10) + "Hoehe".padStart(8) + "Schirme".padStart(9) + + "Bedien.".padStart(9) + "1. Ding".padStart(9) + "Woerter".padStart(9) + " Ring oben"); +for (const e of ergebnisse) { + console.log( + e.rolle.padEnd(10) + + `${e.hoehe}px`.padStart(8) + + (e.hoehe / HOEHE).toFixed(1).padStart(9) + + String(e.anfassbar).padStart(9) + + (e.yErstes < 0 ? "-" : `${(e.yErstes / HOEHE).toFixed(1)}`).padStart(9) + + String(e.woerter).padStart(9) + + " " + (e.ring + " " + e.ringUnter).trim()); +} +console.log(""); +console.log("Schirme = Seitenlaenge in Handy-Bildschirmen"); +console.log("1. Ding = nach wie vielen Bildschirmen das erste Bedienelement kommt"); +console.log("Bilder = server/mess-start-.png"); + +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ } +process.exit(0); diff --git a/server/pruef-zentrale-ring.mjs b/server/pruef-zentrale-ring.mjs new file mode 100644 index 00000000..55070543 --- /dev/null +++ b/server/pruef-zentrale-ring.mjs @@ -0,0 +1,220 @@ +/* ===================================================================== + DER RING OBEN ZEIGT ARBEIT, NICHT DIE UHRZEIT (17.09.2026) + + Auf der Startseite von Creator, rechter Hand und Modis steht das + groesste Element ganz oben: ein Ring mit einer Prozentzahl und dem + Wort darunter. Bis heute rechnete er: + + prozent = (aktuelle Stunde - 6) / 18 + + ...und nannte das "Tag geschafft". Um 9 Uhr also 17 %, um 23 Uhr + 94 % -- voellig unabhaengig davon, ob jemand etwas getan hatte. + + AUFGEFALLEN IST ES AUF EINEM BILDSCHIRMFOTO: Bei einem Modi stand + "0 % TAG GESCHAFFT", waehrend sie ueberhaupt nichts offen hatte. + Jede der beiden Angaben war fuer sich richtig. Zusammen waren sie + Unsinn -- und es war das Erste, was sie morgens sah. + + --------------------------------------------------------------------- + WAS DIESE PRUEFUNG SICHERT + + Drei Faelle, und der dritte ist der, an dem so etwas wieder + kaputtgeht: + + 1. Es liegt etwas an, ein Teil ist fertig -> ein Anteil + 2. Es liegt NICHTS an -> voll, und es steht da + 3. Es liegt etwas an, NICHTS ist fertig -> null + + Fall 3 ist der wichtige. Die naheliegende Rechnung waere "was ist + heute faellig" -- dann meldete der Ring bei drei UEBERFAELLIGEN + Sachen "nichts faellig, alles gut". Das waere derselbe Fehler noch + einmal, nur mit anderen Zahlen. Gezaehlt wird deshalb, was heute auf + dem Tisch liegt: faellig ODER laenger offen. + + UND DIE GEGENPROBE: Zwischen zwei Abfragen wird eine Aufgabe fertig + gemacht. Der Ring MUSS sich bewegen. Ohne diesen Teil koennte er + dauerhaft dieselbe Zahl liefern und alle Faelle oben bestehen. + + Aufrufen mit: node server/pruef-zentrale-ring.mjs + ===================================================================== */ + +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { portMussFreiSein } from "./helfer-port.mjs"; +import { heuteLokal, tagLokal } from "./helfer-tag.mjs"; + +const PORT = await portMussFreiSein(4473, "pruef-zentrale-ring"); +const ordner = mkdtempSync(join(tmpdir(), "ws-ring-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = String(PORT); +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; + +const express = (await import("express")).default; +const ec = express.response.cookie; +express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; +import { notbremse } from "./helfer-notbremse.mjs"; +await import("./index.js"); +notbremse(180_000, "pruef-zentrale-ring"); +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); }); + +let fehler = 0, geprueft = 0; +const melde = (t) => console.log(t); +const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); +const 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; +} + +/* Drei Menschen mit drei Lagen -- getrennt, damit sich die Faelle nicht + gegenseitig beeinflussen. Ein einziger Mensch, bei dem zwischendurch + Zeilen geloescht werden, misst am Ende den Loeschvorgang mit. */ +const idTut = anlegen("Marina", "modi", "CODE-TUT-00001"); +const idLeer = anlegen("Nane", "modi", "CODE-LEER-0001"); +const idSaeu = anlegen("Rieke", "modi", "CODE-SAEU-0001"); + +const setzen = (personId, status, frist, erledigtAm = null) => + d.prepare(`INSERT INTO aufgaben (titel, status, verantwortlich_id, frist, erledigt_am, erstellt) + VALUES (?,?,?,?,?,?)`) + .run("Sache", status, personId, frist, erledigtAm, jetzt); + +/* 1. Etwas liegt an, ein Teil ist fertig: 2 offen (eins ueberfaellig, + eins heute) + 1 heute erledigt -> 1 von 3 -> 33 % */ +setzen(idTut, "offen", tagLokal(-2)); +setzen(idTut, "offen", heuteLokal()); +setzen(idTut, "erledigt", heuteLokal(), jetzt); +/* Und Dinge, die NICHT mitzaehlen duerfen: morgen faellig, und etwas, + das vorgestern fertig wurde. Ohne sie waere nicht geprueft, dass der + Ring den TAG meint und nicht die Gesamtlage. */ +setzen(idTut, "offen", tagLokal(3)); +setzen(idTut, "erledigt", tagLokal(-9), new Date(Date.now() - 2 * 86400_000).toISOString()); + +/* 2. Es liegt nichts an -- nur etwas in der Zukunft. */ +setzen(idLeer, "offen", tagLokal(5)); + +/* 3. Drei UEBERFAELLIGE, nichts fertig. Der Fall, an dem eine Rechnung + ueber "heute faellig" faelschlich "alles gut" melden wuerde. */ +setzen(idSaeu, "offen", tagLokal(-4)); +setzen(idSaeu, "offen", tagLokal(-3)); +setzen(idSaeu, "arbeit", tagLokal(-1)); +d.close(); + +async function ring(code) { + const an = await fetch(BASIS + "/workspace/api/anmelden", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rolle: "creator", code }), + }); + const keks = (an.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; "); + const a = await fetch(BASIS + "/workspace/api/zentrale", { headers: { cookie: keks } }); + return a.json(); +} + +/* ======================================================================= + 1. ES LIEGT ETWAS AN + ======================================================================= */ +melde(""); +melde("=== 1. Etwas liegt an, ein Teil ist fertig ==="); +{ + const r = await ring("CODE-TUT-00001"); + ok(r.titel === "Heute geschafft", `der Ring heisst "${r.titel}"`); + ok(r.prozent === 33, `und steht auf ${r.prozent} % (1 von 3)`); + /* DIE ENTSCHEIDENDE ABGRENZUNG: Ohne diese Zeile koennte der Ring + alle fuenf Zeilen zaehlen und zufaellig auch auf eine plausible + Zahl kommen. */ + ok(r.prozent !== 20 && r.prozent !== 40, + "morgen und vorgestern zaehlen nicht mit"); +} + +/* ======================================================================= + 2. ES LIEGT NICHTS AN + ======================================================================= */ +melde(""); +melde("=== 2. Es liegt nichts an ==="); +{ + const r = await ring("CODE-LEER-0001"); + ok(r.prozent === 100, `voller Ring (${r.prozent} %)`); + /* UND ES STEHT IN WORTEN DA. Ein voller Ring ohne Erklaerung waere + wieder eine Zahl, bei der man raten muss -- genau der Fehler, der + hier repariert wurde. */ + ok(r.titel === "Heute nichts offen", `und er sagt es: "${r.titel}"`); +} + +/* ======================================================================= + 3. ÜBERFÄLLIG IST NICHT "NICHTS" + ======================================================================= */ +melde(""); +melde("=== 3. Drei ueberfaellige Sachen ==="); +{ + const r = await ring("CODE-SAEU-0001"); + ok(r.prozent === 0, `leerer Ring (${r.prozent} %)`); + ok(r.titel === "Heute geschafft", + `und NICHT "nichts offen" -- ueberfaellig liegt an ("${r.titel}")`); +} + +/* ======================================================================= + 4. GEGENPROBE: BEWEGT ER SICH? + ======================================================================= */ +melde(""); +melde("=== 4. Die Gegenprobe ==="); +{ + const vorher = await ring("CODE-SAEU-0001"); + const e = new DatabaseSync(process.env.WORKSPACE_DB); + /* SQLite kennt ORDER BY im UPDATE nicht (ohne eigenes Uebersetzen). + Deshalb die Zeile ueber eine Unterabfrage auswaehlen. */ + e.prepare(`UPDATE aufgaben SET status = 'erledigt', erledigt_am = ? + WHERE id = (SELECT id FROM aufgaben + WHERE verantwortlich_id = ? AND status = 'offen' + ORDER BY id LIMIT 1)`).run(jetzt, idSaeu); + e.close(); + const nachher = await ring("CODE-SAEU-0001"); + /* OHNE DIESE ZEILE beweisen die drei Abschnitte darueber nichts: Ein + Ring, der immer dieselbe Zahl liefert, koennte sie alle bestehen, + solange die Zahl zufaellig passt. */ + ok(nachher.prozent > vorher.prozent, + `eine Aufgabe fertig gemacht: ${vorher.prozent} % -> ${nachher.prozent} %`); + ok(nachher.prozent === 33, `und zwar auf 1 von 3 (${nachher.prozent} %)`); +} + +/* ======================================================================= + 5. DIE UHRZEIT SPIELT KEINE ROLLE MEHR + ======================================================================= */ +melde(""); +melde("=== 5. Was der Ring NICHT mehr ist ==="); +{ + /* Die alte Rechnung, wortwoertlich, mit der jetzigen Stunde. Sie + steht hier, damit auffaellt, wenn jemand sie zurueckholt: Der Ring + darf nicht zufaellig wieder auf ihren Wert fallen. */ + const alt = Math.round((Math.min(Math.max(new Date().getHours() - 6, 0), 18) / 18) * 100); + const r = await ring("CODE-LEER-0001"); + ok(r.prozent === 100 && (alt !== 100 || new Date().getHours() >= 24), + `die alte Uhrzeit-Rechnung ergaebe jetzt ${alt} %, der Ring zeigt ${r.prozent} %`); +} + +/* ------------------------------------------------------------------- */ +melde(""); +melde(`${geprueft} Pruefungen, ${fehler} Fehler`); +melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG"); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ } +process.exit(fehler ? 1 : 0); diff --git a/server/workspace-zentrale.js b/server/workspace-zentrale.js index 058092c1..388f8f8d 100644 --- a/server/workspace-zentrale.js +++ b/server/workspace-zentrale.js @@ -263,11 +263,41 @@ zentraleRouter.get("/workspace/api/zentrale", (req, res) => { segmente.push({ name: `${p2(s)} Uhr`, rolle: "stunde", stufe: mitTermin.has(s) ? "dran" : (s < stundeJetzt ? "frei" : "offen") }); } - const durch = Math.min(Math.max(stundeJetzt - VON, 0), BIS - VON); + /* ---------- DIE ZAHL IN DER MITTE (17.09.2026) ---------- + + HIER STAND DIE UHRZEIT, und sie nannte sich "Tag geschafft": + `(aktuelle Stunde - 6) / 18`. Um 9 Uhr also 17 %, um 23 Uhr + 94 % -- unabhaengig davon, ob jemand auch nur eine Sache getan + hatte. Das groesste Element der Startseite, ganz oben, und es + beantwortete die Frage nicht, die es stellte. + + Gesehen auf dem Bildschirmfoto eines Modis: "0 % TAG + GESCHAFFT", waehrend sie ueberhaupt nichts offen hatte. Beides + war "richtig" und zusammen war es Unsinn. + + JETZT ZAEHLT ES ARBEIT. Nenner ist, was heute auf dem Tisch + liegt: was heute faellig ist ODER laenger offen steht -- denn + ein Ring, der bei drei ueberfaelligen Sachen "nichts faellig" + meldet, waere derselbe Fehler noch einmal. Zaehler ist, was + HEUTE fertig geworden ist (`erledigt_am`), nicht was irgendwann + einmal abgehakt wurde. + + Liegt nichts an, sind es 100 % und der Titel sagt es in Worten. + Ein voller Ring ohne Erklaerung waere wieder eine Zahl, bei der + man raten muss. */ + const meineAufgaben = d.prepare( + `SELECT status, frist, erledigt_am FROM aufgaben + WHERE verantwortlich_id = ?`).all(zeigt.id); + const nochOffen = (a) => ["offen", "arbeit", "review"].includes(a.status); + const offenDran = meineAufgaben.filter( + (a) => nochOffen(a) && a.frist && String(a.frist) <= heute).length; + const heuteFertig = meineAufgaben.filter( + (a) => String(a.erledigt_am || "").slice(0, 10) === heute).length; + const dran = offenDran + heuteFertig; return res.json({ art: "tag", - titel: "Tag geschafft", - prozent: Math.round((durch / (BIS - VON)) * 100), + titel: dran ? "Heute geschafft" : "Heute nichts offen", + prozent: dran ? Math.round((heuteFertig / dran) * 100) : 100, segmente, zahlen: [ { wert: meine.length, schild: "heute" },