Filipe, 01.10.2026: „mach das. es soll fest sein." VanVan hatte gemeldet: „Man kann sich mit seinem Zugangscode immer noch über jeden Button der Startseite anmelden, egal welche Rolle man hat." ICH HATTE ZUERST ABGERATEN -- und lag falsch, weil ich eine alte Lage beschrieben habe. Am 09.09.2026 hatte Filipe entschieden, dass die Modis KEINE eigene Eingangskachel bekommen: „damit die von der workspace auch nicht mal sehen dass die modis von mir einen eigenen zugang haben." Wer keine Kachel hat, muss irgendeine nehmen koennen -- daher der stille Zugang. SEIT DER HAUSTRENNUNG AM 24.09.2026 STIMMT DAS NICHT MEHR. Auf `crew.` steht laengst ein eigener Kachelsatz mit ALLEN fuenf Rollen: DogFather, rechte Hand, linke Hand, Modi, Community (CREW_KACHEL, und crew-index.html zeigt sie). Das Verbergen leistet seither die ADRESSE -- wer sie nicht kennt, findet die Wand nicht; wer sie kennt, liest die Rollennamen ohnehin offen darauf. Der stille Zugang war damit ein Rest. Er hat niemanden mehr geschuetzt und nur dafuer gesorgt, dass die Kachelwahl folgenlos blieb. Nachgesehen habe ich das erst, NACHDEM Filipe widersprochen hat; die Kachelsaetze standen die ganze Zeit im Quelltext. WAS SICH NICHT AENDERT: Auf der Agenturwand war der stille Zugang nie aktiv. Ein Team-Dogi-Code verhaelt sich dort weiterhin wie ein erfundener -- gleiche Antwort, gleicher Weg, gleiche Dauer. Das ist jetzt ausdruecklich gemessen. EINE PRUEFADRESSE IST KEINE WAND. `127.0.0.1` ist weder crew. noch Agentur. Ohne den stillen Zugang gaelte dort der Agentursatz -- und kein Modi kaeme mehr herein. Fuenfzig Pruefdateien melden Team-Rollen ueber diese Adresse an. Auf einer Adresse ohne Wand gibt es deshalb ALLE Kacheln; welche auf welcher ECHTEN Wand steht, misst pruef-modi-verborgen mit ausdruecklichem Host-Kopf. GEPRUEFT -- pruef-modi-verborgen 87/0 (war 85; die fuenf Zeilen „jede Kachel geht" sind durch sieben ersetzt, die die neue Regel und ihre Gegenproben messen). Die Anzahl ist Zeile fuer Zeile verglichen. Modi-Kachel + Modi-Code -> herein admin/hand/linke/gast -> abgewiesen rechte Hand auf ihrer Kachel -> herein Agenturwand + Modi-Code -> wie ein erfundener pruef-crew-adresse 169/0 (unveraenderte Anzahl, zwei Zeilen umgedreht). SECHZEHN PRUEFDATEIEN MELDETEN SICH UEBER FREMDE KACHELN AN -- ein Rest derselben Zeit. Systematisch gesucht statt einzeln entdeckt: Waere ich dem roten Lauf hinterhergelaufen, haette ich beim zwoelften aufgehoert. UND DABEI EIN EIGENER FEHLER: Mein erster Durchlauf las die Rolle am CODENAMEN ab (CODE-MODI- -> modi). Das ging gut, bis „Nane" kam: ein Modi mit dem Code CODE-NANE-0001. Zwei Pruefungen wurden rot, und zwar an einer Stelle („die Stimmen stimmen"), die mit Anmeldung nichts zu tun hat. Ein Codename ist eine Beschriftung, keine Tatsache -- die Rolle steht in `anlegen()`. Danach abgeleitet blieben genau zwei Abweichungen uebrig, und beide sind absichtliche Gegenproben. Drei Browserpruefungen tippten die Creator-Kachel mit einem Modi-Code. Die Modi-Kachel gibt es nur auf der Crew-Wand, und ein Browser auf 127.0.0.1 bekommt die Agenturwand; sie melden sich jetzt ueber die Schnittstelle an und bekommen den Keks. Gemessen werden soll dort, was ein Modi SIEHT -- nicht, wie er hereinkommt. Grün: pruef-modi-verborgen, pruef-crew-adresse, pruef-treff 85/0, pruef-galerie, pruef-kanaele, pruef-modi-katalog 150/0, pruef-modi-ideen, pruef-modi-kategorien, pruef-modi-checkliste 75/0, pruef-modi-livecheck, pruef-kachelraster, pruef-team-ampel 32/0, pruef-team-stufen 47/0, pruef-wunschliste, pruef-bremse, pruef-gespraech, pruef-personen-formular 43/0, pruef-start-ansicht, pruef-community-sicht, pruef-reaktion 421/0, pruef-abzeichen, pruef-chat, pruef-chat-kanaele 81/0, pruef-entwicklung 79/0, pruef-bewerbung-aufgaben 154/0, pruef-struktur, pruef-zwischenspeicher 34/0. `code_kennung` wird weiter geschrieben, aber nicht mehr gelesen -- sie war der Suchschluessel des stillen Weges. Stehen gelassen: Eine Spalte zu entfernen ist eine Schemaaenderung mit Sicherung, und sie kostet nichts. Co-Authored-By: Claude Opus 5 <[email protected]>
304 lines
14 KiB
JavaScript
304 lines
14 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");
|
|
/* DER PORT MUSS MEINER SEIN (nachgeruestet 19.09.2026).
|
|
Ohne diese Zeile startet der eigene Server bei belegtem Port
|
|
STILL nicht -- und alles Folgende misst gegen einen fremden
|
|
Stand. Das ist der dritte Ausgang: belegt ist etwas anderes
|
|
als kaputt. */
|
|
const PORT = await eigenerPort(import.meta, "pruef-team-ampel");
|
|
process.env.PORT = `${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";
|
|
import { eigenerPort } from "./helfer-port.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:${PORT}`;
|
|
|
|
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("hand", "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.leute || [];
|
|
|
|
/* =====================================================================
|
|
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()).leute || []).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()).leute || []).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()).leute || []).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).leute?.[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()).leute || []).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()).leute || []).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);
|