Die Kachel "Dashboard" fuehrte nirgendwohin -- man stand ja schon auf der
Startseite. Statt sie zu streichen bekommt sie ein Ziel: die
Gesamtuebersicht je Creator, die das Konzept auf Seite 1 als "UEBERSICHT
-- alles zentral" verspricht und die es bisher nirgends gab. Die
Startseite bleibt der Einstieg (was ist dran, wohin gehe ich), das
Dashboard ist der Ueberblick (wie steht es um wen).
Eine Karte je Creator: ueberfaellig, dringend, offen, in Arbeit, im
Review, erledigt in 30 Tagen, dazu der naechste Termin, der Start-Check
als Balken und wie lange die Person nicht mehr da war. Jede Zahl ist ein
Verweis -- eine Zahl ohne Weg dorthin waere nur ein schlechtes Gewissen.
Sortiert nach dem, was Handlung verlangt, nicht alphabetisch.
Sichtbarkeit serverseitig wie ueberall: Creator sieht sich selbst
("Mein Stand"), Scout nur seine zugeteilten, DogFather und Manager alle.
Der naechste Review-Termin faellt fuer alle ausser der Leitung weg.
Nebenbei zwei alte Fehler gefunden und behoben:
1. Die Kopfleiste schob auf einem 390px-Handy 214 px aus dem Bild -- der
Abmelden-Knopf war dort auf JEDER Seite unerreichbar. Ursache: In der
Flex-Zeile fehlte min-width: 0, also konnte nichts schrumpfen. Auf
kleinen Schirmen weicht jetzt die Marke (steht direkt darunter noch
einmal als Ueberschrift) und der Zurueck-Knopf zeigt nur den Pfeil.
Geprueft von 320 bis 1440 px auf 14 Seiten.
2. .inhalt--breit, .kopf-zeile, .zurueck und .leise standen in
aufgaben.css -- einer Datei, die eine neue Seite gar nicht laden muss.
Derselbe Fehler wie frueher bei .knopf-still und .schalter. Jetzt in
start.css, die jede Seite laedt.
Co-Authored-By: Claude Opus 5 <[email protected]>
246 lines
11 KiB
JavaScript
246 lines
11 KiB
JavaScript
/* ===================================================================
|
|
Pruefung: Dashboard (uebersicht.html + /api/uebersicht/creator)
|
|
|
|
Startet einen echten Express-Server mit einer frischen Testdatenbank,
|
|
legt DogFather, Manager, Scout und zwei Creator an und prueft dann,
|
|
was jede Rolle sieht. Wichtig ist vor allem der NEGATIVE Fall: Der
|
|
Scout darf den nicht betreuten Creator nirgends finden, und niemand
|
|
ausser der Leitung darf den Review-Termin sehen.
|
|
=================================================================== */
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-uebersicht-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "pruef.sqlite");
|
|
process.env.PORT = "4176";
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
|
|
process.env.SITE_ACCESS_CODE_VANVAN = "VANVAN-TEST-1";
|
|
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
|
|
|
const express = (await import("express")).default;
|
|
const echteCookie = express.response.cookie;
|
|
express.response.cookie = function (name, wert, opt) {
|
|
return echteCookie.call(this, name, wert, { ...(opt || {}), secure: false });
|
|
};
|
|
|
|
await import("./index.js");
|
|
await new Promise((r) => setTimeout(r, 700));
|
|
|
|
const BASIS = "http://127.0.0.1:4176";
|
|
|
|
/* Notbremse. index.js faengt uncaughtException ab, damit die Website
|
|
nicht an einem Einzelfehler stirbt -- im Test heisst das aber: ein
|
|
Fehler bliebe stumm und der Lauf haenge fuer immer. */
|
|
const notbremse = setTimeout(() => {
|
|
console.log("\nABBRUCH: Der Testlauf hing über 90 Sekunden.");
|
|
process.exit(1);
|
|
}, 90_000);
|
|
notbremse.unref?.();
|
|
|
|
let fehler = 0;
|
|
const ok = (bedingung, text) => {
|
|
console.log((bedingung ? " ok " : " FEHL ") + text);
|
|
if (!bedingung) fehler++;
|
|
};
|
|
|
|
/* ---------- Testdaten direkt in die Datenbank ------------------------ */
|
|
|
|
/* Die Datenbank wird absichtlich erst beim ersten echten Zugriff
|
|
geoeffnet -- ein Fehler dort soll nie die Website mitreissen. Fuer
|
|
diesen Test heisst das: erst einen (scheiternden) Anmeldeversuch
|
|
schicken, damit Schema und Umstellungen laufen. Ohne das faende die
|
|
eigene Verbindung unten noch gar keine Tabellen. */
|
|
await fetch(BASIS + "/workspace/api/anmelden", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ rolle: "admin", code: "gibt-es-nicht" }),
|
|
}).catch(() => {});
|
|
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes } = await import("node:crypto");
|
|
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const N = 32768;
|
|
function anlegen(name, rolle, code) {
|
|
const salt = randomBytes(16).toString("hex");
|
|
const hash = scryptSync(code, salt, 64, { N, r: 8, p: 1, maxmem: 96 * 1024 * 1024 })
|
|
.toString("hex");
|
|
d.prepare(
|
|
"INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)"
|
|
).run(name, rolle, hash, salt, N, new Date().toISOString());
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
}
|
|
|
|
const idDogi = anlegen("Test-DogFather", "admin", "CODE-DOGI-0001");
|
|
const idManager = anlegen("Test-Manager", "manager", "CODE-MANA-0001");
|
|
const idScout = anlegen("Test-Scout", "scout", "CODE-SCOU-0001");
|
|
const idAnna = anlegen("Anna Betreut", "creator", "CODE-ANNA-0001");
|
|
const idBen = anlegen("Ben Fremd", "creator", "CODE-BENN-0001");
|
|
|
|
/* Anna wird vom Scout betreut, Ben nicht. */
|
|
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)")
|
|
.run(idAnna, idScout, new Date().toISOString());
|
|
|
|
const jetzt = new Date().toISOString();
|
|
const gestern = new Date(Date.now() - 86400_000).toISOString().slice(0, 10);
|
|
const morgen = new Date(Date.now() + 86400_000).toISOString().slice(0, 16);
|
|
|
|
/* Aufgaben: eine ueberfaellige, zwei offene, je eine in Arbeit, im
|
|
Review und erledigt -- damit jede Zahl der Karte einen anderen Wert
|
|
bekommt und eine vertauschte Spalte sofort auffiele. */
|
|
const aufgabe = (creator, status, frist, erledigt) =>
|
|
d.prepare(
|
|
"INSERT INTO aufgaben (titel, creator_id, status, frist, erledigt_am, erstellt, erstellt_von) VALUES (?,?,?,?,?,?,?)"
|
|
).run("Test", creator, status, frist, erledigt, jetzt, idDogi);
|
|
|
|
aufgabe(idAnna, "offen", gestern, null); // ueberfaellig
|
|
aufgabe(idAnna, "offen", null, null);
|
|
aufgabe(idAnna, "arbeit", null, null);
|
|
aufgabe(idAnna, "review", null, null);
|
|
aufgabe(idAnna, "erledigt", null, jetzt);
|
|
aufgabe(idBen, "offen", gestern, null);
|
|
|
|
/* Ein dringender, offener Punkt in den Bereichen -- und einer, der
|
|
erledigt ist. Der erledigte darf NICHT mitzaehlen. */
|
|
const eintrag = (creator, dringlichkeit, status) =>
|
|
d.prepare(
|
|
"INSERT INTO eintraege (bereich, art, titel, datum, dringlichkeit, status, creator_id, erstellt, erstellt_von) VALUES ('live','notiz','Test',?,?,?,?,?,?)"
|
|
).run(gestern, dringlichkeit, status, creator, jetzt, idDogi);
|
|
eintrag(idAnna, "hoch", "offen");
|
|
eintrag(idAnna, "hoch", "erledigt");
|
|
eintrag(idAnna, "mittel", "offen");
|
|
|
|
/* Termin morgen -- der naechste anstehende. Ein bereits erledigter und
|
|
ein vergangener duerfen ihn nicht verdraengen. */
|
|
const termin = (beginn, erledigt) =>
|
|
d.prepare(
|
|
"INSERT INTO termine (titel, beginn, creator_id, erledigt, erstellt, erstellt_von) VALUES ('Test',?,?,?,?,?)"
|
|
).run(beginn, idAnna, erledigt, jetzt, idDogi);
|
|
termin(morgen, 0);
|
|
termin(new Date(Date.now() + 3 * 86400_000).toISOString().slice(0, 16), 0);
|
|
termin(new Date(Date.now() + 2 * 86400_000).toISOString().slice(0, 16), 1); // erledigt
|
|
termin("2020-01-01T10:00", 0); // vergangen
|
|
|
|
/* Start-Check: drei geprueft, davon einer mit Handlungsbedarf. */
|
|
const check = (feld, punkt, bewertung) =>
|
|
d.prepare(
|
|
"INSERT INTO startcheck (creator_id, feld, punkt, bewertung, geaendert) VALUES (?,?,?,?,?)"
|
|
).run(idAnna, feld, punkt, bewertung, jetzt);
|
|
check("technik", "licht", "ok");
|
|
check("technik", "ton", "mittel");
|
|
check("inhalt", "plan", "handlung");
|
|
check("inhalt", "offen", null); // angelegt, aber nicht bewertet -> zaehlt nicht
|
|
|
|
/* Review-Termin im Profil -- nur die Leitung darf ihn sehen. */
|
|
d.prepare("INSERT INTO profile (person_id, naechster_review) VALUES (?,?)")
|
|
.run(idAnna, "2026-12-24");
|
|
|
|
d.close();
|
|
|
|
/* ---------- Hilfen --------------------------------------------------- */
|
|
|
|
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 }),
|
|
redirect: "manual",
|
|
});
|
|
const keks = (a.headers.getSetCookie?.() || []).map((c) => c.split(";")[0]).join("; ");
|
|
if (!a.ok || !keks) throw new Error(`Anmeldung ${rolle} gescheitert (${a.status})`);
|
|
return keks;
|
|
}
|
|
const hole = (pfad, keks) =>
|
|
fetch(BASIS + pfad, { headers: { cookie: keks }, redirect: "manual" });
|
|
|
|
/* ---------- Pruefungen ------------------------------------------------ */
|
|
|
|
console.log("\n1) Ohne Anmeldung");
|
|
{
|
|
const a = await fetch(BASIS + "/workspace/uebersicht.html", { redirect: "manual" });
|
|
ok(a.status === 302, `Seite leitet um statt sie auszuliefern (${a.status})`);
|
|
const b = await fetch(BASIS + "/workspace/api/uebersicht/creator", { redirect: "manual" });
|
|
ok(b.status === 401, `Schnittstelle antwortet 401 (${b.status})`);
|
|
}
|
|
|
|
console.log("\n2) DogFather sieht alle");
|
|
const kDogi = await anmelden("admin", "CODE-DOGI-0001");
|
|
{
|
|
const a = await hole("/workspace/uebersicht.html", kDogi);
|
|
ok(a.status === 200, `Seite wird ausgeliefert (${a.status})`);
|
|
const d2 = await (await hole("/workspace/api/uebersicht/creator", kDogi)).json();
|
|
ok(d2.creator.length === 2, `beide Creator (${d2.creator.length})`);
|
|
ok(d2.eigen === false, "eigen = false");
|
|
ok(d2.check_gesamt === 16, "Start-Check-Gesamtzahl mitgeliefert");
|
|
const anna = d2.creator.find((c) => c.name === "Anna Betreut");
|
|
ok(anna.ueberfaellig === 1, `Anna: 1 ueberfaellig (${anna.ueberfaellig})`);
|
|
ok(anna.offen === 2, `Anna: 2 offen (${anna.offen})`);
|
|
ok(anna.arbeit === 1, `Anna: 1 in Arbeit (${anna.arbeit})`);
|
|
ok(anna.review === 1, `Anna: 1 im Review (${anna.review})`);
|
|
ok(anna.erledigt30 === 1, `Anna: 1 erledigt in 30 Tagen (${anna.erledigt30})`);
|
|
ok(anna.dringend === 1, `Anna: 1 dringend, Erledigtes zaehlt nicht mit (${anna.dringend})`);
|
|
ok(anna.check_geprueft === 3, `Anna: 3 Punkte bewertet (${anna.check_geprueft})`);
|
|
ok(anna.check_handlung === 1, `Anna: 1 Punkt mit Handlungsbedarf (${anna.check_handlung})`);
|
|
ok(String(anna.naechster_termin).startsWith(
|
|
new Date(Date.now() + 86400_000).toISOString().slice(0, 10)),
|
|
`Anna: naechster Termin ist morgen, nicht der erledigte (${anna.naechster_termin})`);
|
|
ok(anna.betreuer === "Test-Scout", `Anna: Betreuer steht dran (${anna.betreuer})`);
|
|
ok(anna.naechster_review === "2026-12-24", "Leitung sieht den Review-Termin");
|
|
ok(anna.hat_profil === 1, "Profil erkannt");
|
|
const ben = d2.creator.find((c) => c.name === "Ben Fremd");
|
|
ok(ben.betreuer === null, "Ben hat keinen Betreuer");
|
|
ok(ben.hat_profil === 0, "Ben hat kein Profil");
|
|
}
|
|
|
|
console.log("\n3) Manager sieht dasselbe");
|
|
{
|
|
const k = await anmelden("manager", "CODE-MANA-0001");
|
|
const d2 = await (await hole("/workspace/api/uebersicht/creator", k)).json();
|
|
ok(d2.creator.length === 2, `beide Creator (${d2.creator.length})`);
|
|
ok(d2.creator[0].naechster_review !== undefined
|
|
|| d2.creator[1].naechster_review !== undefined, "Manager sieht Review-Termine");
|
|
}
|
|
|
|
console.log("\n4) Scout sieht nur die betreute Person");
|
|
{
|
|
const k = await anmelden("scout", "CODE-SCOU-0001");
|
|
const a = await hole("/workspace/uebersicht.html", k);
|
|
ok(a.status === 200, `Seite wird ausgeliefert (${a.status})`);
|
|
const d2 = await (await hole("/workspace/api/uebersicht/creator", k)).json();
|
|
ok(d2.creator.length === 1, `genau ein Creator (${d2.creator.length})`);
|
|
ok(d2.creator[0].name === "Anna Betreut", "und zwar die betreute");
|
|
ok(!d2.creator.some((c) => c.name === "Ben Fremd"), "Ben taucht NICHT auf");
|
|
ok(!("naechster_review" in d2.creator[0]), "Scout sieht KEINEN Review-Termin");
|
|
}
|
|
|
|
console.log("\n5) Creator sieht nur sich selbst");
|
|
{
|
|
const k = await anmelden("creator", "CODE-ANNA-0001");
|
|
const a = await hole("/workspace/uebersicht.html", k);
|
|
ok(a.status === 200, `Seite wird ausgeliefert (${a.status})`);
|
|
const d2 = await (await hole("/workspace/api/uebersicht/creator", k)).json();
|
|
ok(d2.eigen === true, "eigen = true");
|
|
ok(d2.creator.length === 1, `genau ein Eintrag (${d2.creator.length})`);
|
|
ok(d2.creator[0].name === "Anna Betreut", "und zwar er selbst");
|
|
ok(!("naechster_review" in d2.creator[0]), "Creator sieht KEINEN Review-Termin");
|
|
}
|
|
|
|
console.log("\n6) Scout ohne Zuteilung bekommt eine leere, keine fremde Liste");
|
|
{
|
|
const d3 = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
d3.prepare("DELETE FROM betreuung WHERE creator_id = ?").run(idAnna);
|
|
d3.close();
|
|
const k = await anmelden("scout", "CODE-SCOU-0001");
|
|
const d2 = await (await hole("/workspace/api/uebersicht/creator", k)).json();
|
|
ok(d2.creator.length === 0, `leer statt alle (${d2.creator.length})`);
|
|
}
|
|
|
|
console.log(`\n${fehler === 0 ? "Alles in Ordnung." : fehler + " Prüfung(en) fehlgeschlagen."}`);
|
|
/* Windows gibt die Datei erst frei, wenn der Server sie loslaesst -- das
|
|
Aufraeumen darf den Lauf deshalb nicht zum Scheitern bringen. Der
|
|
Ordner liegt im Temp-Verzeichnis und verschwindet ohnehin. */
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
process.exit(fehler === 0 ? 0 : 1);
|