diff --git a/server/pruef-chat-kanaele.mjs b/server/pruef-chat-kanaele.mjs index 5ce6e265..910448c0 100644 --- a/server/pruef-chat-kanaele.mjs +++ b/server/pruef-chat-kanaele.mjs @@ -47,9 +47,9 @@ const ordner = mkdtempSync(join(tmpdir(), "ws-kanal-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); const { portMussFreiSein } = await import("./helfer-port.mjs"); -await portMussFreiSein(4359, "die Kanalpruefung"); +await portMussFreiSein(4377, "die Kanalpruefung"); -process.env.PORT = "4359"; +process.env.PORT = "4377"; process.env.SITE_ACCESS_SECRET = "lokaler-test"; process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; @@ -90,7 +90,7 @@ 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, 1000)); -const BASIS = "http://127.0.0.1:4359"; +const BASIS = "http://127.0.0.1:4377"; /* ---- Notbremse und eigener Zuhoerer ------------------------------------- @@ -185,7 +185,7 @@ function anmelden(rolle, code, wand = "crew.dogfather-universe.com") { return new Promise((fertig) => { const rumpf = JSON.stringify({ rolle, code }); const a = httpAnfrage({ - host: "127.0.0.1", port: 4359, path: "/workspace/api/anmelden", method: "POST", + host: "127.0.0.1", port: 4377, path: "/workspace/api/anmelden", method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(rumpf), diff --git a/server/pruef-haus-trennung.mjs b/server/pruef-haus-trennung.mjs new file mode 100644 index 00000000..f0b0a4e6 --- /dev/null +++ b/server/pruef-haus-trennung.mjs @@ -0,0 +1,358 @@ +/* ZWEI HÄUSER, EINE DATENBANK (10.09.2026) + + Filipe: "die daten von dieser seite sollen nichts mit den daten am + hut haben von der workspace seite bitte, die hier soll ihre eigene + daten haben und komplett von der anderen getrennt sein. dogfather + soll die daten auch auf der anderen seite sehen in der team dogi + kategorie aber auch nur er und vanvan." + + --------------------------------------------------------------------- + WAS HIER GEMESSEN WIRD -- UND WARUM ES ZWEI RICHTUNGEN BRAUCHT + + Die Trennung hängt an EINEM Wert: `person.haus`, gesetzt in + sitzungLesen() aus dem Hostnamen. Von dort reist er durch jede + Sichtbarkeitsregel im Haus. Ein Wert, der so weit trägt, hat zwei + Arten, falsch zu sein: + + ER GREIFT NICHT -- dann steht die Agentur weiterhin auf der + Team-Seite, und niemand merkt es, weil alles funktioniert. + + ER GREIFT ZU WEIT -- dann verschwindet auf der AGENTURSEITE etwas, + das dort hingehört. Das ist der gefährlichere Fall: Eine Liste, die + zu kurz ist, sieht aus wie "nichts zu tun". + + Deshalb steht hinter JEDER Messung auf crew. dieselbe Messung auf + workspace., mit DERSELBEN Sitzung. Der einzige Unterschied zwischen + den beiden Aufrufen ist der Host-Kopf -- alles andere ist gleich. + + `fetch` kann diesen Kopf nicht setzen (undici setzt ihn aus der + Adresse; nachgemessen, nicht vermutet). Deshalb node:http. + ===================================================================== */ + +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { request } from "node:http"; + +const ordner = mkdtempSync(join(tmpdir(), "ws-haus-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); + +const { portMussFreiSein } = await import("./helfer-port.mjs"); +await portMussFreiSein(4381, "die Haustrennung"); + +process.env.PORT = "4381"; +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 }); }; +await import("./index.js"); +await new Promise((r) => setTimeout(r, 1000)); + +for (const art of ["uncaughtException", "unhandledRejection"]) { + process.on(art, (f) => { console.error(""); console.error("ABBRUCH:", f?.stack || f); process.exit(1); }); +} +setTimeout(() => { console.error(""); console.error("ABBRUCH: 150 s vorbei."); process.exit(1); }, + 150000).unref?.(); + +let fehler = 0, geprueft = 0; +const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +const CREW = "crew.dogfather-universe.com"; +const WS = "workspace.dogfather-universe.com"; + +function hole(pfad, { host, methode = "GET", rumpf = null, keks = "" } = {}) { + return new Promise((fertig, schief) => { + const kopf = { Host: host }; + if (rumpf) { kopf["Content-Type"] = "application/json"; kopf["Content-Length"] = Buffer.byteLength(rumpf); } + if (keks) kopf.Cookie = keks; + const a = request({ host: "127.0.0.1", port: 4381, path: pfad, method: methode, headers: kopf }, + (antwort) => { + let text = ""; + antwort.setEncoding("utf8"); + antwort.on("data", (t) => { text += t; }); + antwort.on("end", () => { + let daten = null; + try { daten = JSON.parse(text); } catch { /* kein JSON */ } + fertig({ status: antwort.statusCode, text, daten, + keks: (antwort.headers["set-cookie"] || []).map((z) => z.split(";")[0]).join("; ") }); + }); + }); + a.on("error", schief); + a.setTimeout(15000, () => { a.destroy(new Error("Zeitueberschreitung")); }); + if (rumpf) a.write(rumpf); + a.end(); + }); +} + +/* ---- Bestand -------------------------------------------------------- */ +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes } = await import("node:crypto"); +await hole("/workspace/api/anmelden", { host: WS, methode: "POST", + rumpf: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {}); +for (let i = 0; i < 60; i++) { + try { + const p = new DatabaseSync(process.env.WORKSPACE_DB); + try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); } + } catch { await new Promise((r) => setTimeout(r, 250)); } +} +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); +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, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)") + .run(name, rolle, hash, salt, 32768, jetzt); + return d.prepare("SELECT last_insert_rowid() AS id").get().id; +} +const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001"); +const idHand = anlegen("VanVan", "hand", "CODE-HAND-0001"); +const idModi = anlegen("Ayla", "modi", "CODE-TEAM-0001"); +const idMana = anlegen("Cigdem", "manager", "CODE-MANA-0001"); +const idScout = anlegen("Patrick", "scout", "CODE-SCOU-0001"); +const idCrea = anlegen("Luna", "creator", "CODE-CREA-0001"); +d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idCrea, idScout, jetzt); + +/* Zwei Aufgaben: eine der Agentur, eine des Teams. */ +d.prepare(`INSERT INTO aufgaben (titel, status, creator_id, verantwortlich_id, erstellt, erstellt_von) + VALUES ('Lunas Steckbrief nachtragen','offen',?,?,?,?)`).run(idCrea, idScout, jetzt, idDogi); +d.prepare(`INSERT INTO aufgaben (titel, status, verantwortlich_id, erstellt, erstellt_von) + VALUES ('Clips vom Freitag schneiden','offen',?,?,?)`).run(idModi, jetzt, idDogi); + +/* Zwei Eintraege: einer im Bereich eines Creators, einer im Ideen-Board. */ +d.prepare(`INSERT INTO eintraege (bereich, art, titel, datum, creator_id, erstellt, erstellt_von) + VALUES ('live','auswertung','Lunas Live vom Montag','2026-09-08',?,?,?)`) + .run(idCrea, jetzt, idDogi); +d.prepare(`INSERT INTO eintraege (bereich, art, titel, datum, erstellt, erstellt_von) + VALUES ('ideen','community','Wunschkonzert am Sonntag','2026-09-08',?,?)`) + .run(jetzt, idModi); + +/* Zwei Termine: einer mit einem Creator, einer mit dem Team. */ +d.prepare(`INSERT INTO termine (titel, beginn, dauer_min, creator_id, erstellt, erstellt_von) + VALUES ('Call mit Luna','2026-09-12T18:00:00.000Z',60,?,?,?)`).run(idCrea, jetzt, idDogi); +d.prepare(`INSERT INTO termine (titel, beginn, dauer_min, teilnehmer_id, erstellt, erstellt_von) + VALUES ('Teamrunde','2026-09-12T20:00:00.000Z',30,?,?,?)`).run(idModi, jetzt, idDogi); + +async function anmelden(rolle, code, wand) { + const a = await hole("/workspace/api/anmelden", { host: wand, methode: "POST", + rumpf: JSON.stringify({ rolle, code }) }); + return { ok: a.status === 200, keks: a.keks }; +} +const dogi = await anmelden("admin", "CODE-DOGI-0001", CREW); +const hand = await anmelden("hand", "CODE-HAND-0001", CREW); +const mana = await anmelden("manager", "CODE-MANA-0001", WS); +ok(dogi.ok && hand.ok && mana.ok, "angemeldet"); + +/* Beide Aufrufe mit DERSELBEN Sitzung -- nur der Host unterscheidet sie. */ +const beide = async (pfad, keks) => ({ + crew: (await hole(pfad, { host: CREW, keks })).daten, + ws: (await hole(pfad, { host: WS, keks })).daten, +}); + +/* ======================================================================= + 1. Die Kacheln + ======================================================================= */ +console.log(""); +console.log("=== Was DogFather zu sehen bekommt ==="); +{ + const a = await beide("/workspace/api/ich", dogi.keks); + const namen = (x) => [...(x?.bereiche || []), ...(x?.bereiche_zusatz || [])].map((k) => k.name); + const aufCrew = namen(a.crew); + const aufWs = namen(a.ws); + + ok(aufCrew.includes("Rückmeldung") && aufCrew.includes("Aufgaben"), + `auf crew. stehen die Kacheln des Teams (${aufCrew.length})`); + ok(!aufCrew.some((n) => ["Creator-Profile", "Zahlen", "Agentur", "Scout-Pipeline"].includes(n)), + `und keine der Agentur (${aufCrew.join(", ")})`); + + /* GEGENPROBE, und sie ist schaerfer als sie aussieht. + + Auf der Agenturadresse liefert der Server `bereiche: null` -- das + heisst "nimm die vollstaendige Liste aus assets/js/bereiche.js". + Nur auf crew. schickt er eine eigene. Der Unterschied zwischen + "null" und "einer Liste" IST die Trennung; wer hier bloss Namen + zaehlt, misst am Mechanismus vorbei. + + (Beim ersten Anlauf stand hier genau das: eine Suche nach + "Creator-Profile" in der Antwort. Sie schlug fehl -- nicht weil + die Kachel fehlt, sondern weil sie dort gar nicht ueber die + Schnittstelle kommt. Die Pruefung hatte unrecht, nicht der Code.) */ + ok(a.ws?.bereiche === null, + `auf workspace. bleibt die Kachelliste die der Datei (${a.ws?.bereiche === null ? "null" : "eigene Liste"})`); + ok(Array.isArray(a.crew?.bereiche) && a.crew.bereiche.length > 0, + `auf crew. schickt der Server eine eigene (${a.crew?.bereiche?.length})`); + ok(aufWs.includes("Rückmeldung"), + "und die Team-Kachel steht dort ebenfalls -- er sieht seine Daten auch dort"); + + /* Und keine doppelt: bereicheFuer liefert auf crew. schon die + Team-Liste, zusatzBereicheFuer muss dort schweigen. */ + ok(new Set(aufCrew).size === aufCrew.length, + `keine Kachel steht zweimal da (${aufCrew.length})`); +} + +/* ======================================================================= + 2. Aufgaben + ======================================================================= */ +console.log(""); +console.log("=== Aufgaben ==="); +{ + const a = await beide("/workspace/api/aufgaben", dogi.keks); + const t = (x) => (x?.aufgaben || []).map((z) => z.titel); + ok(t(a.crew).includes("Clips vom Freitag schneiden"), + `auf crew. steht die Aufgabe des Teams (${t(a.crew).join(" | ") || "leer"})`); + ok(!t(a.crew).includes("Lunas Steckbrief nachtragen"), + "und die der Agentur nicht"); + ok(t(a.ws).includes("Lunas Steckbrief nachtragen") && t(a.ws).includes("Clips vom Freitag schneiden"), + `auf workspace. stehen beide (${t(a.ws).length})`); +} + +/* ======================================================================= + 3. Bereiche + ======================================================================= */ +console.log(""); +console.log("=== Bereiche ==="); +{ + const live = await beide("/workspace/api/bereich/live", dogi.keks); + ok((live.crew?.eintraege || []).length === 0, + `auf crew. ist der Creator-Bereich leer (${(live.crew?.eintraege || []).length})`); + ok((live.ws?.eintraege || []).some((e) => e.titel === "Lunas Live vom Montag"), + "auf workspace. steht der Eintrag da"); + + const ideen = await beide("/workspace/api/bereich/ideen", dogi.keks); + ok((ideen.crew?.eintraege || []).some((e) => e.titel === "Wunschkonzert am Sonntag"), + "das Ideen-Board des Teams steht auf crew."); + ok((ideen.ws?.eintraege || []).some((e) => e.titel === "Wunschkonzert am Sonntag"), + "und auf workspace. ebenfalls -- er sieht es hier wie dort"); +} + +/* ======================================================================= + 4. Der Kalender + ======================================================================= */ +console.log(""); +console.log("=== Kalender ==="); +{ + const weg = "/workspace/api/termine?von=2026-09-01&bis=2026-09-30"; + const a = await beide(weg, dogi.keks); + const t = (x) => (x?.termine || []).map((z) => z.titel); + ok(t(a.crew).includes("Teamrunde"), `auf crew. steht die Teamrunde (${t(a.crew).join(" | ") || "leer"})`); + ok(!t(a.crew).includes("Call mit Luna"), "der Call mit der Creatorin nicht"); + ok(t(a.ws).includes("Call mit Luna") && t(a.ws).includes("Teamrunde"), + `auf workspace. stehen beide (${t(a.ws).length})`); +} + +/* ======================================================================= + 5. Mit wem man schreiben kann + ======================================================================= */ +console.log(""); +console.log("=== Die Personenliste ==="); +{ + const a = await beide("/workspace/api/chat/partner", dogi.keks); + const n = (x) => (x?.personen || []).map((p) => p.name); + ok(n(a.crew).includes("Ayla") && n(a.crew).includes("VanVan"), + `auf crew. steht das Team (${n(a.crew).join(", ") || "leer"})`); + ok(!n(a.crew).some((x) => ["Luna", "Cigdem", "Patrick"].includes(x)), + "und niemand aus der Agentur"); + ok(["Luna", "Cigdem", "Patrick", "Ayla"].every((x) => n(a.ws).includes(x)), + `auf workspace. stehen alle (${n(a.ws).length})`); +} + +/* ======================================================================= + 6. Die rechte Hand -- dieselbe Trennung + ======================================================================= */ +console.log(""); +console.log("=== VanVan ==="); +{ + const a = await beide("/workspace/api/aufgaben", hand.keks); + const t = (x) => (x?.aufgaben || []).map((z) => z.titel); + ok(t(a.crew).includes("Clips vom Freitag schneiden"), "sie sieht die Team-Aufgabe auf crew."); + ok(!t(a.crew).includes("Lunas Steckbrief nachtragen"), "die der Agentur nicht"); + /* Auf der Agenturadresse kommt sie gar nicht erst herein -- das ist + die Regel vom 10.09., und sie muss weiter gelten. */ + const ws = await hole("/workspace/api/aufgaben", { host: WS, keks: hand.keks }); + ok(ws.status === 401, `und auf workspace. ist sie niemand (${ws.status})`); +} + +/* ======================================================================= + 7. Die Agentur bleibt unberuehrt + ======================================================================= */ +console.log(""); +console.log("=== Und der Manager? ==="); +{ + const a = await hole("/workspace/api/aufgaben", { host: WS, keks: mana.keks }); + const t = (a.daten?.aufgaben || []).map((z) => z.titel); + ok(a.status === 200, `er kommt herein (${a.status})`); + /* Er sieht die Aufgabe seines Creators nicht -- Luna gehoert Patrick, + nicht ihm. Das ist die alte Regel, und sie darf sich nicht + geaendert haben. Wichtig ist hier nur: Die Team-Aufgabe sieht er + ganz sicher nicht. */ + ok(!t.includes("Clips vom Freitag schneiden"), + `und sieht die Aufgabe des Teams nicht (${t.join(" | ") || "leer"})`); +} + +/* ======================================================================= + 8. Die Zahlen aus dem Bildschirmfoto + ======================================================================= + + Genau die beiden Stellen, auf die Filipe gezeigt hat: der Ring der + Zentrale ("9 IM TEAM", obwohl das Team drei Leute hat) und die + Hinweiszeile darunter ("Creator-Profile sind noch leer"). + + Beide gehen ABSICHTLICH nicht durch die zentralen Listenfunktionen -- + im Quelltext der Zentrale steht das sogar ausdruecklich. Genau + deshalb greift die Haustrennung dort nicht von selbst, und genau + deshalb muessen sie hier einzeln stehen. + ======================================================================= */ +console.log(""); +console.log("=== Der Ring und die Hinweise ==="); +{ + const z = await beide("/workspace/api/zentrale", dogi.keks); + /* Der Ring heisst in der Antwort `segmente` -- ein Segment je Person. + Beim ersten Anlauf stand hier `team.leute`; die Pruefung meldete + "leer" auf BEIDEN Adressen und haette damit fast bestaetigt, was + sie widerlegen sollte. Deshalb steht unten eine Zeile, die + verlangt, dass ueberhaupt jemand gefunden wird. */ + const leute = (x) => (x?.segmente || []).map((p) => p.name); + const aufCrew = leute(z.crew); + const aufWs = leute(z.ws); + ok(aufCrew.length > 0, `der Ring ist auf crew. nicht leer (${aufCrew.join(", ") || "leer"})`); + ok(!aufCrew.some((n) => ["Luna", "Cigdem", "Patrick"].includes(n)), + "und es steht niemand aus der Agentur darin"); + ok(aufWs.some((n) => ["Luna", "Cigdem", "Patrick"].includes(n)), + `auf workspace. steht das ganze Haus darin (${aufWs.length})`); + + const h = await beide("/workspace/api/hinweise", dogi.keks); + const texte = (x) => (x?.hinweise || []).map((w) => w.text || w.titel || ""); + ok(!texte(h.crew).some((w) => /Creator/i.test(w)), + `keine Creator-Hinweise auf crew. (${texte(h.crew).length} Hinweise)`); + ok(texte(h.ws).some((w) => /Creator/i.test(w)), + `auf workspace. stehen sie sehr wohl (${texte(h.ws).length} Hinweise)`); +} + +/* ======================================================================= + 9. Die Gegenprobe zur Regel selbst + ======================================================================= */ +console.log(""); +console.log("=== Haengt es wirklich an der Adresse? ==="); +{ + /* Auf localhost aendert sich NICHTS -- das ist Absicht: Alle + Pruefungen im Haus laufen dort, und eine stille Einschraenkung + haette sie alle blind gemacht, ohne dass irgendwo etwas rot wird. + Genau diese Falle steht schon in crew-adresse.js beschrieben. */ + const lokal = await hole("/workspace/api/aufgaben", { host: "127.0.0.1:4381", keks: dogi.keks }); + const t = (lokal.daten?.aufgaben || []).map((z) => z.titel); + ok(t.includes("Lunas Steckbrief nachtragen"), + `auf 127.0.0.1 bleibt alles wie bisher (${t.length} Aufgaben)`); + + /* Und eine erfundene Adresse gilt als Agentur, nicht als Team -- + wer sich einen Hostnamen ausdenkt, bekommt keine Sonderbehandlung. */ + const fremd = await hole("/workspace/api/aufgaben", { host: "crew.boese.de", keks: dogi.keks }); + ok((fremd.daten?.aufgaben || []).length === 2, + `eine erfundene Adresse oeffnet kein drittes Haus (${(fremd.daten?.aufgaben || []).length})`); +} + +d.close(); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } +console.log(""); +console.log(`${fehler === 0 ? "BESTANDEN" : "FEHLGESCHLAGEN"} -- ${geprueft} geprueft, ${fehler} Fehler`); +process.exit(fehler ? 1 : 0); diff --git a/server/pruef-rueckmeldung.mjs b/server/pruef-rueckmeldung.mjs index 33a43dfd..0d19a721 100644 --- a/server/pruef-rueckmeldung.mjs +++ b/server/pruef-rueckmeldung.mjs @@ -37,9 +37,9 @@ const ordner = mkdtempSync(join(tmpdir(), "ws-rueck-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); const { portMussFreiSein } = await import("./helfer-port.mjs"); -await portMussFreiSein(4371, "die Rueckmeldungspruefung"); +await portMussFreiSein(4379, "die Rueckmeldungspruefung"); -process.env.PORT = "4371"; +process.env.PORT = "4379"; process.env.SITE_ACCESS_SECRET = "lokaler-test"; process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; @@ -48,7 +48,7 @@ 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, 1000)); -const BASIS = "http://127.0.0.1:4371"; +const BASIS = "http://127.0.0.1:4379"; /* Netz und Notbremse -- index.js faengt sonst jeden Absturz ab, und der Lauf sieht von aussen aus wie "laeuft noch". */ @@ -96,7 +96,7 @@ function anmelden(rolle, code, wand = "crew.dogfather-universe.com") { return new Promise((fertig) => { const rumpf = JSON.stringify({ rolle, code }); const a = httpAnfrage({ - host: "127.0.0.1", port: 4371, path: "/workspace/api/anmelden", method: "POST", + host: "127.0.0.1", port: 4379, path: "/workspace/api/anmelden", method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(rumpf), Host: wand }, }, (antwort) => { diff --git a/server/workspace-aufgaben.js b/server/workspace-aufgaben.js index 1172e691..e48011c6 100644 --- a/server/workspace-aufgaben.js +++ b/server/workspace-aufgaben.js @@ -13,7 +13,7 @@ import { db, protokolliere, echteIp, sitzungLesen, betreutWo, istLeitung, ohneDogFather, heuteLokal, ROLLEN_SORTIERUNG, betreuteIds, externPruefen, externSql, sichtbarePersonenIds, sichtbareCreatorIds, MODI_KATEGORIEN, kategorienFuer, kategoriePersonen, - ohneTeamDogi, siehtModis, TEAM_DOGI_ROLLEN, + ohneTeamDogi, ohneAgentur, siehtModis, TEAM_DOGI_ROLLEN, } from "./workspace.js"; export const aufgabenRouter = express.Router(); @@ -65,7 +65,28 @@ aufgabenRouter.use("/workspace/api", angemeldet); ===================================================================== */ export function sichtbar(person) { const regel = sichtbarRoh(person); - if (!regel || siehtModis(person)) return regel; + if (!regel) return regel; + /* AUF DER ADRESSE VON TEAM DOGI NUR TEAM DOGI (10.09.2026). + + Filipe: "die daten von dieser seite sollen nichts mit den daten am + hut haben von der workspace seite ... komplett von der anderen + getrennt sein." + + Die Bedingung steht VOR der Regel fuer das andere Haus und nicht + danach: Beide schliessen sich aus, und wer sie hintereinander + haengt, bekommt auf der Team-Adresse eine Bedingung, die sich + selbst widerspricht -- und damit eine leere Seite, die aussieht + wie ein Fehler. + + `person.haus` setzt sitzungLesen() aus der Adresse. Es aendert + keine Rechte, nur den Ausschnitt. */ + if (person.haus === "crew") { + return { + wo: `(${regel.wo}) AND ${ohneAgentur("a", ["creator_id", "verantwortlich_id", "erstellt_von"])}`, + werte: regel.werte, + }; + } + if (siehtModis(person)) return regel; return { wo: `(${regel.wo}) AND ${ohneTeamDogi("a", ["creator_id", "verantwortlich_id", "erstellt_von"])}`, werte: regel.werte, diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js index 63732c6b..b859b595 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -18,7 +18,7 @@ import express from "express"; import { db, protokolliere, echteIp, sitzungLesen, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, siehtAlles, istSpicy, ohneDogFather, externPruefen, externSql, - ohneTeamDogi, siehtModis, MODI_BEREICHE_ERLAUBT, TEAM_DOGI_ROLLEN, + ohneTeamDogi, ohneAgentur, siehtModis, MODI_BEREICHE_ERLAUBT, TEAM_DOGI_ROLLEN, } from "./workspace.js"; export const bereicheRouter = express.Router(); @@ -464,7 +464,28 @@ for (const weg of ["post", "patch", "delete", "put"]) { ===================================================================== */ export function sichtbar(person) { const regel = sichtbarRoh(person); - if (!regel || siehtModis(person)) return regel; + if (!regel) return regel; + /* AUF DER ADRESSE VON TEAM DOGI NUR TEAM DOGI (10.09.2026). + + Filipe: "die daten von dieser seite sollen nichts mit den daten am + hut haben von der workspace seite ... komplett von der anderen + getrennt sein." + + Die Bedingung steht VOR der Regel fuer das andere Haus und nicht + danach: Beide schliessen sich aus, und wer sie hintereinander + haengt, bekommt auf der Team-Adresse eine Bedingung, die sich + selbst widerspricht -- und damit eine leere Seite, die aussieht + wie ein Fehler. + + `person.haus` setzt sitzungLesen() aus der Adresse. Es aendert + keine Rechte, nur den Ausschnitt. */ + if (person.haus === "crew") { + return { + wo: `(${regel.wo}) AND ${ohneAgentur("e", ["creator_id", "erstellt_von"])}`, + werte: regel.werte, + }; + } + if (siehtModis(person)) return regel; return { wo: `(${regel.wo}) AND ${ohneTeamDogi("e", ["creator_id", "erstellt_von"])}`, werte: regel.werte, diff --git a/server/workspace-dateien.js b/server/workspace-dateien.js index f9fdf17c..0fe66088 100644 --- a/server/workspace-dateien.js +++ b/server/workspace-dateien.js @@ -31,7 +31,7 @@ import { mkdirSync, createReadStream, writeFileSync, unlinkSync, statSync } from import { db, protokolliere, echteIp, sitzungLesen, DATEN_ORDNER, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, siehtAlles, istSpicy, ohneDogFather, ROLLEN_SORTIERUNG, externSql, EXTERN_MAX, sichtbarePersonenIds, - ohneTeamDogi, siehtModis, + ohneTeamDogi, ohneAgentur, siehtModis, } from "./workspace.js"; export const dateienRouter = express.Router(); @@ -80,7 +80,28 @@ dateienRouter.use("/workspace/api/dateien", angemeldet); ===================================================================== */ export function sichtbar(person) { const regel = sichtbarRoh(person); - if (!regel || siehtModis(person)) return regel; + if (!regel) return regel; + /* AUF DER ADRESSE VON TEAM DOGI NUR TEAM DOGI (10.09.2026). + + Filipe: "die daten von dieser seite sollen nichts mit den daten am + hut haben von der workspace seite ... komplett von der anderen + getrennt sein." + + Die Bedingung steht VOR der Regel fuer das andere Haus und nicht + danach: Beide schliessen sich aus, und wer sie hintereinander + haengt, bekommt auf der Team-Adresse eine Bedingung, die sich + selbst widerspricht -- und damit eine leere Seite, die aussieht + wie ein Fehler. + + `person.haus` setzt sitzungLesen() aus der Adresse. Es aendert + keine Rechte, nur den Ausschnitt. */ + if (person.haus === "crew") { + return { + wo: `(${regel.wo}) AND ${ohneAgentur("d", ["creator_id", "hochgeladen_von"])}`, + werte: regel.werte, + }; + } + if (siehtModis(person)) return regel; return { wo: `(${regel.wo}) AND ${ohneTeamDogi("d", ["creator_id", "hochgeladen_von"])}`, werte: regel.werte, diff --git a/server/workspace-hinweise.js b/server/workspace-hinweise.js index a4c863db..f2d414a5 100644 --- a/server/workspace-hinweise.js +++ b/server/workspace-hinweise.js @@ -247,7 +247,18 @@ hinweisRouter.get("/workspace/api/hinweise", (req, res) => { Review-Termine und fehlende Zustaendigkeit sind Steuerungswissen. Sie erscheinen deshalb weder bei Creator noch bei Scouts -- so wie die zugehoerigen Felder im Profil auch. */ - if (istLeitung(person)) { + /* DIE AGENTUR-HINWEISE NICHT AUF DER TEAM-ADRESSE (10.09.2026). + + "Creator-Profile sind noch leer" und "Creator hat noch keinen + Start-Check" standen im Bildschirmfoto mitten auf der Team-Seite. + Sie zaehlen ausdruecklich Creator -- auf dieser Adresse gibt es + keine, und ein Hinweis auf etwas, das man dort weder sehen noch + anfassen kann, ist eine Aufgabe ohne Tuer. + + Die Bedingung steht an DIESER Stelle und nicht in den vier + Abfragen darunter: Wer eine fuenfte hinzufuegt, bekommt sie + dadurch mit, ohne daran zu denken. */ + if (istLeitung(person) && person.haus !== "crew") { dazu("review_faellig", "warnung", "Review-Termin überfällig", "profil.html", zaehle(`SELECT COUNT(*) n FROM profile f diff --git a/server/workspace-zentrale.js b/server/workspace-zentrale.js index 8cc46f7b..e868709a 100644 --- a/server/workspace-zentrale.js +++ b/server/workspace-zentrale.js @@ -50,7 +50,7 @@ ===================================================================== */ import express from "express"; -import { db, sitzungLesen, istLeitung, betreuteIds, scoutsVon, verborgeneIds, +import { db, sitzungLesen, istLeitung, betreuteIds, scoutsVon, verborgeneIds, hausBedingung, TEAM_DOGI_ROLLEN } from "./workspace.js"; export const zentraleRouter = express.Router(); @@ -145,9 +145,19 @@ zentraleRouter.get("/workspace/api/zentrale", (req, res) => { der Sprechblase. */ const weg = verborgeneIds(ich); const zusatz = weg.length ? ` AND id NOT IN (${weg.map(() => "?").join(",")})` : ""; + /* UND AUF DER TEAM-ADRESSE NUR DAS TEAM (10.09.2026). + + Genau die Stelle, auf die Filipe im Bildschirmfoto gezeigt + hat: Im Ring stand "9 IM TEAM", obwohl sein Team drei Leute + hat -- gezaehlt wurde das ganze Haus. Der Kommentar darueber + sagt es selbst: Diese Abfrage ist die einzige, die sich die + Menschen selbst holt, statt durch die Listenfunktionen zu + gehen. Wer dort etwas aendert, muss hier daran denken; die + Bedingung kommt deshalb aus derselben Funktion. */ leute = d.prepare( `SELECT id, name, rolle FROM personen - WHERE aktiv = 1 AND id <> ?${zusatz} ORDER BY rolle, name`).all(ich.id, ...weg); + WHERE aktiv = 1 AND id <> ?${zusatz}${hausBedingung(ich)} ORDER BY rolle, name`) + .all(ich.id, ...weg); } else { /* Manager: seine Scouts UND die Creator (eigene wie die seiner Scouts). Scout: nur seine Creator. */ diff --git a/server/workspace.js b/server/workspace.js index f7ef6190..6a4ac51f 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -285,6 +285,51 @@ export function ohneDogFather(praefix, spalten = ["creator_id", "erstellt_von"]) jemand einen Fehler sieht. (Dieselbe Falle steht schon im Kommentar von ohneDogFather; sie ist es wert, zweimal dazustehen.) ===================================================================== */ +/* DIE ROLLEN DER AGENTUR -- das andere Haus. + + Sie stehen als eigene Menge da und nicht als "alles ausser Team + Dogi": Kaeme morgen eine sechste Rolle dazu, waere sie mit einem + `ausser` automatisch in der Agentur, ohne dass jemand darueber + nachgedacht hat. Eine Aufzaehlung zwingt zur Entscheidung -- dieselbe + Ueberlegung wie bei OHNE_MODI_HOSTS in crew-adresse.js. */ +const AGENTUR_ROLLEN = new Set(["spicy", "manager", "scout", "creator"]); + +/* Die gemeinsame Bauweise beider Filter. Sie stand bis zum 10.09.2026 + nur einmal da, in ohneTeamDogi -- und beim zweiten Haus waere sie + abgeschrieben worden. Eine Abschrift ist hier besonders teuer: In ihr + steckt die NULL-Falle (siehe unten), und die sieht man einer Kopie + nicht an. */ +function ohneRollen(praefix, spalten, rollen) { + const liste = [...rollen].map((r) => `'${r}'`).join(", "); + return spalten + .map((sp) => `(${praefix}.${sp} IS NULL OR ${praefix}.${sp} NOT IN ` + + `(SELECT id FROM personen WHERE rolle IN (${liste})))`) + .join(" AND "); +} + +/* ===================================================================== + NUR DAS HAUS VON TEAM DOGI (10.09.2026) + + Das Gegenstueck zu ohneTeamDogi -- und ABSICHTLICH nach demselben + Muster gebaut: "keine Spalte zeigt auf jemanden aus dem anderen + Haus". + + WARUM NICHT "MINDESTENS EINE SPALTE ZEIGT AUF TEAM DOGI": Das waere + die naheliegende Formulierung und sie waere falsch. Eine Aufgabe, die + DogFather fuer einen Creator anlegt, haette dann ueber `erstellt_von` + (er selbst gehoert zum Haus) trotzdem gepasst -- und stuende auf der + Team-Seite, obwohl sie die Agentur betrifft. Andersherum stimmt es: + Sobald IRGENDEINE Spalte auf einen Creator, Scout, Manager oder + Spicy Media zeigt, gehoert die Zeile ins andere Haus. + + Zeilen ohne jede Zuordnung (alle Spalten NULL) bleiben sichtbar. Das + ist richtig: Sie gehoeren dem, der sie geschrieben hat, und ueber die + Regel darueber steht ohnehin schon, wer das sein darf. + ===================================================================== */ +export function ohneAgentur(praefix, spalten = ["creator_id", "erstellt_von"]) { + return ohneRollen(praefix, spalten, AGENTUR_ROLLEN); +} + export function ohneTeamDogi(praefix, spalten = ["creator_id", "erstellt_von"]) { /* Die Rollenliste kommt aus TEAM_DOGI_ROLLEN und steht nicht hier als Text. Sie hiess bis zum 10.09.2026 fest `rolle = 'modi'` -- mit dem @@ -296,11 +341,7 @@ export function ohneTeamDogi(praefix, spalten = ["creator_id", "erstellt_von"]) weil dieser Ausdruck in ein `WHERE` eingebaut wird, das an vielen Stellen zusammengesetzt wird. Die Werte stammen aus einer festen Menge im Code, nie aus einer Eingabe. */ - const liste = [...TEAM_DOGI_ROLLEN].map((r) => `'${r}'`).join(", "); - return spalten - .map((sp) => `(${praefix}.${sp} IS NULL OR ${praefix}.${sp} NOT IN ` - + `(SELECT id FROM personen WHERE rolle IN (${liste})))`) - .join(" AND "); + return ohneRollen(praefix, spalten, TEAM_DOGI_ROLLEN); } /** Darf diese Person ueberhaupt etwas sehen, das einem Modi gehoert? @@ -498,6 +539,21 @@ const HAND_BEREICHE = [...MODI_BEREICHE, EINGANG_KACHEL]; export function bereicheFuer(person) { if (person?.rolle === "modi") return MODI_BEREICHE; if (person?.rolle === "hand") return HAND_BEREICHE; + /* AUF DER ADRESSE VON TEAM DOGI SIEHT AUCH DOGFATHER DIE KACHELN DES + TEAMS (10.09.2026). + + Filipe: "die hier soll ihre eigene daten haben und komplett von der + anderen getrennt sein." Fuenfundzwanzig Kacheln, von denen zwei + Drittel Creator, Scouts und Agentur betreffen, waeren dort + Fenster in ein Haus, in dem er gerade nicht ist -- und hinter + jedem stuende seit heute eine leere Liste. + + ES IST DIESELBE LISTE WIE BEI DER RECHTEN HAND, nicht eine dritte. + Beide sehen auf dieser Adresse dasselbe; eine eigene Liste fuer + ihn waere die, die beim naechsten Umbau auseinanderlaeuft -- und + sie stuende ausserdem gegen die Hausregel, ihn nicht ueber sein + Team zu stellen. */ + if (person?.haus === "crew") return HAND_BEREICHE; return null; } @@ -766,7 +822,14 @@ const ZUSATZ_BEREICHE = { ], }; +/* NICHT AUF DER TEAM-ADRESSE (10.09.2026). Dort liefert bereicheFuer() + ohnehin schon die Kacheln des Teams -- Eingang, Ideen-Board und + Rueckmeldung stuenden sonst zweimal auf der Seite, einmal in + "Taeglich" und einmal in "Team Dogi". Zwei gleiche Kacheln sind + schlimmer als eine fehlende: Man klickt die falsche und sucht den + Unterschied. */ export function zusatzBereicheFuer(person) { + if (person?.haus === "crew") return []; return ZUSATZ_BEREICHE[person?.rolle] || []; } @@ -2859,7 +2922,35 @@ export function sitzungLesen(req) { dieser Adresse ist diese Person schlicht niemand. */ if (!sitzungPasstZurAdresse(reihe.rolle, req.get?.("host"))) return null; - return { id: reihe.id, name: reihe.name, rolle: reihe.rolle }; + /* IN WELCHEM HAUS STEHT DIESE PERSON GERADE? (10.09.2026) + + Filipe: "die daten von dieser seite sollen nichts mit den daten + am hut haben von der workspace seite ... die hier soll ihre + eigene daten haben und komplett von der anderen getrennt sein. + dogfather soll die daten auch auf der anderen seite sehen in der + team dogi kategorie aber auch nur er und vanvan." + + Dasselbe Konto, dieselbe Datenbank -- aber je nach Adresse ein + anderer AUSSCHNITT. Auf der Adresse von Team Dogi sieht auch + DogFather nur sein Team: keine Creator, keine Scouts, keine + Agentur. Auf der Agenturadresse aendert sich nichts. + + ES HAENGT AN DER PERSON UND NICHT AN EINEM ZWEITEN PARAMETER. + Die Sichtbarkeitsregeln bekommen ueberall dieselbe Person + gereicht -- `sichtbar(person)`, `sichtbareCreatorIds(person)`, + `bereicheFuer(person)`. Ein zusaetzliches Argument muesste an + ueber dreissig Aufrufstellen mitgeschleift werden, und die eine + vergessene waere das Loch. So reist die Antwort mit dem + Menschen mit, durch jede Funktion, ohne dass eine davon davon + wissen muss. + + UND ES AENDERT KEINE RECHTE. Es entscheidet, WAS jemand sieht, + nicht, was er darf -- genau wie die Sicht eines anderen + (sichtPerson) das auch nicht tut. Wer hier etwas anderes einbaut, + macht aus einer Anzeigefrage eine Rechtefrage. */ + const haus = istCrewAdresse(req.get?.("host")) ? "crew" : "agentur"; + + return { id: reihe.id, name: reihe.name, rolle: reihe.rolle, haus }; } catch { return null; } } @@ -4075,6 +4166,24 @@ export function termineSichtbar(person, praefix = "t") { DIESELBE REGEL GILT FUER CALLS & PROTOKOLLE: Die Call-Liste ruft genau diese Funktion auf (workspace-calls.js -> sichtbar). Beide Seiten koennen deshalb gar nicht auseinanderlaufen. */ + + /* UND AUF DER ADRESSE VON TEAM DOGI OHNE DIE AGENTUR (10.09.2026). + + Hier steht die Bedingung IN termineSichtbar und nicht in + workspace-kalender.js -- sonst haetten der Kalender, die + Wiederholungen (workspace-serien.js) und der ICS-Abruf + (workspace-ics.js) sie einzeln gebraucht, und der ICS-Abruf ist + genau der, den man vergisst: Er laeuft ohne Bildschirm. + + `teilnehmer_id` gehoert in die Liste. Ein Gespraech mit einem + Creator haengt oft an gar keiner creator_id -- der Creator IST das + Gegenueber. Wer nur zwei Spalten prueft, hat hier nichts geprueft. */ + if (person.haus === "crew") { + return { + wo: `(${eigen}) AND ${ohneAgentur(p, ["creator_id", "teilnehmer_id", "erstellt_von"])}`, + werte, + }; + } return { wo: eigen, werte }; } @@ -4324,10 +4433,72 @@ export function protokollLesen(anzahl = 20) { werden, und niemand kann versehentlich die ungefilterte Fassung benutzen -- die `Roh`-Funktionen werden nicht exportiert. ===================================================================== */ -export const sichtbareCreatorIds = (person) => ohneVerborgene(sichtbareCreatorIdsRoh(person), person); -export const sichtbarePersonenIds = (person) => ohneVerborgene(sichtbarePersonenIdsRoh(person), person); -export const betreuerIds = (person) => ohneVerborgene(betreuerIdsRoh(person), person); -export const einladbareIds = (person) => ohneVerborgene(einladbareIdsRoh(person), person); +/* ===================================================================== + PERSONENLISTEN IM HAUS VON TEAM DOGI (10.09.2026) + + Die sechs Funktionen darunter beantworten "welche Menschen gehen + mich etwas an" -- fuer die Chat-Partnerwahl, die Teilnehmerwahl im + Kalender, die Creator-Auswahl in Formularen und die Pipeline. + + Auf der Adresse von Team Dogi ist die Antwort eine andere: dort gibt + es keine Creator, keine Scouts, keine Manager. Nicht "sie sind + ausgeblendet", sondern: Sie kommen gar nicht erst aus der Datenbank. + + WARUM ALS ZWEITE HUELLE UM ohneVerborgene UND NICHT DARIN: Die beiden + beantworten verschiedene Fragen. ohneVerborgene() nimmt heraus, was + jemand NICHT SEHEN DARF -- eine Rechtefrage, die ueberall gilt. + nurHaus() nimmt heraus, was HIER NICHT HINGEHOERT -- eine Frage der + Adresse. Wer beides in eine Funktion legt, kann spaeter nicht mehr + sagen, welche der beiden gerade zugeschlagen hat. + + Die Reihenfolge ist egal (beide verkleinern nur), die Trennung nicht. + ===================================================================== */ +const HAUS_TEAM_ROLLEN = new Set(["admin", "hand", "modi"]); + +function nurHaus(ids, person) { + if (person?.haus !== "crew") return ids; + try { + const liste = [...HAUS_TEAM_ROLLEN].map((r) => `'${r}'`).join(", "); + const erlaubt = db().prepare( + `SELECT id FROM personen WHERE rolle IN (${liste})`).all().map((z) => z.id); + /* `null` heisst bei diesen Funktionen "alle". Sobald das Haus + einschraenkt, darf es das nicht mehr heissen -- die Liste wird + deshalb AUSGESCHRIEBEN, genau wie ohneVerborgene() es tut. */ + if (ids === null) return erlaubt; + const menge = new Set(erlaubt); + return ids.filter((i) => menge.has(i)); + } catch { + /* EIN FEHLSCHLAG SCHLIESST, ER OEFFNET NICHT. Gaebe es hier `ids` + zurueck, waere bei einer Datenbankstoerung ploetzlich die ganze + Agentur auf der Team-Adresse zu sehen -- und niemand haette eine + Fehlermeldung gesehen. Eine leere Liste ist sichtbar falsch, eine + zu volle nicht. */ + return []; + } +} + +/* DASSELBE ALS SQL-SCHNIPSEL -- fuer die zwei Abfragen im Haus, die + bewusst NICHT ueber die Listenfunktionen gehen. + + Das ist der Ring in der Zentrale (er holt sich das ganze Haus selbst; + im Kommentar dort steht ausdruecklich, dass er die einzige solche + Stelle ist) und die Hinweiszeile darunter. Beide zaehlen Menschen, + und beide standen auf der Team-Adresse mit Zahlen aus der Agentur da + -- "9 IM TEAM", obwohl das Team drei Leute hat. Filipe hat genau + darauf gezeigt. + + EINE FUNKTION UND KEINE ZWEITE ROLLENLISTE: Wer den Ausdruck + abschreibt, hat beim naechsten Rollenwechsel zwei Wahrheiten. */ +export function hausBedingung(person, spalte = "rolle") { + if (person?.haus !== "crew") return ""; + const liste = [...HAUS_TEAM_ROLLEN].map((r) => `'${r}'`).join(", "); + return ` AND ${spalte} IN (${liste})`; +} + +export const sichtbareCreatorIds = (person) => nurHaus(ohneVerborgene(sichtbareCreatorIdsRoh(person), person), person); +export const sichtbarePersonenIds = (person) => nurHaus(ohneVerborgene(sichtbarePersonenIdsRoh(person), person), person); +export const betreuerIds = (person) => nurHaus(ohneVerborgene(betreuerIdsRoh(person), person), person); +export const einladbareIds = (person) => nurHaus(ohneVerborgene(einladbareIdsRoh(person), person), person); /* SICH SELBST NICHT -- UND ZWAR AUCH DANN NICHT, WENN DIE LISTE AUSGESCHRIEBEN WIRD (10.09.2026). @@ -4349,7 +4520,7 @@ export const einladbareIds = (person) => ohneVerborgene(einladbareIdsRoh(person) seit es ueberhaupt jemanden zu verbergen gibt; vorher konnte es ihn nicht geben. */ export const schreibbareIds = (person) => { - const ids = ohneVerborgene(schreibbareIdsRoh(person), person); + const ids = nurHaus(ohneVerborgene(schreibbareIdsRoh(person), person), person); return ids === null ? null : ids.filter((i) => i !== person?.id); }; -export const pipelineIds = (person) => ohneVerborgene(pipelineIdsRoh(person), person); +export const pipelineIds = (person) => nurHaus(ohneVerborgene(pipelineIdsRoh(person), person), person);