Filipe, drei Bildschirmfotos: den Namen der Kachel aendern, "diese zwei kacheln noch mehr perfektionieren", "veraender den hintergrund von diesen kacheln komplett", "perfektionnier diese seite einfach komplett". DER NAME, DRITTE FASSUNG. Zuerst "Team-Lage" -- das klang nach Bericht UEBER Menschen. Dann "Eingang" -- das beschrieb nur die eine Haelfte (was hereinkommt) und liess die groessere weg: wer da ist, wer heute kann, wer was offen hat. "Dein Team" sagt beides und stellt niemanden ueber jemanden. DREI SACHEN WAREN NICHT GESCHMACK, SONDERN FEHLER -- und alle drei hat erst das Nachmessen am fertigen Bildschirm gezeigt: UEBER SIEBEN TAGEN STAND "Kann heute". Im Singular, ueber einer ganzen Woche. Wer nur die Ueberschrift liest -- und das tut man bei einer Zeile in Versalien --, haelt den ganzen Streifen fuer den heutigen Tag und liest sechs Kaestchen falsch. Der Satz DARUNTER war immer richtig; zwei Aussagen ueber demselben Bild, und die auffaelligere war die falsche. DIE VIER ZAHLEN BRACHEN 3 + 1. Gemessen: Die Karte ist innen 536 px breit, die Regel verlangte je Spalte mindestens 130 px, vier Spalten braeuchten 544. Acht Pixel zu wenig. Die Lehre ist NICHT "130 auf 122 senken" -- das waere dieselbe Rechnung mit einer anderen Zahl. Vier Dinge sehen nur in 1x4, 2x2 oder 4x1 richtig aus; 3+1 ist die eine Anordnung, die immer falsch wirkt, und eine `auto-fit`-Regel kann jederzeit dort landen. Zwei feste Spalten koennen es nicht. DER WEG IN DEN CHAT war ein unterstrichener Satz ueber die volle Kartenbreite -- er sah aus wie eine Ueberschrift, nicht wie ein Knopf. DER GRUND DER KARTEN, und hier hat mich das erste Ergebnis widerlegt: Der Schein aus der oberen Ecke nahm `var(--r)`, die Farbe der STUFE. Das war logisch und unsichtbar -- bei "Probe" ist sie ein gedaempftes Grau, und ein Grauschein auf fast Schwarz ist kein Schein. Nach der Aenderung sah die Karte auf dem Bildschirmfoto genauso aus wie davor. Jetzt tragen die beiden Ecken die Hausfarben (Lila oben links, Babyblau unten rechts), dazu ein Lichtstreifen quer und ein Kantenlicht oben. Die Stufe bleibt, wo sie hingehoert: im Kantenlicht und am Schild. AUGENSCHONEND BLEIBT PFLICHT: Keine Lage geht ueber 26 %, der Lichtstreifen liegt bei 3 %, und die hellsten Stellen sitzen in den ECKEN -- nicht hinter dem Text. Ein Schein hinter einer Zahl macht sie schwerer lesbar, egal wie schoen er ist. Dazu: HEUTE ist im Wochenstreifen markiert (sieben gleich aussehende Kaestchen zwingen sonst dazu, den Wochentag im Kopf auszurechnen), ein freier Tag hat eine Andeutung statt gar nichts (sieben leere Rahmen sahen aus wie "noch nicht geladen"), und der Erklaerkasten im Kopf darf 68 statt 44 Zeichen breit sein -- auf einem breiten Bildschirm stand er als schmale Saeule mit sechs Zeilen zu je vier Woertern neben viel Bild. WARUM DIE FALSCHE UEBERSCHRIFT UEBERLEBEN KONNTE: pruef-team-ampel prueft seit dem ersten Tag, dass SIEBEN Tage kommen und der erste heute ist. Was sie nie angesehen hat, ist der Text darueber -- die Zahl stimmte ja. Sie prueft ihn ab jetzt, an derselben Stelle wie die Zahl, damit beide zusammen gelesen werden. Mit Gegenprobe: Die Suche muss das Wort "heute" auch finden koennen, sonst waere sie gruen, weil sie nie etwas liest. pruef-team-ampel 28 -> 32 · pruef-team-stufen 24 · pruef-css-klassen gruen · pruef-start-ansicht 143 · pruef-rollen 278 · pruef-modi-wortleck 5. OFFEN GEBLIEBEN und ihm gemeldet: Auf einem 1920er Bildschirm steht der Inhalt in einer Saeule von rund 1150 px, links und rechts bleibt Bild. Das ist die Breite ALLER zwanzig Seiten; sie hier allein zu aendern hiesse, eine Seite anders zu bauen als die anderen neunzehn. Co-Authored-By: Claude Opus 5 <[email protected]>
297 lines
13 KiB
JavaScript
297 lines
13 KiB
JavaScript
/* =====================================================================
|
|
DIE VERFUEGBARKEITS-AMPEL (Blueprint Kapitel 7.1)
|
|
|
|
"Persoenlicher Kalender (privat): Inhalt ausschliesslich fuer die
|
|
jeweilige Person sichtbar; Owner/rechte Hand sehen nur die
|
|
Verfuegbarkeits-Ampel, nicht den Klartext."
|
|
|
|
Zwei Fragen, und die ERSTE ist die wichtigere:
|
|
|
|
1. KOMMT WIRKLICH KEIN KLARTEXT DURCH? Die Termine bekommen hier
|
|
Titel, Beschreibungen und Orte, die es sonst nirgends gibt
|
|
("ZAHNARZT-GEHEIM-4711"). Danach wird die GANZE Antwort nach
|
|
ihnen durchsucht -- nicht einzelne Felder, sondern der ganze
|
|
Text. Wer ein Feld durchreicht, das er nicht bedacht hat,
|
|
faellt damit auf; eine Pruefung, die nur die erwarteten Felder
|
|
ansieht, faende genau das nicht.
|
|
2. STIMMT DIE AMPEL? Drei Stufen, drei Faelle -- und die Grenze
|
|
bei drei Stunden von beiden Seiten.
|
|
|
|
DAZU DER ABGLEICH MIT DER OFFIZIELLEN REGEL. Die Ampel zaehlt die
|
|
Termine ueber eine eigene Mengenvereinigung, weil eine Abfrage je
|
|
Person bei zehn Leuten zehn Abfragen waeren. Zwei Wege zu derselben
|
|
Frage laufen irgendwann auseinander -- hier werden sie
|
|
nebeneinandergelegt. Ohne das waere die Ampel eines Tages leise
|
|
falsch, und niemand haette einen Anhaltspunkt: Sie zeigt ja nur eine
|
|
Farbe.
|
|
===================================================================== */
|
|
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-ampel-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
process.env.PORT = "4385";
|
|
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-team-ampel");
|
|
await new Promise((r) => setTimeout(r, 700));
|
|
const BASIS = "http://127.0.0.1:4385";
|
|
|
|
let fehler = 0, geprueft = 0, offen = 0;
|
|
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
const unklar = (t) => { offen++; console.log(" -- " + t); };
|
|
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
|
const w = await import("./workspace.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;
|
|
}
|
|
|
|
anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
|
anlegen("Rieke", "hand", "CODE-HAND-0001");
|
|
const idFrei = anlegen("Frida", "modi", "CODE-MODI-0001"); // nichts eingetragen
|
|
const idTeils = anlegen("Tomma", "modi", "CODE-MODI-0002"); // knapp unter der Grenze
|
|
const idVoll = anlegen("Vito", "modi", "CODE-MODI-0003"); // darueber
|
|
|
|
/* HEUTE, wie der Server ihn versteht -- nicht wie ich ihn ausrechne.
|
|
Sonst misst die Pruefung um Mitternacht etwas anderes als der
|
|
Server, und der Fehlalarm kommt genau dann, wenn niemand hinsieht. */
|
|
const heute = w.heuteLokal();
|
|
|
|
/* DIE GEHEIMWOERTER. Sie stehen in Titel, Beschreibung und Ort -- also
|
|
in jedem Feld, das ein Termin traegt. Kaeme eines davon durch, waere
|
|
die Grenze aus dem Blueprint gebrochen. */
|
|
const GEHEIM = ["ZAHNARZT-GEHEIM-4711", "PRIVATSACHE-BESCHREIBUNG-8899", "ORT-NUR-FUER-MICH-2233"];
|
|
|
|
function termin(personId, minuten, stunde) {
|
|
d.prepare(`INSERT INTO termine
|
|
(titel, beschreibung, art, beginn, dauer_min, ort, creator_id, teilnehmer_id,
|
|
erledigt, erstellt, erstellt_von)
|
|
VALUES (?,?,'termin',?,?,?,?,NULL,0,?,?)`)
|
|
.run(GEHEIM[0], GEHEIM[1], `${heute}T${String(stunde).padStart(2, "0")}:00:00`,
|
|
minuten, GEHEIM[2], personId, jetzt, personId);
|
|
}
|
|
|
|
/* Tomma: 150 Minuten -- unter der Grenze von 180. */
|
|
termin(idTeils, 90, 14);
|
|
termin(idTeils, 60, 18);
|
|
/* Vito: 240 Minuten -- darueber. */
|
|
termin(idVoll, 240, 16);
|
|
|
|
async function anmelden(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((z) => z.split(";")[0]).join("; ");
|
|
}
|
|
const holeLage = (keks) => fetch(`${BASIS}/workspace/api/teamlage`, { headers: { Cookie: keks } });
|
|
|
|
const keksDogi = await anmelden("admin", "CODE-DOGI-0001");
|
|
const keksHand = await anmelden("creator", "CODE-HAND-0001");
|
|
if (!keksDogi || !keksHand) {
|
|
console.error("FEHLER: Anmeldung misslang -- ohne sie misst diese Datei nichts.");
|
|
process.exit(1);
|
|
}
|
|
|
|
const antwort = await holeLage(keksDogi);
|
|
ok(antwort.status === 200, `die Lage kommt (${antwort.status})`);
|
|
const roh = await antwort.text();
|
|
const lage = JSON.parse(roh);
|
|
const leute = lage.modis || [];
|
|
|
|
/* =====================================================================
|
|
1. KOMMT KLARTEXT DURCH?
|
|
===================================================================== */
|
|
console.log("");
|
|
console.log("--- Was NICHT in der Antwort stehen darf ---");
|
|
|
|
/* DIE VORAUSSETZUNG ZUERST: Gibt es die Termine ueberhaupt? Ohne sie
|
|
waere "kein Geheimwort gefunden" trivial wahr -- der gruene Haken,
|
|
der nichts bestaetigt. */
|
|
const termineDa = d.prepare("SELECT COUNT(*) AS n FROM termine").get()?.n || 0;
|
|
ok(termineDa === 3, `es gibt ${termineDa} Termine mit Klartext zum Finden (3 erwartet)`);
|
|
|
|
for (const wort of GEHEIM) {
|
|
ok(!roh.includes(wort), `"${wort.slice(0, 22)}…" steht nicht in der Antwort`);
|
|
}
|
|
/* UND DIE GEGENPROBE ZUR SUCHE SELBST: Findet sie ein Wort, das
|
|
wirklich dasteht? Sonst waere die Zeile darueber auch dann gruen,
|
|
wenn `roh` leer waere. */
|
|
ok(roh.includes("Frida"), "die Suche funktioniert (der Name steht sehr wohl darin)");
|
|
|
|
/* =====================================================================
|
|
2. STIMMT DIE AMPEL?
|
|
===================================================================== */
|
|
console.log("");
|
|
console.log("--- Die drei Stufen ---");
|
|
|
|
const von = (id) => leute.find((m) => m.id === id);
|
|
for (const [name, id, erwartet, minuten] of [
|
|
["Frida", idFrei, "frei", 0],
|
|
["Tomma", idTeils, "teils", 150],
|
|
["Vito", idVoll, "voll", 240],
|
|
]) {
|
|
const m = von(id);
|
|
const heuteWert = m?.verfuegbar?.[0];
|
|
ok(!!heuteWert, `${name}: die Ampel ist da`);
|
|
ok(heuteWert?.ampel === erwartet,
|
|
`${name}: heute "${heuteWert?.ampel}" (erwartet "${erwartet}")`);
|
|
ok(heuteWert?.minuten === minuten,
|
|
`${name}: ${heuteWert?.minuten} belegte Minuten (erwartet ${minuten})`);
|
|
}
|
|
|
|
ok(von(idFrei)?.verfuegbar?.length === 7,
|
|
`sieben Tage kommen mit (${von(idFrei)?.verfuegbar?.length})`);
|
|
ok(von(idFrei)?.verfuegbar?.[0]?.tag === heute,
|
|
`und der erste ist heute (${von(idFrei)?.verfuegbar?.[0]?.tag})`);
|
|
|
|
/* Die Tage müssen aufeinanderfolgen -- eine Lücke faellt sonst
|
|
niemandem auf, die Farben sehen ja gleich aus. */
|
|
const tage = von(idFrei)?.verfuegbar?.map((t) => t.tag) || [];
|
|
const luecken = tage.filter((t, i) => i > 0
|
|
&& Date.parse(`${t}T12:00:00Z`) - Date.parse(`${tage[i - 1]}T12:00:00Z`) !== 86400000);
|
|
ok(tage.length === 7 && luecken.length === 0,
|
|
`die sieben Tage folgen ohne Luecke aufeinander (${luecken.length} Sprünge)`);
|
|
|
|
/* =====================================================================
|
|
3. DIE GRENZE VON BEIDEN SEITEN
|
|
|
|
Drei Stunden trennen "teils" von "voll". Nur EINE Seite zu pruefen
|
|
hiesse, eine Grenze zu bestaetigen, ohne zu wissen, wo sie liegt.
|
|
===================================================================== */
|
|
console.log("");
|
|
console.log("--- Die Grenze bei drei Stunden ---");
|
|
|
|
const idGrenze = anlegen("Grete", "modi", "CODE-MODI-0004");
|
|
termin(idGrenze, 179, 10);
|
|
const g1 = ((await (await holeLage(keksDogi)).json()).modis || []).find((m) => m.id === idGrenze);
|
|
ok(g1?.verfuegbar?.[0]?.ampel === "teils", `179 Minuten sind "teils" (${g1?.verfuegbar?.[0]?.ampel})`);
|
|
|
|
d.prepare("UPDATE termine SET dauer_min = 180 WHERE creator_id = ?").run(idGrenze);
|
|
const g2 = ((await (await holeLage(keksDogi)).json()).modis || []).find((m) => m.id === idGrenze);
|
|
ok(g2?.verfuegbar?.[0]?.ampel === "voll", `180 Minuten sind "voll" (${g2?.verfuegbar?.[0]?.ampel})`);
|
|
|
|
/* =====================================================================
|
|
4. ZWEI WEGE, EINE ANTWORT
|
|
|
|
Die Ampel zaehlt ueber eine eigene Mengenvereinigung; die offizielle
|
|
Regel heisst termineSichtbar(). Beide muessen dasselbe sagen.
|
|
===================================================================== */
|
|
console.log("");
|
|
console.log("--- Abgleich mit der offiziellen Regel ---");
|
|
|
|
let gleich = 0, ungleich = 0;
|
|
for (const id of [idFrei, idTeils, idVoll, idGrenze]) {
|
|
const regel = w.termineSichtbar({ id }, "t");
|
|
const nachRegel = d.prepare(
|
|
`SELECT COUNT(*) AS n FROM termine t WHERE (${regel.wo}) AND date(t.beginn) = ?`)
|
|
.get(...regel.werte, heute)?.n || 0;
|
|
const lage2 = ((await (await holeLage(keksDogi)).json()).modis || []).find((m) => m.id === id);
|
|
const nachAmpel = lage2?.verfuegbar?.[0]?.termine ?? -1;
|
|
if (nachRegel === nachAmpel) gleich++; else ungleich++;
|
|
}
|
|
ok(gleich === 4 && ungleich === 0,
|
|
`beide Wege zaehlen bei allen ${gleich} Personen dasselbe`);
|
|
|
|
/* =====================================================================
|
|
5. WER SIE SEHEN DARF
|
|
===================================================================== */
|
|
console.log("");
|
|
console.log("--- Wer die Ampel sieht ---");
|
|
|
|
const handLage = await holeLage(keksHand);
|
|
ok(handLage.status === 200, `die rechte Hand sieht die Lage (${handLage.status})`);
|
|
const handRoh = await handLage.text();
|
|
ok(JSON.parse(handRoh).modis?.[0]?.verfuegbar?.length === 7,
|
|
"und die Ampel gehoert dazu -- sie koordiniert das Team");
|
|
for (const wort of GEHEIM) {
|
|
ok(!handRoh.includes(wort), `auch bei ihr steht "${wort.slice(0, 18)}…" nicht drin`);
|
|
}
|
|
|
|
/* =====================================================================
|
|
DIE GEGENPROBE
|
|
|
|
Bis hierhin koennte die Ampel auch einfach immer "frei" sagen -- bei
|
|
Frida stimmt das ja. Also bekommt sie jetzt einen Termin, und die
|
|
Farbe MUSS sich aendern.
|
|
===================================================================== */
|
|
console.log("");
|
|
console.log("--- Gegenprobe: ein Termin veraendert die Farbe ---");
|
|
|
|
termin(idFrei, 300, 9);
|
|
const nachher = ((await (await holeLage(keksDogi)).json()).modis || []).find((m) => m.id === idFrei);
|
|
ok(nachher?.verfuegbar?.[0]?.ampel === "voll",
|
|
`aus "frei" wird "voll" (${nachher?.verfuegbar?.[0]?.ampel})`);
|
|
|
|
d.prepare("DELETE FROM termine WHERE creator_id = ?").run(idFrei);
|
|
const zurueck = ((await (await holeLage(keksDogi)).json()).modis || []).find((m) => m.id === idFrei);
|
|
ok(zurueck?.verfuegbar?.[0]?.ampel === "frei",
|
|
`und zurueckgenommen wieder "frei" (${zurueck?.verfuegbar?.[0]?.ampel})`);
|
|
|
|
/* =======================================================================
|
|
DIE UEBERSCHRIFT MUSS ZU DEM PASSEN, WAS DARUNTER STEHT (11.09.2026)
|
|
|
|
Diese Datei hat von Anfang an geprueft, dass SIEBEN Tage kommen und
|
|
der erste heute ist. Was sie nie angesehen hat: was ueber den sieben
|
|
Kaestchen steht. Dort stand "Kann heute" -- im Singular, ueber einer
|
|
ganzen Woche.
|
|
|
|
Wer nur die Ueberschrift liest (und das tut man bei einer Zeile in
|
|
Versalien), haelt den ganzen Streifen fuer den heutigen Tag und liest
|
|
sechs Kaestchen falsch. Der Satz DARUNTER war immer richtig; zwei
|
|
Aussagen ueber demselben Bild, und die auffaelligere war die falsche.
|
|
|
|
Eine Pruefung ueber die Schnittstelle allein konnte das nicht finden
|
|
-- die Zahl stimmte ja. Deshalb hier eine Pruefung auf den TEXT, an
|
|
derselben Stelle wie die Zahl, damit beide zusammen gelesen werden.
|
|
======================================================================= */
|
|
console.log("");
|
|
console.log("--- Was steht ueber den sieben Tagen? ---");
|
|
{
|
|
const quelle = readFileSync(new URL("../workspace/assets/js/teamlage.js", import.meta.url), "utf8");
|
|
const schild = /tampel__schild',\s*'([^']*)'/.exec(quelle)?.[1] || "";
|
|
ok(!!schild, `die Ueberschrift steht im Skript ("${schild}")`);
|
|
ok(!/heute/i.test(schild),
|
|
`und sie spricht nicht von EINEM Tag ("${schild}")`);
|
|
ok(/sieben|woche|7/i.test(schild),
|
|
"sondern von dem Zeitraum, den sie zeigt");
|
|
|
|
/* GEGENPROBE: Findet die Suche das Wort ueberhaupt? Sonst waere die
|
|
Zeile darueber gruen, weil sie nie etwas liest. */
|
|
ok(/heute/i.test("Kann heute"), "und die Suche wuerde 'heute' erkennen");
|
|
}
|
|
|
|
/* ---------- Ende ------------------------------------------------------ */
|
|
d.close();
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Windows haelt manchmal */ }
|
|
|
|
console.log("");
|
|
console.log(`${fehler === 0 ? "BESTANDEN" : "FEHLGESCHLAGEN"}`
|
|
+ ` -- ${geprueft} geprueft, ${fehler} Fehler, ${offen} nicht nachsehbar`);
|
|
process.exit(fehler === 0 ? 0 : 1);
|