Workspace: Dashboard gebaut, Kopfleiste auf dem Handy repariert
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]>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
/* Misst die einzelnen Teile der Kopfleiste auf dem Handy -- damit die
|
||||
Reparatur an der richtigen Stelle ansetzt und nicht geraten wird. */
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const ordner = mkdtempSync(join(tmpdir(), "ws-messen-"));
|
||||
process.env.WORKSPACE_DB = join(ordner, "pruef.sqlite");
|
||||
process.env.PORT = "4179";
|
||||
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 ec = express.response.cookie;
|
||||
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
|
||||
await import("./index.js");
|
||||
await new Promise((r) => setTimeout(r, 700));
|
||||
const BASIS = "http://127.0.0.1:4179";
|
||||
setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 120_000).unref?.();
|
||||
|
||||
await fetch(BASIS + "/workspace/api/anmelden", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
|
||||
|
||||
const { DatabaseSync } = await import("node:sqlite");
|
||||
const { scryptSync, randomBytes } = await import("node:crypto");
|
||||
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
const salt = randomBytes(16).toString("hex");
|
||||
const hash = scryptSync("CODE-DOGI-0001", salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
||||
/* Bewusst ein langer Name -- kurze Namen wuerden den Fehler verstecken. */
|
||||
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
|
||||
.run("Filipe Fernandes Ribeiro", "admin", hash, salt, 32768, new Date().toISOString());
|
||||
d.close();
|
||||
|
||||
const { chromium } = await import(
|
||||
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
||||
const browser = await chromium.launch();
|
||||
|
||||
for (const breite of [320, 390, 430]) {
|
||||
const kontext = await browser.newContext({ viewport: { width: breite, height: 844 } });
|
||||
const seite = await kontext.newPage();
|
||||
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
||||
await seite.click('.rolle[data-rolle="admin"]');
|
||||
await seite.fill("#code", "CODE-DOGI-0001");
|
||||
await seite.click("button[type=submit]");
|
||||
await seite.waitForURL("**/start.html", { timeout: 15000 });
|
||||
await seite.goto(BASIS + "/workspace/uebersicht.html", { waitUntil: "networkidle" });
|
||||
await seite.waitForTimeout(300);
|
||||
const r = await seite.evaluate(() => {
|
||||
const k = document.querySelector(".kopfleiste");
|
||||
const teil = (e) => ({
|
||||
was: (e.tagName + "." + String(e.className).split(" ")[0]).slice(0, 34),
|
||||
breite: Math.round(e.getBoundingClientRect().width),
|
||||
rechts: Math.round(e.getBoundingClientRect().right),
|
||||
});
|
||||
return {
|
||||
fenster: window.innerWidth,
|
||||
ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
||||
kinder: [...k.children].map(teil),
|
||||
rechtsKinder: [...(k.querySelector(".kopfleiste__rechts")?.children || [])].map(teil),
|
||||
abmeldenSichtbar: (() => {
|
||||
const a = document.querySelector(".abmelden")?.getBoundingClientRect();
|
||||
return a ? a.right <= window.innerWidth : null;
|
||||
})(),
|
||||
};
|
||||
});
|
||||
console.log(`\n--- ${breite}px (Überstand ${r.ueber}px, Abmelden erreichbar: ${r.abmeldenSichtbar})`);
|
||||
for (const t of r.kinder) console.log(` ${t.was.padEnd(34)} b=${String(t.breite).padStart(4)} rechts=${t.rechts}`);
|
||||
console.log(" darin:");
|
||||
for (const t of r.rechtsKinder) console.log(` ${t.was.padEnd(32)} b=${String(t.breite).padStart(4)} rechts=${t.rechts}`);
|
||||
await kontext.close();
|
||||
}
|
||||
await browser.close();
|
||||
try { rmSync(ordner, { recursive: true, force: true }); } catch {}
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,182 @@
|
||||
/* ===================================================================
|
||||
Browserpruefung des Dashboards.
|
||||
|
||||
Startet denselben Testserver wie pruef-uebersicht.mjs, meldet sich
|
||||
als DogFather bzw. als Creator an und sieht mit einem echten Browser
|
||||
nach: Fehler in der Konsole, ueberstehender Inhalt auf dem Handy,
|
||||
und ob die Karten ueberhaupt gezeichnet werden. Legt Screenshots ab.
|
||||
=================================================================== */
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const ordner = mkdtempSync(join(tmpdir(), "ws-uebersicht-b-"));
|
||||
process.env.WORKSPACE_DB = join(ordner, "pruef.sqlite");
|
||||
process.env.PORT = "4177";
|
||||
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:4177";
|
||||
let fehler = 0;
|
||||
const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||||
|
||||
const notbremse = setTimeout(() => {
|
||||
console.log("\nABBRUCH: hing über 150 Sekunden.");
|
||||
process.exit(1);
|
||||
}, 150_000);
|
||||
notbremse.unref?.();
|
||||
|
||||
/* ---------- Daten ------------------------------------------------------ */
|
||||
|
||||
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;
|
||||
const jetzt = new Date().toISOString();
|
||||
function anlegen(name, rolle, code, letzterLogin) {
|
||||
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, letzter_login) VALUES (?,?,?,?,?,1,?,?)")
|
||||
.run(name, rolle, hash, salt, N, jetzt, letzterLogin ?? null);
|
||||
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||||
}
|
||||
|
||||
const idDogi = anlegen("Test-DogFather", "admin", "CODE-DOGI-0001", jetzt);
|
||||
const namen = ["Anna Sturm", "Ben Kohl", "Cara Weiss", "Deniz Yildiz"];
|
||||
const ids = namen.map((n, i) =>
|
||||
anlegen(n, "creator", `CODE-CRE-000${i + 1}`,
|
||||
i === 3 ? null : new Date(Date.now() - i * 9 * 86400_000).toISOString()));
|
||||
|
||||
const gestern = new Date(Date.now() - 86400_000).toISOString().slice(0, 10);
|
||||
const aufgabe = (c, status, frist, erl) =>
|
||||
d.prepare("INSERT INTO aufgaben (titel, creator_id, status, frist, erledigt_am, erstellt, erstellt_von) VALUES ('Test',?,?,?,?,?,?)")
|
||||
.run(c, status, frist, erl, jetzt, idDogi);
|
||||
|
||||
/* Bewusst unterschiedliche Lagen, damit im Bild sowohl eine ruhige als
|
||||
auch eine brennende Karte zu sehen ist. */
|
||||
for (let k = 0; k < 3; k++) aufgabe(ids[0], "offen", gestern, null);
|
||||
aufgabe(ids[0], "arbeit", null, null);
|
||||
aufgabe(ids[1], "offen", null, null);
|
||||
aufgabe(ids[1], "erledigt", null, jetzt);
|
||||
aufgabe(ids[2], "review", null, null);
|
||||
|
||||
d.prepare("INSERT INTO eintraege (bereich, art, titel, datum, dringlichkeit, status, creator_id, erstellt, erstellt_von) VALUES ('live','notiz','Test',?,'hoch','offen',?,?,?)")
|
||||
.run(gestern, ids[0], jetzt, idDogi);
|
||||
|
||||
d.prepare("INSERT INTO termine (titel, beginn, creator_id, erledigt, erstellt, erstellt_von) VALUES ('Test',?,?,0,?,?)")
|
||||
.run(new Date(Date.now() + 2 * 86400_000).toISOString().slice(0, 16), ids[1], jetzt, idDogi);
|
||||
|
||||
for (const [i, feld] of ["a", "b", "c", "d", "e", "f", "g"].entries()) {
|
||||
d.prepare("INSERT INTO startcheck (creator_id, feld, punkt, bewertung, geaendert) VALUES (?,?,?,?,?)")
|
||||
.run(ids[0], feld, "p", i === 0 ? "handlung" : "ok", jetzt);
|
||||
}
|
||||
for (let i = 0; i < 16; i++) {
|
||||
d.prepare("INSERT INTO startcheck (creator_id, feld, punkt, bewertung, geaendert) VALUES (?,?,?,'ok',?)")
|
||||
.run(ids[1], "f" + i, "p", jetzt);
|
||||
}
|
||||
d.prepare("INSERT INTO profile (person_id, naechster_review) VALUES (?,?)").run(ids[0], "2026-09-30");
|
||||
d.prepare("INSERT INTO profile (person_id, naechster_review) VALUES (?,?)").run(ids[1], null);
|
||||
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(ids[0], idDogi, jetzt);
|
||||
d.close();
|
||||
|
||||
/* ---------- Browser ---------------------------------------------------- */
|
||||
|
||||
const { chromium } = await import(
|
||||
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
||||
|
||||
const browser = await chromium.launch();
|
||||
|
||||
async function ansehen(name, rolle, code, groesse, datei) {
|
||||
const kontext = await browser.newContext({ viewport: groesse });
|
||||
const seite = await kontext.newPage();
|
||||
const konsole = [];
|
||||
seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); });
|
||||
seite.on("pageerror", (e) => konsole.push("pageerror: " + e.message));
|
||||
|
||||
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
||||
await seite.click(`.rolle[data-rolle="${rolle}"]`);
|
||||
await seite.fill("#code", code);
|
||||
await seite.click("button[type=submit]");
|
||||
await seite.waitForURL("**/start.html", { timeout: 15000 });
|
||||
|
||||
await seite.goto(BASIS + "/workspace/uebersicht.html", { waitUntil: "networkidle" });
|
||||
await seite.waitForTimeout(500);
|
||||
|
||||
const bild = await seite.evaluate(() => ({
|
||||
karten: document.querySelectorAll(".creator").length,
|
||||
summen: document.querySelectorAll(".summe").length,
|
||||
titel: document.getElementById("titel")?.textContent,
|
||||
unterzeile: document.getElementById("unterzeile")?.textContent,
|
||||
fehler: document.getElementById("fehler")?.textContent,
|
||||
/* Ueberstehender Inhalt: das ist der Fehler, den man am Rechner nie
|
||||
sieht und der auf dem Handy die Seite unbrauchbar macht. */
|
||||
ueberstand: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
||||
breit: [...document.querySelectorAll("main *")]
|
||||
.filter((e) => e.getBoundingClientRect().right > window.innerWidth + 1)
|
||||
.map((e) => e.className).slice(0, 5),
|
||||
/* Der Balken muss eine echte Breite haben, sonst ist der
|
||||
Start-Check nur eine leere Rinne. */
|
||||
balken: [...document.querySelectorAll(".fortschritt__fuellung")]
|
||||
.map((e) => e.style.width),
|
||||
zahlen: [...document.querySelectorAll(".creator")].map((k) => ({
|
||||
name: k.querySelector(".creator__name")?.textContent,
|
||||
achtung: k.dataset.achtung === "ja",
|
||||
werte: [...k.querySelectorAll(".wert__zahl")].map((z) => z.textContent).join("/"),
|
||||
naechstes: k.querySelector(".naechstes__wert")?.textContent,
|
||||
unter: k.querySelector(".creator__unter")?.textContent,
|
||||
})),
|
||||
}));
|
||||
|
||||
await seite.screenshot({ path: datei, fullPage: true });
|
||||
console.log(`\n${name}`);
|
||||
ok(konsole.length === 0, `keine Fehler in der Konsole${konsole.length ? " -> " + konsole.join(" | ") : ""}`);
|
||||
ok(bild.ueberstand <= 0, `nichts steht über (${bild.ueberstand}px, ${bild.breit.join(", ")})`);
|
||||
ok(!bild.fehler, `keine Fehlermeldung auf der Seite ("${bild.fehler}")`);
|
||||
console.log(" ->", JSON.stringify(bild.zahlen, null, 1).replace(/\n\s*/g, " "));
|
||||
console.log(" -> Titel:", bild.titel, "| Summen:", bild.summen, "| Balken:", bild.balken.join(" "));
|
||||
await kontext.close();
|
||||
return bild;
|
||||
}
|
||||
|
||||
const gross = await ansehen("DogFather · Computer", "admin", "CODE-DOGI-0001",
|
||||
{ width: 1440, height: 1000 }, "pruef-uebersicht-computer.png");
|
||||
ok(gross.karten === 4, `vier Karten (${gross.karten})`);
|
||||
ok(gross.summen === 6, `sechs Summen (${gross.summen})`);
|
||||
/* Sortierung: Anna hat 3 ueberfaellige + 1 dringend und muss oben stehen. */
|
||||
ok(gross.zahlen[0]?.name === "Anna Sturm", `die brennendste Karte steht oben (${gross.zahlen[0]?.name})`);
|
||||
ok(gross.zahlen[0]?.achtung === true, "sie ist als Achtung markiert");
|
||||
ok(gross.zahlen.at(-1)?.achtung === false, "eine ruhige Karte ist nicht markiert");
|
||||
ok(gross.zahlen.some((z) => /nicht da|nie angemeldet/.test(z.unter || "")),
|
||||
"lange Abwesenheit wird benannt");
|
||||
|
||||
const klein = await ansehen("DogFather · Handy", "admin", "CODE-DOGI-0001",
|
||||
{ width: 390, height: 844 }, "pruef-uebersicht-handy.png");
|
||||
ok(klein.karten === 4, `vier Karten auch auf dem Handy (${klein.karten})`);
|
||||
|
||||
const eigen = await ansehen("Creator · Handy", "creator", "CODE-CRE-0001",
|
||||
{ width: 390, height: 844 }, "pruef-uebersicht-creator.png");
|
||||
ok(eigen.karten === 1, `Creator sieht genau seine Karte (${eigen.karten})`);
|
||||
ok(eigen.titel === "Mein Stand", `Titel heisst "Mein Stand" (${eigen.titel})`);
|
||||
ok(eigen.summen === 0, "keine Summenzeile bei nur einer Karte");
|
||||
ok(!/betreut von/.test(eigen.zahlen[0]?.unter || ""),
|
||||
"dem Creator wird die Betreuung nicht vorgehalten");
|
||||
|
||||
await browser.close();
|
||||
console.log(`\n${fehler === 0 ? "Alles in Ordnung." : fehler + " Prüfung(en) fehlgeschlagen."}`);
|
||||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
||||
process.exit(fehler === 0 ? 0 : 1);
|
||||
@@ -0,0 +1,245 @@
|
||||
/* ===================================================================
|
||||
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);
|
||||
@@ -0,0 +1,103 @@
|
||||
/* Kurzpruefung: Steht die Kopfleiste auf dem Handy ueber -- und zwar
|
||||
auf ALLEN Seiten oder nur auf der neuen? Beantwortet die Frage, ob
|
||||
ich einen eigenen Fehler gebaut oder einen alten gefunden habe. */
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const ordner = mkdtempSync(join(tmpdir(), "ws-kopf-"));
|
||||
process.env.WORKSPACE_DB = join(ordner, "pruef.sqlite");
|
||||
process.env.PORT = "4178";
|
||||
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 (n, w, o) {
|
||||
return echteCookie.call(this, n, w, { ...(o || {}), secure: false });
|
||||
};
|
||||
await import("./index.js");
|
||||
await new Promise((r) => setTimeout(r, 700));
|
||||
const BASIS = "http://127.0.0.1:4178";
|
||||
setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 150_000).unref?.();
|
||||
|
||||
await fetch(BASIS + "/workspace/api/anmelden", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ rolle: "admin", code: "x" }),
|
||||
}).catch(() => {});
|
||||
|
||||
const { DatabaseSync } = await import("node:sqlite");
|
||||
const { scryptSync, randomBytes } = await import("node:crypto");
|
||||
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
const salt = randomBytes(16).toString("hex");
|
||||
const hash = scryptSync("CODE-DOGI-0001", 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, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
|
||||
.run("Test-DogFather", "admin", hash, salt, 32768, new Date().toISOString());
|
||||
d.close();
|
||||
|
||||
const { chromium } = await import(
|
||||
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
||||
const browser = await chromium.launch();
|
||||
|
||||
/* Alle Seiten, die eine angemeldete Person zu sehen bekommt. Geprueft
|
||||
wird auf beiden Groessen, weil die geteilten Klassen (.kopf-zeile,
|
||||
.inhalt--breit, .leise) am 31.08.2026 von aufgaben.css nach start.css
|
||||
gewandert sind -- faellt dabei etwas herunter, sieht man es hier. */
|
||||
const SEITEN = ["start.html", "uebersicht.html", "aufgaben.html", "kalender.html",
|
||||
"personen.html", "profil.html", "wissen.html", "dateien.html",
|
||||
"calls.html", "startcheck.html", "report.html", "scouting.html",
|
||||
"automation.html", "bereich.html?b=live"];
|
||||
|
||||
let fehler = 0;
|
||||
for (const [breite, hoehe, wie] of [[1440, 1000, "Computer"], [390, 844, "Handy"]]) {
|
||||
console.log(`\n=== ${wie} (${breite}px)`);
|
||||
const kontext = await browser.newContext({ viewport: { width: breite, height: hoehe } });
|
||||
const seite = await kontext.newPage();
|
||||
const konsole = [];
|
||||
seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); });
|
||||
seite.on("pageerror", (e) => konsole.push("pageerror: " + e.message));
|
||||
|
||||
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
||||
await seite.click('.rolle[data-rolle="admin"]');
|
||||
await seite.fill("#code", "CODE-DOGI-0001");
|
||||
await seite.click("button[type=submit]");
|
||||
await seite.waitForURL("**/start.html", { timeout: 15000 });
|
||||
|
||||
for (const s of SEITEN) {
|
||||
konsole.length = 0;
|
||||
await seite.goto(BASIS + "/workspace/" + s, { waitUntil: "networkidle" });
|
||||
await seite.waitForTimeout(300);
|
||||
const r = await seite.evaluate(() => ({
|
||||
ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
||||
schuld: [...document.querySelectorAll("body *")]
|
||||
.filter((e) => e.getBoundingClientRect().right > window.innerWidth + 1)
|
||||
.map((e) => (e.tagName + "." + e.className).slice(0, 40)).slice(0, 4),
|
||||
/* Die verschobenen Klassen muessen weiterhin greifen. .inhalt--breit
|
||||
setzt 1240px -- kommt nur 880px an, ist die Regel verloren. */
|
||||
breit: document.querySelector(".inhalt--breit")
|
||||
? getComputedStyle(document.querySelector(".inhalt--breit")).maxWidth : null,
|
||||
kopfzeile: document.querySelector(".kopf-zeile")
|
||||
? getComputedStyle(document.querySelector(".kopf-zeile")).display : null,
|
||||
abmelden: (() => {
|
||||
const a = document.querySelector(".abmelden")?.getBoundingClientRect();
|
||||
return a ? a.right <= window.innerWidth && a.left >= 0 : null;
|
||||
})(),
|
||||
}));
|
||||
const gut = r.ueber <= 0 && konsole.length === 0 && r.abmelden !== false
|
||||
&& (r.breit === null || r.breit === "1240px")
|
||||
&& (r.kopfzeile === null || r.kopfzeile === "flex");
|
||||
if (!gut) fehler++;
|
||||
console.log(` ${gut ? "ok " : "FEHL"} ${String(s).padEnd(20)}`
|
||||
+ `über=${String(r.ueber).padStart(3)}px abmelden=${r.abmelden}`
|
||||
+ ` breit=${r.breit} kopfzeile=${r.kopfzeile}`
|
||||
+ (konsole.length ? " KONSOLE: " + konsole.join(" | ") : "")
|
||||
+ (r.schuld.length ? " -> " + r.schuld.join(" | ") : ""));
|
||||
}
|
||||
await kontext.close();
|
||||
}
|
||||
await browser.close();
|
||||
console.log(`\n${fehler === 0 ? "Alles in Ordnung." : fehler + " Seite(n) fehlerhaft."}`);
|
||||
try { rmSync(ordner, { recursive: true, force: true }); } catch {}
|
||||
process.exit(fehler === 0 ? 0 : 1);
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
import express from "express";
|
||||
import {
|
||||
db, protokolliere, echteIp, sitzungLesen, betreutWo, istLeitung, ROLLEN_SORTIERUNG,
|
||||
db, protokolliere, echteIp, sitzungLesen, betreutWo, istLeitung, ROLLEN_SORTIERUNG, betreuteIds,
|
||||
} from "./workspace.js";
|
||||
|
||||
export const aufgabenRouter = express.Router();
|
||||
@@ -188,6 +188,84 @@ function pruefeFelder(körper, { neu }) {
|
||||
return { aus, fehler };
|
||||
}
|
||||
|
||||
/* ---------- Gesamtuebersicht ----------------------------------------------
|
||||
"Die Admin-Rolle behaelt den Gesamtueberblick" (Konzept, Seite 2) und
|
||||
"UEBERSICHT -- alles zentral" (Seite 1). Genau das fehlte: eine
|
||||
Ansicht, die JE CREATOR zeigt, wie es steht, statt nur die eigenen
|
||||
Zahlen.
|
||||
|
||||
Fuehrt keine eigenen Daten -- alles kommt aus Aufgaben, Terminen,
|
||||
Bereichen, Profil und Start-Check. Wer welchen Creator sieht, richtet
|
||||
sich nach derselben Betreuungsregel wie ueberall. */
|
||||
|
||||
aufgabenRouter.get("/workspace/api/uebersicht/creator", (req, res) => {
|
||||
try {
|
||||
const person = req.person;
|
||||
let wo = "p.rolle = 'creator' AND p.aktiv = 1";
|
||||
let werte = [];
|
||||
if (person.rolle === "creator") {
|
||||
wo += " AND p.id = ?";
|
||||
werte = [person.id];
|
||||
} else if (person.rolle === "scout") {
|
||||
const ids = betreuteIds(person);
|
||||
if (!ids.length) return res.json({ creator: [], eigen: false });
|
||||
wo += ` AND p.id IN (${ids.map(() => "?").join(",")})`;
|
||||
werte = ids;
|
||||
}
|
||||
|
||||
const heute = new Date().toISOString().slice(0, 10);
|
||||
const jetztIso = new Date().toISOString();
|
||||
|
||||
const liste = db().prepare(`
|
||||
SELECT p.id, p.name, p.letzter_login,
|
||||
(SELECT name FROM personen b
|
||||
WHERE b.id = (SELECT betreuer_id FROM betreuung WHERE creator_id = p.id)) AS betreuer,
|
||||
(SELECT COUNT(*) FROM aufgaben a
|
||||
WHERE a.creator_id = p.id AND a.status = 'offen') AS offen,
|
||||
(SELECT COUNT(*) FROM aufgaben a
|
||||
WHERE a.creator_id = p.id AND a.status = 'arbeit') AS arbeit,
|
||||
(SELECT COUNT(*) FROM aufgaben a
|
||||
WHERE a.creator_id = p.id AND a.status = 'review') AS review,
|
||||
(SELECT COUNT(*) FROM aufgaben a
|
||||
WHERE a.creator_id = p.id AND a.status <> 'erledigt'
|
||||
AND a.frist IS NOT NULL AND a.frist < ?) AS ueberfaellig,
|
||||
(SELECT COUNT(*) FROM aufgaben a
|
||||
WHERE a.creator_id = p.id AND a.erledigt_am IS NOT NULL
|
||||
AND a.erledigt_am >= ?) AS erledigt30,
|
||||
(SELECT COUNT(*) FROM eintraege e
|
||||
WHERE e.creator_id = p.id AND e.status = 'offen'
|
||||
AND e.dringlichkeit = 'hoch') AS dringend,
|
||||
(SELECT MIN(t.beginn) FROM termine t
|
||||
WHERE (t.creator_id = p.id OR t.teilnehmer_id = p.id)
|
||||
AND t.beginn >= ? AND t.erledigt = 0) AS naechster_termin,
|
||||
(SELECT COUNT(*) FROM startcheck s
|
||||
WHERE s.creator_id = p.id AND s.bewertung IS NOT NULL) AS check_geprueft,
|
||||
(SELECT COUNT(*) FROM startcheck s
|
||||
WHERE s.creator_id = p.id AND s.bewertung = 'handlung') AS check_handlung,
|
||||
(SELECT naechster_review FROM profile f WHERE f.person_id = p.id) AS naechster_review,
|
||||
(SELECT COUNT(*) FROM profile f WHERE f.person_id = p.id) AS hat_profil
|
||||
FROM personen p
|
||||
WHERE ${wo}
|
||||
ORDER BY p.name`).all(
|
||||
heute, new Date(Date.now() - 30 * 86400_000).toISOString(),
|
||||
heute + "T00:00", ...werte);
|
||||
|
||||
/* Der Review-Termin ist Steuerungswissen -- ein Creator sieht ihn in
|
||||
seinem Profil auch nicht, also hier ebenso wenig. */
|
||||
if (!istLeitung(person)) for (const c of liste) delete c.naechster_review;
|
||||
|
||||
res.json({
|
||||
creator: liste,
|
||||
eigen: person.rolle === "creator",
|
||||
check_gesamt: 16,
|
||||
stand: jetztIso,
|
||||
});
|
||||
} catch (fehler) {
|
||||
console.error("[workspace] Gesamtuebersicht:", fehler?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
/* ---------- Rueckmeldungen ("Aufgaben & Feedback") ------------------------
|
||||
Wer die Aufgabe sieht, darf mitreden. Geprueft wird das ueber dieselbe
|
||||
Sichtbarkeitsregel wie fuer die Aufgabe selbst -- eine zweite Regel
|
||||
|
||||
@@ -636,6 +636,7 @@ export const workspaceRouter = express.Router();
|
||||
das Skript ihn wegschickt. Sichtbar sein soll sie gar nicht. */
|
||||
const GESCHUETZT = {
|
||||
"/workspace/start.html": null,
|
||||
"/workspace/uebersicht.html": null,
|
||||
"/workspace/aufgaben.html": null,
|
||||
"/workspace/personen.html": ["admin", "manager"],
|
||||
"/workspace/profil.html": ["admin", "manager", "scout", "creator"],
|
||||
|
||||
Reference in New Issue
Block a user