Verfuegbarkeits-Ampel im Eingang -- und eine eigene Gruppe fuer Team Dogi
Filipe: "ich will dass die kacheln von den modis bei der rolle dogfather eine eigene kategorie haben. damit ich nicht zwischen den kacheln suchen muss." DIE AMPEL (Blueprint 7.1). Der Eingang zeigt je Person sieben Tage: gruen frei, gelb belegt, rot voll ab 180 Minuten. Die Abfrage sammelt Termine ueber VIER Wege (creator_id, teilnehmer_id, erstellt_von und die Tabelle termin_teilnehmer) -- ueber nur einen davon waeren die meisten Termine unsichtbar geblieben und die Ampel dauerhaft gruen. Sie waehlt NIE titel, beschreibung oder ort: Filipe soll sehen, WANN jemand kann, nicht WAS die Person vorhat. Belegung ist Arbeitslage, Inhalt ist privat. DIE GRUPPE. Die zwei Team-Kacheln standen zwischen einundzwanzig anderen. Jetzt tragen sie `gruppe: "Team Dogi"` und `gruppeNach: "Täglich"`; start.js setzt eine so markierte Gruppe direkt HINTER die genannte statt ans Ende. Ohne das waere sie unten gelandet -- richtig gruppiert und trotzdem zum Suchen. Das Ideen-Board ist dabei aus workspace/assets/js/bereiche.js ausgezogen. Es stand dort mit `rollen: ['admin']` in einer Datei, die jeder Modi herunterlaedt: die Kachel war unsichtbar, ihr Name nicht. Jetzt liefert der Server sie, wie den Eingang auch. DREI FEHLER, DIE DER BILDSCHIRM GEZEIGT HAT, NICHT DER CODE: Das Profilbild sprengte die Karte. teamlage.js baute ein blankes <img> in `.tperson__zeichen` -- ohne die Klasse `tperson__bild`, die es auf 44 px begrenzt. Gemeldet hat es Filipe mit einem Bildschirm- foto, nicht eine Pruefung. Der Eingang hatte ueberhaupt keine Buehne. `zuSeite()` sucht nur in bereiche.js, und die Eingangs-Kachel kommt vom Server -- der Rueck- fall war ausgerechnet der Spicy-Wasserfall. Jetzt haengt das Bild an `data-buehne="eingang"` im CSS, wo kein Skript daran vorbeikommt. Pausierte Mitglieder verschwanden. `WHERE aktiv = 1` versteckte sie samt ihrer offenen Meldungen. Jetzt stehen sie hinten, sichtbar gekennzeichnet. DIE ERWARTUNG IN pruef-start-ansicht steht auf FUENF Gruppen, in ihrer Reihenfolge -- die Position ist hier die eigentliche Aussage. Eine Gruppe, die ans Ende rutscht, faellt auf dem Bildschirm kaum auf. Die Kachelzahl blieb bei 24: umgezogen, nichts hinzugefuegt. pruef-team-ampel 28, pruef-team-stufen 24, pruef-rollen 274, pruef-crew-adresse 123, pruef-modi-ideen 30, pruef-modi-wortleck 5, pruef-zwischenspeicher 21, pruef-start-ansicht gruen. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
/* =====================================================================
|
||||
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 } 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})`);
|
||||
|
||||
/* ---------- 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);
|
||||
Reference in New Issue
Block a user