diff --git a/server/pruef-community-sicht.mjs b/server/pruef-community-sicht.mjs new file mode 100644 index 00000000..cc1b1422 --- /dev/null +++ b/server/pruef-community-sicht.mjs @@ -0,0 +1,268 @@ +/* ===================================================================== + WAS EIN MITGLIED DER COMMUNITY SIEHT -- SYSTEMATISCH (18.09.2026) + + Filipe: „perfektioniere jede einzelne kategorie der community." + + --------------------------------------------------------------------- + WARUM ES DIESE PRÜFUNG GIBT + + An einem Tag habe ich in der Community elf Dinge gefunden, die dort + nicht hingehörten: Aufgabenzähler, „gönn dir was", „0 betreut", + sieben Links auf eine Seite, die ein Mitglied gar nicht öffnen darf. + Jeder einzelne Fund kam daher, dass ich zufällig hingesehen habe. + + Das ist keine Methode. Der Workspace ist für die Agentur gebaut + worden, und die Community hat ihn geerbt -- überall dort, wo + niemand hingesehen hat, steht noch die Sprache und die Logik eines + Arbeitsplatzes. Solche Reste findet man nicht durch Nachdenken, + sondern indem man ALLE Seiten durchgeht, die ein Mitglied erreichen + kann. + + --------------------------------------------------------------------- + ZWEI FRAGEN, BEIDE MESSBAR + + 1. TOTE WEGE: Führt irgendein Link, Knopf oder Formular auf eine + Seite, die diese Person nicht öffnen darf? Sie landet dann wieder + auf der Startseite -- ohne Meldung, ohne Grund. Man drückt, es + passiert nichts, und beim nächsten Knopf zweifelt man an sich. + + 2. FREMDE SPRACHE: Stehen dort Wörter, die es hier nicht gibt -- + „Review", „betreut", „Pipeline", „Creator anlegen"? Sie sind + nicht falsch, sie gehören nur jemand anderem. + + Aufrufen mit: node server/pruef-community-sicht.mjs + ===================================================================== */ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { request as httpAnfrage } from "node:http"; +import { portMussFreiSein } from "./helfer-port.mjs"; + +const PORT = await portMussFreiSein(4198, "pruef-community-sicht"); +const ordner = mkdtempSync(join(tmpdir(), "ws-cs-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = String(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"; +await import("./index.js"); +notbremse(180_000, "pruef-community-sicht"); +await new Promise((r) => setTimeout(r, 700)); + +process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); }); + +let fehler = 0, geprueft = 0; +const melde = (t) => console.log(t); +const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +const CREW = "crew.dogfather-universe.com"; + +/* --------------------------------------------------------------------- + WELCHE SEITEN DARF EIN MITGLIED? -- aus der Rechtetabelle abgeleitet. + + NICHT VON HAND AUFGEZÄHLT. Eine Liste hier müsste bei jeder neuen + Seite nachgezogen werden, und genau das vergisst man. Die Tabelle + in rechte.js ist ohnehin die Wahrheit -- also wird sie gefragt. + --------------------------------------------------------------------- */ +const { darfSeite } = await import("./rechte.js"); +const { readFileSync, readdirSync } = await import("node:fs"); +const alleSeiten = readdirSync(join(import.meta.dirname, "..", "workspace")) + .filter((d) => d.endsWith(".html")); + +const GAST = { rolle: "gast", haus: "crew" }; +const erlaubt = alleSeiten.filter((d) => darfSeite(GAST, `/workspace/${d}`)); +const verboten = alleSeiten.filter((d) => !darfSeite(GAST, `/workspace/${d}`)); + +melde(""); +melde("=== 1. Was ueberhaupt offensteht ==="); +ok(erlaubt.length >= 3 && erlaubt.length <= 8, + `ein Mitglied darf ${erlaubt.length} Seiten: ${erlaubt.join(", ")}`); +ok(verboten.length >= 15, + `und ${verboten.length} nicht -- so viele Wege gibt es, ins Leere zu zeigen`); + +/* --------------------------------------------------------------------- + DIE MESSUNG IM BROWSER + --------------------------------------------------------------------- */ +function roh(pfad, host, kopf = {}, koerper = null) { + return new Promise((f, s) => { + const a = httpAnfrage({ + host: "127.0.0.1", port: PORT, path: pfad, + method: koerper ? "POST" : "GET", + headers: { Host: host, ...(koerper ? { "Content-Type": "application/json" } : {}), ...kopf }, + }, (an) => { + let t = ""; + an.on("data", (c) => { t += c; }); + an.on("end", () => f({ code: an.statusCode, text: t, kopf: an.headers })); + }); + a.on("error", s); + if (koerper) a.write(JSON.stringify(koerper)); + a.end(); + }); +} + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes } = await import("node:crypto"); +{ + const d = new DatabaseSync(process.env.WORKSPACE_DB); + for (const [name, rolle, code] of [ + ["Filipe", "admin", "CODE-DOGI-0001"], + ["Mara", "gast", "CODE-GAST-0001"], + ]) { + const salz = randomBytes(16).toString("hex"); + const hash = scryptSync(code, salz, 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(name, rolle, hash, salz, 32768, new Date().toISOString()); + } + d.close(); +} + +async function anmelden(rolle, code) { + let a = await roh("/workspace/api/anmelden", CREW, {}, { rolle, code }); + if (a.code === 400 && a.text.includes("alter_offen")) { + a = await roh("/workspace/api/anmelden", CREW, {}, { rolle, code, alter_ok: true }); + } + return [].concat(a.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; "); +} +const kGast = await anmelden("gast", "CODE-GAST-0001"); +const kDogi = await anmelden("admin", "CODE-DOGI-0001"); + +/* Damit die Bretter nicht leer sind -- auf einer leeren Seite findet + man keine Wege, die ins Leere fuehren. */ +for (const b of ["treff", "anschlag", "ansteht", "wunsch", "highlight", "regeln", "mitmachen"]) { + await roh(`/workspace/api/bereich/${b}/start`, CREW, + { cookie: kDogi, origin: `http://${CREW}:${PORT}` }, {}); +} + +let pw = null; +try { + pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); +} catch { /* dritter Ausgang unten */ } + +if (!pw) { + melde(""); + melde("KANN NICHT NACHSEHEN: Playwright fehlt -- die Seiten wurden nicht geoeffnet."); + melde(`${geprueft} Pruefungen, ${fehler} Fehler`); + process.exit(3); +} + +/* WÖRTER, DIE JEMAND ANDEREM GEHÖREN. + + Keine Rollennamen -- die stehen bewusst nirgends in ausgelieferten + Dateien. Gesucht wird nach der SPRACHE eines Arbeitsplatzes. */ +const FREMDE_WOERTER = [ + "Vorarbeiten", "gönn dir was", "betreut", "Review", + "Pipeline", "überfällig", "In Arbeit", "Abgebrochen", +]; + +const browser = await pw.chromium.launch(); +try { + const ctx = await browser.newContext({ viewport: { width: 1400, height: 1000 } }); + await ctx.addCookies(kGast.split("; ").filter(Boolean).map((z) => { + const [name, ...rest] = z.split("="); + return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" }; + })); + const seite = await ctx.newPage(); + const skriptfehler = []; + seite.on("pageerror", (e) => skriptfehler.push(String(e).slice(0, 100))); + + /* Jede erlaubte Seite -- und bei bereich.html jedes der sieben + Bretter, denn sie sehen völlig verschieden aus. */ + const ziele = []; + for (const d of erlaubt) { + if (d === "bereich.html") { + for (const b of ["treff", "anschlag", "ansteht", "wunsch", "highlight", "regeln", "mitmachen"]) { + ziele.push(`${d}?b=${b}`); + } + } else ziele.push(d); + } + + melde(""); + melde("=== 2. Wege, die ins Leere fuehren ==="); + let toteWege = 0, geprueftLinks = 0; + const gefunden = []; + for (const ziel of ziele) { + await seite.goto(`http://127.0.0.1:${PORT}/workspace/${ziel}`, { waitUntil: "networkidle" }); + await seite.waitForTimeout(900); + const wege = await seite.evaluate(() => + [...document.querySelectorAll("a[href]")] + .filter((a) => a.offsetParent !== null) + .map((a) => ({ ziel: a.getAttribute("href"), text: a.textContent.trim().slice(0, 30) }))); + for (const w of wege) { + const datei = String(w.ziel).split("?")[0].split("#")[0].replace(/^\.?\//, ""); + if (!datei.endsWith(".html")) continue; + geprueftLinks++; + if (verboten.includes(datei)) { + toteWege++; + gefunden.push(`${ziel}: „${w.text}" -> ${datei}`); + } + } + } + /* DIE ANZAHL GEHÖRT IN DIE BEDINGUNG. Ohne sie wäre „0 tote Wege" + auch dann wahr, wenn gar keine Seite geladen hätte. */ + ok(geprueftLinks >= 20 && toteWege === 0, + `${geprueftLinks} sichtbare Wege auf ${ziele.length} Seiten, davon ${toteWege} ins Leere`); + for (const g of gefunden.slice(0, 10)) console.log(` -> ${g}`); + + melde(""); + melde("=== 3. Sprache, die jemand anderem gehoert ==="); + let fremd = 0; + const wo = []; + for (const ziel of ziele) { + await seite.goto(`http://127.0.0.1:${PORT}/workspace/${ziel}`, { waitUntil: "networkidle" }); + await seite.waitForTimeout(700); + const text = await seite.evaluate(() => document.body.innerText); + for (const w of FREMDE_WOERTER) { + if (text.includes(w)) { fremd++; wo.push(`${ziel}: „${w}"`); } + } + } + ok(fremd === 0, `keine fremde Arbeitssprache auf ${ziele.length} Seiten (${fremd} Stellen)`); + for (const g of wo.slice(0, 10)) console.log(` -> ${g}`); + + /* ===================================================================== + 4. DIE GEGENPROBE: BEHAELT DAS TEAM SEINE WEGE? + + Was hier weggenommen wird, weil es fuer ein Mitglied ins Leere + fuehrt, muss fuer die anderen stehen bleiben. Ohne diesen + Abschnitt saehe eine abgeschaffte Funktion genauso aus wie eine, + die richtig entscheidet -- und zwar in beiden Faellen gruen. + ===================================================================== */ + melde(""); + melde("=== 4. Und behaelt das Team seine Wege? ==="); + { + const ctx2 = await browser.newContext({ viewport: { width: 1400, height: 1000 } }); + await ctx2.addCookies(kDogi.split("; ").filter(Boolean).map((z) => { + const [name, ...rest] = z.split("="); + return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" }; + })); + const seite2 = await ctx2.newPage(); + await seite2.goto(`http://127.0.0.1:${PORT}/workspace/start.html`, { waitUntil: "networkidle" }); + await seite2.waitForTimeout(1200); + const z = await seite2.evaluate(() => ({ + chat: !!document.querySelector(".chat-knopf"), + wege: [...document.querySelectorAll("a[href$='.html']")].length, + })); + ok(z.chat, "DogFather hat seinen Chat-Knopf weiterhin"); + ok(z.wege >= 5, `und weiterhin Wege auf andere Seiten (${z.wege})`); + await ctx2.close(); + } + + melde(""); + melde("=== 5. Und laeuft ueberhaupt alles? ==="); + ok(skriptfehler.length === 0, + `kein Skriptfehler auf ${ziele.length} Seiten` + + (skriptfehler.length ? ` -- ${skriptfehler[0]}` : "")); +} finally { + await browser.close(); +} + +melde(""); +melde(`${geprueft} Pruefungen, ${fehler} Fehler`); +melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG"); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Rest */ } +process.exit(fehler ? 1 : 0); diff --git a/server/workspace.js b/server/workspace.js index 5ef70edb..62fcedba 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -4786,6 +4786,20 @@ workspaceRouter.get("/workspace/api/ich", (req, res) => { `ich.rolle === 'admin'`, und dieselbe Zeile schaltete auch das Loeschen frei; die beiden gehoeren nicht zusammen. */ darf_rollen_wechseln: darfRollenWechseln(person), + /* OB DER CHAT-KNOPF IN DER KOPFLEISTE ERSCHEINT (18.09.2026). + + Er wurde auf JEDER Seite gebaut. Fuer ein Mitglied der Community + fuehrte er ins Leere: `chat.html` steht ihm nicht offen, der + Klick landete wieder auf der Startseite. Gemessen von + pruef-community-sicht: zehn Seiten, zehn tote Wege -- und der + Knopf steht oben rechts, wo man ihn am ehesten drueckt. + + DIE ANTWORT KOMMT AUS DERSELBEN TABELLE wie die Schranke selbst. + Die Oberflaeche vergleicht keine Rollennamen -- das waere eine + zweite Wahrheit, und bei der naechsten Rechteaenderung liefe sie + auseinander. Genau die Ueberlegung steht schon zwei Zeilen + darueber; hier gilt sie noch einmal. */ + darf_chat: darfSeite(person, "/workspace/chat.html"), /* DIE KACHELN, WENN SIE NICHT IM BROWSER STEHEN DUERFEN. Fuer die fuenf bekannten Rollen steht hier `null`, und die diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 6c66103f..28e2eb36 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -22,25 +22,25 @@ }, "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609182128", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609182158", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609182128", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609182158", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609182128", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609182158", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609182128", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609182158", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/js/kopf.js b/workspace/assets/js/kopf.js index c7fc4fd2..36e552e2 100644 --- a/workspace/assets/js/kopf.js +++ b/workspace/assets/js/kopf.js @@ -20,6 +20,28 @@ Steht hier und nicht in jeder Seitendatei -- jede von ihnen setzte bisher dieselbe Zeile selbst zusammen, vierzehnmal. */ window.werZeigen = function (ich) { + /* DER CHAT-KNOPF GEHOERT NICHT ZU JEDEM (18.09.2026). + + Er wurde auf JEDER Seite gebaut. Fuer ein Mitglied der Community + fuehrte er ins Leere: `chat.html` steht ihm nicht offen, der + Klick landete wieder auf der Startseite -- keine Meldung, kein + Grund, nichts passiert. Gemessen von pruef-community-sicht: + zehn Seiten, zehn tote Wege, und der Knopf steht oben rechts, + wo man ihn am ehesten drueckt. + + HIER UND NICHT IN `aufbauChat`: Dort gibt es die Person nicht -- + sie kommt als Parameter in diese Funktion, und zwar je nach + Seite frueher oder spaeter als der Knopf gebaut wird. Auf die + Reihenfolge zu bauen waere eine Annahme; ein vorhandener Knopf + laesst sich dagegen immer entfernen. + + `=== false` und nicht `!`: Fehlt die Angabe (aeltere Antwort des + Servers), bleibt alles wie bisher. Ein fehlendes Feld soll + niemandem den Chat wegnehmen. */ + if (ich && ich.darf_chat === false) { + document.querySelector('.chat-knopf')?.remove(); + } + const ziel = document.getElementById('wer'); if (!ziel || !ich) return; ziel.textContent = ''; @@ -800,6 +822,7 @@ /* Auf der Chatseite selbst gibt es nichts zu verlinken. */ if (/\/chat\.html$/.test(location.pathname)) return; + const el = (tag, klasse, text) => { const k = document.createElement(tag); if (klasse) k.className = klasse; diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 7b4ec5b4..aba98357 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -12,19 +12,19 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + - +
@@ -291,10 +291,10 @@ - - - - - + + + + +