From 986364595286229531933da3ccca9f2f154a4bd8 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Thu, 24 Sep 2026 20:40:20 +0200 Subject: [PATCH] =?UTF-8?q?Die=20zwei=20H=C3=A4user=20sind=20getrennt=20--?= =?UTF-8?q?=20und=20die=20T=C3=BCr=20geht=20in=20beide=20Richtungen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filipe: "ich will dass du zuerst die komplette site vn der workspace seite trennst. da soll nichts verknüpft sein. wenn ich bei der einen was mache soll nichts bei der anderen passieren. … es soll nur für dogfather eine kachel geben wo er mit einem einfachen klick von der einen auf die anderen seite kommt aber sonst garnichts." Das kehrt die Entscheidung vom 10.09.2026 um ("getrennt wird das AUSSEHEN, nicht der Bestand"). Wer den alten Kommentar liest, liest einen überholten Stand -- das steht jetzt an jeder betroffenen Stelle. WAS GEMESSEN WAR, BEVOR ETWAS GEBAUT WURDE * Nur drei Module trennten nach Haus (Aufgaben, Bereiche, Dateien). Chat, Kalender, Wissen, Material, Personenlisten und der Rest nicht. * Die Trennung war EINSEITIG: nurHaus() griff nur auf crew. * siehtModis() hebelte sie für DogFather auf der Agenturseite aus -- in seiner Gesprächsliste standen dort beide Häuser nebeneinander. * Keine haus-Spalte in der Datenbank. * Der Bestand kreuzte aber kaum: 0 von 86 Terminen gemischt, 0 von 7 Zweier-/Gruppengesprächen, genau EIN Kanal. WAS JETZT DASTEHT * Drei Rollenmengen in crew-adresse.js (crew / agentur / beide) und hausVonRolle(); eine unbekannte Rolle bekommt null, kein Haus. * Spalte `haus` an acht Wurzeltabellen, nachgetragen aus Belegen: 238 Zeilen eindeutig, die Wissensablage geschlossen der Agentur, vier Restzeilen namentlich, der gemischte Kanal aufgelöst (die zwei Scouts gehen heraus, die 7 Nachrichten sind alle vom Team). Offen bleiben: null. * nurHaus, hausBedingung und darfAnlegen gelten in BEIDE Richtungen. * Der siehtModis-Durchgriff ist weg -- aber in DREI Fällen, nicht zwei: Prüfadressen bekommen gar kein Haus und verhalten sich exakt wie vorher. Die erste Fassung hatte das übersehen und 19 Prüfungen umgeworfen, an denen nichts kaputt war. * Kalender: getrennt, aber "belegt" bleibt (Filipes Entscheidung). Die Blöcke tragen NUR Beginn und Dauer -- kein Titel, keine Person. Gebaut als Gegenstück zur Liste (meine Termine MINUS die sichtbaren), damit beide nicht auseinanderlaufen können. * Die Wissens-Kachel ist auf der Team-Adresse weg UND die Route antwortet dort mit 404 -- eine fehlende Kachel ist nur eine Bitte. * Die Wechsel-Kachel für DogFather geht jetzt in beide Richtungen. GEPRÜFT: pruef-haus-trennung 97 statt 81, 0 Fehler (vorher 7, alle haben die alte Regel behauptet). Die neuen Abschnitte sind DORT eingezogen statt in eine zweite Datei -- `pruef-haustrennung.mjs` hätte sich von `pruef-haus-trennung.mjs` um einen Bindestrich unterschieden. Dazu grün: haus-seiten, crew-adresse, chat, chat-kanaele, kanal-besetzung, kalender, serien, treffchat, wissen-neu, modi-checkliste, modi-katalog, rechtetafel, personen-liste, sicht, verborgen, fremde-sicht, alle-wege. NICHT VON MIR: pruef-treff (3) und pruef-kachel-universum (2) waren schon vorher rot -- beim Treff auf dem Stand 40b48e89 nachgemessen, bei den Farben steht dieselbe Zahl im Kopf der Prüfung selbst. Co-Authored-By: Claude Opus 5 --- server/crew-adresse.js | 100 +++++- server/pruef-haus-trennung.mjs | 173 +++++++++- server/workspace-aufgaben.js | 33 ++ server/workspace-bereiche.js | 33 ++ server/workspace-calls.js | 10 +- server/workspace-chat.js | 50 ++- server/workspace-content.js | 7 +- server/workspace-dateien.js | 33 ++ server/workspace-kalender.js | 75 ++++- server/workspace-serien.js | 17 +- server/workspace-treffchat.js | 16 +- server/workspace-wissen.js | 23 ++ server/workspace.js | 538 ++++++++++++++++++++++++++++++-- workspace/anruf-probe.html | 10 +- workspace/app.webmanifest | 8 +- workspace/assets/js/kalender.js | 49 ++- workspace/aufgaben.html | 38 +-- workspace/automation.html | 32 +- workspace/befinden.html | 34 +- workspace/bereich.html | 44 +-- workspace/bewerben.html | 38 +-- workspace/bewerbungen.html | 36 +-- workspace/calls.html | 34 +- workspace/chat.html | 38 +-- workspace/content.html | 34 +- workspace/crew-index.html | 12 +- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 34 +- workspace/entwicklung.html | 36 +-- workspace/hilfe.html | 30 +- workspace/index.html | 10 +- workspace/kalender.html | 34 +- workspace/leistung.html | 32 +- workspace/material.html | 32 +- workspace/personen.html | 32 +- workspace/profil.html | 36 +-- workspace/rechte.html | 34 +- workspace/report.html | 36 +-- workspace/scouting.html | 32 +- workspace/start.html | 32 +- workspace/startcheck.html | 34 +- workspace/steckbrief.html | 36 +-- workspace/support.html | 28 +- workspace/talente.html | 36 +-- workspace/team.html | 32 +- workspace/teamlage.html | 34 +- workspace/teilen.html | 30 +- workspace/treff-moderation.html | 32 +- workspace/treff-regeln.html | 32 +- workspace/uebersicht.html | 32 +- workspace/unsere-seiten.html | 30 +- workspace/unterstuetzen.html | 30 +- workspace/werdegang.html | 32 +- workspace/willkommen.html | 30 +- workspace/wissen.html | 34 +- 55 files changed, 1718 insertions(+), 697 deletions(-) diff --git a/server/crew-adresse.js b/server/crew-adresse.js index 7c5dd8c0..1abb1f05 100644 --- a/server/crew-adresse.js +++ b/server/crew-adresse.js @@ -78,6 +78,20 @@ import { fileURLToPath } from "node:url"; export const CREW_ADRESSE = "https://crew.dogfather-universe.com"; +/** Und die Adresse des Agenturhauses. + * + * Sie stand bisher nur als Text in `anmeldeAdresseFuer` ganz unten. + * Seit die Wechselkachel in BEIDE Richtungen geht (24.09.2026), wird + * sie an zwei Stellen gebraucht -- und zwei gleiche Zeichenketten sind + * eine Umzugsadresse, die beim naechsten Umzug halb mitwandert. Das + * ist in diesem Haus schon passiert: Der Workspace ist am 06.09.2026 + * umgezogen, und die Rueckmeldung an den Server lief danach ins Leere. + * + * KEINE ABLEITUNG AUS `OHNE_MODI_HOSTS`: Dort stehen drei Adressen + * (mit und ohne www, plus die Hauptseite). Welche davon die Tuer ist, + * waere geraten. */ +export const WORKSPACE_ADRESSE = "https://workspace.dogfather-universe.com"; + /* WELCHE SYMBOLGROESSEN HAT DIE CREW-APP EIGENS? Abgeleitet aus dem Ordner, nicht aufgezaehlt: Fuer jede Datei crew-.png, zu der es auch workspace-.png gibt, kann umgebogen @@ -157,6 +171,90 @@ export const AUSSEN_ROLLEN = new Set(["gast"]); weniger prueft, sondern mehr: Was vorher die Adresse erledigt hat, muss jetzt jede einzelne Regel halten. */ +/* ===================================================================== + DIE ZWEI HAEUSER -- WELCHE ROLLE WO WOHNT (24.09.2026) + ===================================================================== + + Filipe: „ich will dass du zuerst die komplette site vn der workspace + seite trennst. da soll nichts verknuepft sein. wenn ich bei der einen + was mache soll nichts bei der anderen passieren." + + DAS KEHRT DIE ENTSCHEIDUNG VOM 10.09.2026 UM. Damals hiess es + ausdruecklich: „es soll trotzdem so bleiben dass ich die daten hier + und da sehe", und weiter unten in dieser Datei steht bis heute + „Getrennt wird also das AUSSEHEN, nicht der Bestand." Das gilt nicht + mehr. Wer den alten Satz liest, liest einen ueberholten Stand -- + deshalb steht die Umkehr hier oben und nicht als Fussnote. + + WARUM DIE MENGEN HIER STEHEN: Diese Datei hat keine eigenen Importe + und ist damit der einzige Ort, an dem eine Rollenmenge stehen kann, + ohne einen Kreis zu bauen. TEAM_DOGI_ROLLEN und AUSSEN_ROLLEN wohnen + aus demselben Grund hier. + + DREI MENGEN UND NICHT ZWEI. „Alles ausser Team Dogi" waere die + naheliegende Formulierung fuer die Agentur -- und sie waere falsch, + denn DogFather gehoert in BEIDE Haeuser. Eine Bedingung durch + Ausschluss haette ihn stillschweigend der Agentur zugeschlagen. Genau + diese Bauart ist in dieser Datei schon dreimal schiefgegangen (siehe + Falle 2 im Kopf). Deshalb wird jede Rolle einzeln genannt. + + UND EINE ROLLE MUSS IRGENDWO STEHEN: `hausVonRolle` gibt fuer eine + unbekannte Rolle `null` zurueck, nicht „beide" und nicht „agentur". + Ein `null` faellt auf -- `pruef-haustrennung` wird rot, sobald eine + Rolle in ROLLEN steht, die hier fehlt. Eine stille Voreinstellung + waere eine offene Tuer, die niemand bemerkt. */ + +/** Die Rollen, die ausschliesslich im Agenturhaus leben. */ +export const AGENTUR_ROLLEN = new Set(["spicy", "manager", "scout", "creator"]); + +/** Die Rollen, die in BEIDEN Haeusern arbeiten. Genau eine: DogFather. + * Als Menge und nicht als `rolle === "admin"`, damit die drei Mengen + * zusammen gegen ROLLEN geprueft werden koennen. */ +export const BEIDE_HAEUSER_ROLLEN = new Set(["admin"]); + +/** + * In welchem Haus wohnt diese Rolle? + * + * @returns {"crew"|"agentur"|"beide"|null} `null` heisst: unbekannte + * Rolle. Das ist ein Fehler, kein Standardfall. + */ +export function hausVonRolle(rolle) { + if (TEAM_DOGI_ROLLEN.has(rolle) || AUSSEN_ROLLEN.has(rolle)) return "crew"; + if (AGENTUR_ROLLEN.has(rolle)) return "agentur"; + if (BEIDE_HAEUSER_ROLLEN.has(rolle)) return "beide"; + return null; +} + +/** + * Welche Rollen kommen in diesem Haus vor -- DogFather eingerechnet? + * + * ABGELEITET AUS DENSELBEN MENGEN, nicht abgeschrieben. Wer eine Rolle + * verschiebt, verschiebt damit jede SQL-Abfrage mit, die diese Funktion + * benutzt. Genau das Abschreiben hat am 24.09.2026 dazu gefuehrt, dass + * die „An wen"-Reihe sechs Leute zeigte und der Server vier belieferte. + */ +export function rollenImHaus(haus) { + const eigen = haus === "crew" + ? [...TEAM_DOGI_ROLLEN, ...AUSSEN_ROLLEN] + : [...AGENTUR_ROLLEN]; + return new Set([...eigen, ...BEIDE_HAEUSER_ROLLEN]); +} + +/** + * Und welche Rollen gehoeren NUR dem anderen Haus? + * + * Das ist die Menge, nach der gefiltert wird -- nicht „welche gehoeren + * hierher". Der Unterschied ist DogFather: Er steht in beiden, also + * darf er in keiner der beiden Ausschlussmengen auftauchen. Eine Zeile, + * die „gehoert hierher" fragt, wuerde eine Aufgabe, die DogFather fuer + * einen Creator angelegt hat, ueber `erstellt_von` ins Team-Haus + * ziehen. Derselbe Gedanke steht bei ohneTeamDogi() in workspace.js. + */ +export function nurAnderesHaus(haus) { + return haus === "crew" ? new Set(AGENTUR_ROLLEN) + : new Set([...TEAM_DOGI_ROLLEN, ...AUSSEN_ROLLEN]); +} + /** Die Adresse von Team Dogi -- und seit 11.09.2026 auch die des * Treffs. Feste Menge, kein Muster -- Falle 1. */ const CREW_HOSTS = new Set(["crew.dogfather-universe.com"]); @@ -569,5 +667,5 @@ export function crewWeiche(req, res, next) { export function anmeldeAdresseFuer(rolle) { return (TEAM_DOGI_ROLLEN.has(rolle) || AUSSEN_ROLLEN.has(rolle)) ? CREW_ADRESSE - : "https://workspace.dogfather-universe.com"; + : WORKSPACE_ADRESSE; } diff --git a/server/pruef-haus-trennung.mjs b/server/pruef-haus-trennung.mjs index 9fb69f59..b957b49c 100644 --- a/server/pruef-haus-trennung.mjs +++ b/server/pruef-haus-trennung.mjs @@ -269,8 +269,20 @@ console.log("=== Aufgaben ==="); `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})`); + /* ==== UMGEDREHT AM 24.09.2026 =================================== + Hier stand „auf workspace. stehen beide". Das war richtig, solange + „getrennt wird das Aussehen, nicht der Bestand" galt. Filipe hat + das umgedreht: „wenn ich bei der einen was mache soll nichts bei + der anderen passieren." + + SIE IST DADURCH STRENGER, NICHT SCHWAECHER: Vorher wurde EIN + Zustand geprueft („beides da"), jetzt ein UNTERSCHIED („das eine + ja, das andere nicht"). Ein Filter, der einfach alles wegnimmt, + faellt damit auf; vorher waere er nur an der ersten Haelfte + aufgefallen. */ + ok(t(a.ws).includes("Lunas Steckbrief nachtragen") + && !t(a.ws).includes("Clips vom Freitag schneiden"), + `auf workspace. steht NUR die der Agentur (${t(a.ws).join(" | ") || "leer"})`); } /* ======================================================================= @@ -288,8 +300,11 @@ console.log("=== Bereiche ==="); 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"); + /* AUCH HIER UMGEDREHT (24.09.2026): „er sieht es hier wie dort" war + die alte Regel. Das Ideen-Board gehoert dem Team -- in der Agentur + hat es nichts zu suchen, auch nicht bei ihm. */ + ok(!(ideen.ws?.eintraege || []).some((e) => e.titel === "Wunschkonzert am Sonntag"), + "und auf workspace. NICHT -- das Brett gehoert dem Team"); } /* ======================================================================= @@ -315,12 +330,39 @@ console.log("=== Kalender ==="); Gegenteil. Sie ist dadurch nicht schwaecher: Sie prueft jetzt eine UNTERSCHEIDUNG (er ja, sie nein) statt einer einzelnen Antwort, und das ist mehr, nicht weniger. */ + /* ==== DRITTE FASSUNG DIESER ERWARTUNG (24.09.2026) ============== + + 10.09.: getrennt. 11.09.: fuer DogFather verbunden („mein kalender + soll komplett verbunden sein"). 24.09.: getrennt -- aber die + ZEITEN der anderen Seite bleiben als titellose Bloecke sichtbar. + + Auf die Nachfrage, was aus der Ausnahme wird, hat Filipe genau + das entschieden: „Getrennt, aber ‚belegt' bleibt sichtbar." + + WARUM DAS KEIN RUECKSCHRITT ZUM 10.09. IST: Der Grund fuer die + Ausnahme galt weiter -- wer die Haelfte seines Tages nicht sieht, + legt einen Call auf eine besetzte Stunde. Beide Anspruechen ist + zugleich zu genuegen, indem man den INHALT trennt und die ZEIT + nicht. Genau das wird hier gemessen. */ const a = await beide(weg, dogi.keks); const t = (x) => (x?.termine || []).map((z) => z.titel); - ok(t(a.crew).includes("Teamrunde") && t(a.crew).includes("Call mit Luna"), - `DogFather sieht auf crew. seinen GANZEN Kalender (${t(a.crew).join(" | ") || "leer"})`); - ok(t(a.ws).includes("Call mit Luna") && t(a.ws).includes("Teamrunde"), - `und auf workspace. denselben (${t(a.ws).length})`); + ok(t(a.crew).includes("Teamrunde") && !t(a.crew).includes("Call mit Luna"), + `auf crew. steht der Team-Termin und NICHT der der Agentur (${t(a.crew).join(" | ") || "leer"})`); + ok(t(a.ws).includes("Call mit Luna") && !t(a.ws).includes("Teamrunde"), + `auf workspace. umgekehrt (${t(a.ws).join(" | ") || "leer"})`); + + /* UND DIE ZEIT IST TROTZDEM DA. Ohne diese drei Zeilen waere oben + nur bewiesen, dass etwas verschwindet -- und der Kalender waere + wieder die Falle vom 11.09. */ + const bc = a.crew?.belegt || []; + const bw = a.ws?.belegt || []; + ok(bc.length > 0 && bw.length > 0, + `er sieht auf beiden Seiten belegte Zeiten der anderen (${bc.length}/${bw.length})`); + ok(bc.every((x) => Object.keys(x).sort().join(",") === "beginn,dauer_min"), + "und die Bloecke tragen NUR Beginn und Dauer -- keinen Titel, keine Person"); + const hBelegt = (await hole(weg, { host: CREW, keks: hand.keks })).daten?.belegt || []; + ok(hBelegt.length === 0, + `die rechte Hand bekommt keine Bloecke (${hBelegt.length}) -- "NICHT BEI VANVAN"`); /* UND DIE RECHTE HAND NICHT -- sonst hiesse "verbunden" schlicht "die Trennung ist weg". */ @@ -344,8 +386,12 @@ console.log("=== Die Personenliste ==="); `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})`); + /* UMGEDREHT (24.09.2026): Vorher „auf workspace. stehen alle". + Ayla ist Modi -- sie gehoert ins Team-Haus, nicht in seine + Schreibliste auf der Agenturseite. */ + ok(["Luna", "Cigdem", "Patrick"].every((x) => n(a.ws).includes(x)) + && !n(a.ws).includes("Ayla"), + `auf workspace. steht NUR die Agentur (${n(a.ws).join(", ") || "leer"})`); } /* ======================================================================= @@ -466,8 +512,11 @@ console.log("=== Personen & Zugaenge ==="); `auf crew. steht das Team (${wer(liste.crew).join(", ") || "leer"})`); ok(!wer(liste.crew).some((n) => ["Luna", "Cigdem", "Patrick"].includes(n)), "und niemand aus der Agentur"); - ok(["Luna", "Cigdem", "Patrick", "Ayla"].every((n) => wer(liste.ws).includes(n)), - `auf workspace. stehen alle (${wer(liste.ws).length})`); + /* UMGEDREHT (24.09.2026) -- dieselbe Regel wie eine Seite hoeher: + Wer im Team wohnt, steht in der Agenturverwaltung nicht. */ + ok(["Luna", "Cigdem", "Patrick"].every((n) => wer(liste.ws).includes(n)) + && !wer(liste.ws).includes("Ayla"), + `auf workspace. steht NUR die Agentur (${wer(liste.ws).join(", ") || "leer"})`); /* Die anlegbaren Rollen kommen aus derselben Auskunft, aus der die Oberflaeche ihre Knoepfe baut. Auf crew. duerfen es nur Team-Rollen @@ -807,6 +856,90 @@ console.log("=== Alle Kategorien ==="); ok(dogiLegtAn.status === 201, `DogFather kann es (${dogiLegtAn.status})`); } +/* ======================================================================= + 11b. WAS AM 24.09.2026 DAZUGEKOMMEN IST + + Drei Dinge, die es am 10.09. noch nicht gab und die deshalb in der + Aufteilung oben fehlen: die Rollentafel selbst, die Wissensablage + und die Tuer zwischen den Haeusern. + + Sie stehen HIER und nicht in einer eigenen Datei. Beim Bauen lag + eine zweite, fast gleich benannte Pruefung daneben + (`pruef-haustrennung.mjs` gegen `pruef-haus-trennung.mjs`) -- zwei + Dateien, die dasselbe meinen und deren Namen sich um einen + Bindestrich unterscheiden. Genau die Bauart, vor der in diesem Haus + an fuenfzehn Stellen gewarnt wird. Sie ist wieder weg. + ======================================================================= */ +console.log(""); +console.log("=== Die Rollentafel: wohnt jede Rolle irgendwo? ==="); +{ + const w = await import("./workspace.js"); + const ROLLEN = ["spicy", "admin", "manager", "scout", "creator", + "hand", "linke", "modi", "gast"]; + const ohne = ROLLEN.filter((r) => !w.hausVonRolle(r)); + /* DIE ZAHL GEHOERT IN DIE BEDINGUNG: Waere ROLLEN leer, meldete + `filter().length === 0` brav "alles gut", ohne eine einzige Rolle + angesehen zu haben. Genau so stand dieses Haus am 01.09. dreimal + gruen da. */ + ok(ROLLEN.length === 9 && ohne.length === 0, + `alle ${ROLLEN.length} Rollen haben ein Haus${ohne.length ? " -- ausser: " + ohne.join(", ") : ""}`); + ok(w.hausVonRolle("admin") === "beide", + `DogFather wohnt in beiden (${w.hausVonRolle("admin")})`); + /* DER DRITTE AUSGANG: Eine unbekannte Rolle darf NICHT stillschweigend + in einem Haus landen. Ohne diese Zeile koennte hausVonRolle immer + "agentur" sagen und die Pruefung darueber waere trotzdem gruen. */ + ok(w.hausVonRolle("erfunden") === null, + "eine unbekannte Rolle bekommt KEIN Haus, sondern null"); + ok(!w.rollenImHaus("crew").has("scout") && w.rollenImHaus("crew").has("admin"), + "im Team-Haus: kein Scout, aber DogFather"); + ok(!w.rollenImHaus("agentur").has("modi") && w.rollenImHaus("agentur").has("admin"), + "im Agenturhaus: kein Modi, aber DogFather"); + /* Und die Bedingung muss sich UNTERSCHEIDEN -- waere sie fuer beide + Haeuser gleich, traefen die Messungen oben zufaellig zu. */ + ok(w.hausWo({ haus: "crew" }, "t") !== w.hausWo({ haus: "agentur" }, "t"), + "die Hausbedingung ist fuer die beiden Haeuser verschieden"); + ok(w.hausWo({ rolle: "admin" }, "t") === "1=1", + "ohne bekanntes Haus wird nicht gefiltert -- Pruefadressen bleiben heil"); +} + +console.log(""); +console.log("=== Die Wissensablage wohnt in der Agentur ==="); +{ + /* Filipe, 24.09.2026: „diese komplette kachel soll nur im workspace + sein." Die Kachel ist drueben weg -- das allein ist aber nur eine + Bitte. Wer die Adresse kennt, tippt sie. */ + const a = await beide("/workspace/api/wissen", dogi.keks); + const inAgentur = await hole("/workspace/api/wissen", { host: WS, keks: dogi.keks }); + const imTeam = await hole("/workspace/api/wissen", { host: CREW, keks: dogi.keks }); + void a; + ok(inAgentur.status === 200, + `in der Agentur ist sie da (${inAgentur.status}) -- Gegenprobe`); + ok(imTeam.status === 404, + `auf der Team-Adresse gibt es sie nicht (${imTeam.status})`); +} + +console.log(""); +console.log("=== Eine Tuer, in beide Richtungen -- und sonst nichts ==="); +{ + const w = await import("./workspace.js"); + const ausTeam = w.zusatzBereicheFuer({ rolle: "admin", haus: "crew", id: idDogi }); + const ausAgentur = w.zusatzBereicheFuer({ rolle: "admin", haus: "agentur", id: idDogi }); + ok(ausTeam.length === 1 && ausAgentur.length === 1, + `von beiden Seiten fuehrt GENAU EINE Kachel hinueber (${ausTeam.length}/${ausAgentur.length})`); + ok(ausTeam[0]?.ziel?.startsWith("https://workspace."), + `aus dem Team fuehrt sie in die Agentur (${ausTeam[0]?.ziel})`); + ok(ausAgentur[0]?.ziel?.startsWith("https://crew."), + `aus der Agentur fuehrt sie ins Team (${ausAgentur[0]?.ziel})`); + /* NUR FUER DOGFATHER. Bei einem Modi waere sie zugleich eine + Adresse, die er nicht kennen soll -- und eine Seite, die aus + nichts als einem Ausgang besteht. */ + const andere = ["hand", "linke", "modi", "gast", "scout", "creator", "manager", "spicy"]; + ok(andere.length === 8 + && andere.every((r) => w.zusatzBereicheFuer({ rolle: r, haus: "crew" }).length === 0 + && w.zusatzBereicheFuer({ rolle: r, haus: "agentur" }).length === 0), + `und niemand sonst bekommt sie (${andere.length} Rollen, beide Adressen)`); +} + /* ======================================================================= 12. Die Gegenprobe zur Regel selbst ======================================================================= */ @@ -825,8 +958,20 @@ console.log("=== Haengt es wirklich an der Adresse? ==="); /* 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})`); + /* VERGLICHEN STATT GEZAEHLT (24.09.2026). Hier stand `length === 2` + -- eine feste Zahl, die genau so lange stimmte, bis sich das + Verhalten der Agenturadresse aenderte. Genau das ist am 24.09. + passiert: Seit die Grenze auch dort gilt, sind es nicht mehr zwei. + Die Pruefung wurde rot, obwohl an der Sache nichts kaputt war. + + Gefragt wird jetzt, was die Aussage MEINT: Eine erfundene Adresse + verhaelt sich wie die Agenturadresse und oeffnet nichts Drittes. + Diese Formulierung kann der Kalender nicht einholen. */ + const echt = await hole("/workspace/api/aufgaben", { host: WS, keks: dogi.keks }); + const titel = (x) => (x.daten?.aufgaben || []).map((z) => z.titel).sort().join(" | "); + ok(titel(echt).length > 0 && titel(fremd) === titel(echt), + `eine erfundene Adresse verhaelt sich wie die Agenturadresse ` + + `(${titel(fremd) || "leer"} / ${titel(echt) || "leer"})`); } d.close(); diff --git a/server/workspace-aufgaben.js b/server/workspace-aufgaben.js index 2fbe2885..cc03d844 100644 --- a/server/workspace-aufgaben.js +++ b/server/workspace-aufgaben.js @@ -92,6 +92,39 @@ export function sichtbar(person) { werte: regel.werte, }; } + /* ==== DREI FAELLE UND NICHT ZWEI (24.09.2026, nachgebessert) ===== + + Hier stand bis heute `if (siehtModis(person)) return regel;` -- + "wer Modi-Daten sehen darf, bekommt sie ungefiltert". Auf der + AGENTURADRESSE hiess das in der Praxis: DogFather sieht dort auch + Team Dogi. Genau das hat Filipe am 24.09.2026 abgeschafft. + + DIE ERSTE FASSUNG DIESER REPARATUR WAR ZU GROB: Ich habe die Zeile + einfach gestrichen. Damit fiel auch der Fall "keine der beiden + Adressen" durch -- also jede Pruefadresse. Gemessen: 19 + Fehlschlaege in drei Pruefungen, weil ein Modi seine eigenen + Aufgaben nicht mehr sah. Nicht weil etwas kaputt war, sondern weil + `ohneTeamDogi` dort ploetzlich auf ihn selbst zeigte. + + ES SIND DREI LAGEN, und jede bekommt ihre Zeile: + crew -> ohne die Agentur + agentur -> ohne Team Dogi, AUCH fuer DogFather (das ist neu) + kein Haus (localhost, Pruefungen) -> die alte Rollenregel, + unveraendert. Sonst waeren alle Modi-Pruefungen des + Hauses still gruen und blind -- die Falle, vor der + crew-adresse.js dreimal warnt. + + `siehtModis` BLEIBT UNVERAENDERT und wird anderswo weiter + gebraucht (Rechtefragen, z.B. in der Checkliste). "Darf ich das + sehen" und "welchen Ausschnitt sehe ich gerade" sind zwei Fragen; + wer sie in eine Funktion legt, kann spaeter nicht mehr sagen, + welche zugeschlagen hat. */ + if (person.haus === "agentur") { + return { + wo: `(${regel.wo}) AND ${ohneTeamDogi("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"])}`, diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js index e92d95c3..00fe270a 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -996,6 +996,39 @@ export function sichtbar(person) { werte: regel.werte, }; } + /* ==== DREI FAELLE UND NICHT ZWEI (24.09.2026, nachgebessert) ===== + + Hier stand bis heute `if (siehtModis(person)) return regel;` -- + "wer Modi-Daten sehen darf, bekommt sie ungefiltert". Auf der + AGENTURADRESSE hiess das in der Praxis: DogFather sieht dort auch + Team Dogi. Genau das hat Filipe am 24.09.2026 abgeschafft. + + DIE ERSTE FASSUNG DIESER REPARATUR WAR ZU GROB: Ich habe die Zeile + einfach gestrichen. Damit fiel auch der Fall "keine der beiden + Adressen" durch -- also jede Pruefadresse. Gemessen: 19 + Fehlschlaege in drei Pruefungen, weil ein Modi seine eigenen + Aufgaben nicht mehr sah. Nicht weil etwas kaputt war, sondern weil + `ohneTeamDogi` dort ploetzlich auf ihn selbst zeigte. + + ES SIND DREI LAGEN, und jede bekommt ihre Zeile: + crew -> ohne die Agentur + agentur -> ohne Team Dogi, AUCH fuer DogFather (das ist neu) + kein Haus (localhost, Pruefungen) -> die alte Rollenregel, + unveraendert. Sonst waeren alle Modi-Pruefungen des + Hauses still gruen und blind -- die Falle, vor der + crew-adresse.js dreimal warnt. + + `siehtModis` BLEIBT UNVERAENDERT und wird anderswo weiter + gebraucht (Rechtefragen, z.B. in der Checkliste). "Darf ich das + sehen" und "welchen Ausschnitt sehe ich gerade" sind zwei Fragen; + wer sie in eine Funktion legt, kann spaeter nicht mehr sagen, + welche zugeschlagen hat. */ + if (person.haus === "agentur") { + return { + wo: `(${regel.wo}) AND ${ohneTeamDogi("e", ["creator_id", "erstellt_von"])}`, + werte: regel.werte, + }; + } if (siehtModis(person)) return regel; return { wo: `(${regel.wo}) AND ${ohneTeamDogi("e", ["creator_id", "erstellt_von"])}`, diff --git a/server/workspace-calls.js b/server/workspace-calls.js index 0c25243e..7be0d702 100644 --- a/server/workspace-calls.js +++ b/server/workspace-calls.js @@ -346,10 +346,14 @@ callRouter.post("/workspace/api/calls/:id/protokoll", gleicheHerkunft, (req, res if (nb) { naechster = Number(d.prepare(` INSERT INTO termine - (titel, beschreibung, art, beginn, dauer_min, ort, creator_id, teilnehmer_id, erstellt, erstellt_von) - VALUES (?,?,?,?,?,?,?,?,?,?)`).run( + (titel, beschreibung, art, beginn, dauer_min, ort, creator_id, teilnehmer_id, erstellt, erstellt_von, haus) + VALUES (?,?,?,?,?,?,?,?,?,?,?)`).run( termin.titel, `Folgetermin zu „${termin.titel}"`, termin.art, nb, dauer, - termin.ort, termin.creator_id, termin.teilnehmer_id, jetzt(), req.person.id).lastInsertRowid); + termin.ort, termin.creator_id, termin.teilnehmer_id, jetzt(), req.person.id, + /* Ein Folgetermin erbt das Haus SEINES Vorgaengers und nicht + das der Adresse: Wer ihn ansetzt, verlaengert etwas, das + schon irgendwo wohnt. */ + termin.haus || req.person.haus || null).lastInsertRowid); d.prepare("UPDATE protokolle SET naechster_termin_id = ? WHERE id = ?").run(naechster, protokollId); } diff --git a/server/workspace-chat.js b/server/workspace-chat.js index 98812339..06c8e7ed 100644 --- a/server/workspace-chat.js +++ b/server/workspace-chat.js @@ -45,7 +45,7 @@ import { mkdirSync, writeFileSync, createReadStream, statSync, unlinkSync, import { db, protokolliere, echteIp, sitzungLesen, istDogFather, schreibbareIds, darfSchreibenMit, ROLLEN_SORTIERUNG, DATEN_ORDNER, - fuehrtTeamDogi, MODI_KATEGORIEN, ROLLEN_GRUPPE, TEAM_DOGI_ROLLEN, + fuehrtTeamDogi, MODI_KATEGORIEN, ROLLEN_GRUPPE, TEAM_DOGI_ROLLEN, hausWo, istTeamDogi, } from "./workspace.js"; @@ -511,9 +511,14 @@ function teilnehmerVon(raumId) { * Spalte wäre eine zweite Wahrheit neben chat_teilnehmer. */ function direktRaum(personA, idB, { anlegen = true } = {}) { const d = db(); + /* GESUCHT WIRD NUR IM EIGENEN HAUS (24.09.2026). Ohne das faende + DogFather auf der Team-Adresse sein Agentur-Gespraech mit + derselben Person wieder -- und schriebe hinein, ohne es zu + merken. Zwei Haeuser heisst: notfalls zwei Gespraeche. */ const vorhanden = d.prepare(` SELECT r.id FROM chat_raeume r WHERE r.art = 'direkt' + AND ${hausWo(personA, "r")} AND EXISTS (SELECT 1 FROM chat_teilnehmer t WHERE t.raum_id = r.id AND t.person_id = ?) AND EXISTS (SELECT 1 FROM chat_teilnehmer t WHERE t.raum_id = r.id AND t.person_id = ?) AND (SELECT COUNT(*) FROM chat_teilnehmer t WHERE t.raum_id = r.id) = 2`) @@ -522,8 +527,13 @@ function direktRaum(personA, idB, { anlegen = true } = {}) { if (!anlegen) return null; const n = jetzt(); - d.prepare("INSERT INTO chat_raeume (art, erstellt, erstellt_von, letzte_am) VALUES ('direkt',?,?,NULL)") - .run(n, personA.id); + /* DAS HAUS WIRD BEIM ANLEGEN GESCHRIEBEN, nicht spaeter gerechnet. + `personA.haus` kommt aus der Adresse, auf der gerade gearbeitet + wird -- die einzige Auskunft, die auch dann stimmt, wenn der Raum + noch leer ist. */ + d.prepare("INSERT INTO chat_raeume (art, erstellt, erstellt_von, letzte_am, haus)" + + " VALUES ('direkt',?,?,NULL,?)") + .run(n, personA.id, personA.haus || null); const raumId = d.prepare("SELECT last_insert_rowid() AS id").get().id; const rein = d.prepare( "INSERT INTO chat_teilnehmer (raum_id, person_id, seit, gelesen_bis) VALUES (?,?,?,0)"); @@ -1081,12 +1091,30 @@ chatRouter.get("/workspace/api/chat/raeume", (req, res) => { Chat stehen, ohne dass jemand daran denken muss. */ if (gehoertInTreff(req.person)) treffAngleichen(); + /* ==== NUR DIE RAEUME DIESES HAUSES (24.09.2026) ================ + + Filipe: „die seiten soll nicht mehr im chat kalender und alles + andere verbunden sein." + + DIESE LISTE HATTE KEINEN HAUS-FILTER, und das fiel nur bei einem + einzigen Menschen auf: DogFather. Alle anderen sehen ohnehin nur + Raeume, in denen sie sitzen, und sie sitzen nur in denen ihres + Hauses. Er sitzt in beiden -- gemessen am echten Stand standen + auf der Team-Adresse in seiner Gespraechsliste auch seine zwei + Zweier-Gespraeche mit Scouts. + + UEBER DIE SPALTE UND NICHT UEBER DIE TEILNEHMER: Ein Raum, in + dem nur DogFather sitzt (er legt einen Kanal an, es ist noch + niemand drin), haette ueber die Teilnehmer keinen Beleg und + stuende in beiden Haeusern. Die Spalte weiss, wo er ihn + angelegt hat. */ const reihen = db().prepare(` SELECT r.id, r.art, r.name, r.kategorie, r.letzte_am, r.erstellt_von, t.gelesen_bis, t.leitung, t.geloescht_bis FROM chat_raeume r JOIN chat_teilnehmer t ON t.raum_id = r.id AND t.person_id = ? WHERE t.raus_am IS NULL + AND ${hausWo(req.person, "r")} AND (t.geloescht_am IS NULL OR EXISTS (SELECT 1 FROM chat_nachrichten n WHERE n.raum_id = r.id AND n.id > t.geloescht_bis)) @@ -1265,8 +1293,13 @@ chatRouter.post("/workspace/api/chat/gruppe", gleicheHerkunft, express.json({ li const d = db(); const n = jetzt(); - d.prepare("INSERT INTO chat_raeume (art, name, erstellt, erstellt_von) VALUES ('gruppe',?,?,?)") - .run(name, n, req.person.id); + /* Auch die Gruppe merkt sich ihr Haus (24.09.2026). Sie hat beim + Anlegen genau EIN Mitglied -- den, der sie anlegt. Ueber die + Beteiligten waere sie also gar nicht einzuordnen, solange + DogFather sie anlegt und noch niemand dazugekommen ist. */ + d.prepare("INSERT INTO chat_raeume (art, name, erstellt, erstellt_von, haus)" + + " VALUES ('gruppe',?,?,?,?)") + .run(name, n, req.person.id, req.person.haus || null); const raumId = d.prepare("SELECT last_insert_rowid() AS id").get().id; d.prepare("INSERT INTO chat_teilnehmer (raum_id, person_id, seit, gelesen_bis, leitung) VALUES (?,?,?,0,1)") .run(raumId, req.person.id, n); @@ -1403,12 +1436,13 @@ chatRouter.post("/workspace/api/chat/kanal", gleicheHerkunft, express.json({ lim } const n = jetzt(); - d.prepare(`INSERT INTO chat_raeume (art, name, kategorie, erstellt, erstellt_von) - VALUES ('kanal',?,?,?,?)`) + d.prepare(`INSERT INTO chat_raeume (art, name, kategorie, erstellt, erstellt_von, haus) + VALUES ('kanal',?,?,?,?,?)`) /* Der eigene Name geht in die Spalte, der feste kommt aus MODI_KATEGORIEN. Beides landet in derselben Spalte -- die Zeile im Chat liest nur noch dort nach. */ - .run(anzeigeName || kanalName(schluessel), schluessel, n, req.person.id); + .run(anzeigeName || kanalName(schluessel), schluessel, n, req.person.id, + req.person.haus || null); const raumId = d.prepare("SELECT last_insert_rowid() AS id").get().id; /* DIE GANZE TEAMFUEHRUNG KOMMT HINEIN, nicht nur der Anlegende. diff --git a/server/workspace-content.js b/server/workspace-content.js index c24bd5f1..fcc8a347 100644 --- a/server/workspace-content.js +++ b/server/workspace-content.js @@ -401,9 +401,10 @@ contentRouter.post("/workspace/api/content/:id/uebernehmen", gleicheHerkunft, (r } const beginn = e.geplant.length === 10 ? e.geplant + "T10:00" : e.geplant; const r = db().prepare(` - INSERT INTO termine (titel, beschreibung, art, beginn, dauer_min, creator_id, erledigt, erstellt, erstellt_von) - VALUES (?,?,'termin',?,60,?,0,?,?)`).run( - ("Dreh: " + e.titel).slice(0, 160), beschreibung, beginn, e.creator_id, jetzt(), req.person.id); + INSERT INTO termine (titel, beschreibung, art, beginn, dauer_min, creator_id, erledigt, erstellt, erstellt_von, haus) + VALUES (?,?,'termin',?,60,?,0,?,?,?)`).run( + ("Dreh: " + e.titel).slice(0, 160), beschreibung, beginn, e.creator_id, jetzt(), req.person.id, + req.person.haus || null); neu = { art: "Termin", id: Number(r.lastInsertRowid), wohin: "kalender.html" }; } else { diff --git a/server/workspace-dateien.js b/server/workspace-dateien.js index afe91e04..0f71a401 100644 --- a/server/workspace-dateien.js +++ b/server/workspace-dateien.js @@ -103,6 +103,39 @@ export function sichtbar(person) { werte: regel.werte, }; } + /* ==== DREI FAELLE UND NICHT ZWEI (24.09.2026, nachgebessert) ===== + + Hier stand bis heute `if (siehtModis(person)) return regel;` -- + "wer Modi-Daten sehen darf, bekommt sie ungefiltert". Auf der + AGENTURADRESSE hiess das in der Praxis: DogFather sieht dort auch + Team Dogi. Genau das hat Filipe am 24.09.2026 abgeschafft. + + DIE ERSTE FASSUNG DIESER REPARATUR WAR ZU GROB: Ich habe die Zeile + einfach gestrichen. Damit fiel auch der Fall "keine der beiden + Adressen" durch -- also jede Pruefadresse. Gemessen: 19 + Fehlschlaege in drei Pruefungen, weil ein Modi seine eigenen + Aufgaben nicht mehr sah. Nicht weil etwas kaputt war, sondern weil + `ohneTeamDogi` dort ploetzlich auf ihn selbst zeigte. + + ES SIND DREI LAGEN, und jede bekommt ihre Zeile: + crew -> ohne die Agentur + agentur -> ohne Team Dogi, AUCH fuer DogFather (das ist neu) + kein Haus (localhost, Pruefungen) -> die alte Rollenregel, + unveraendert. Sonst waeren alle Modi-Pruefungen des + Hauses still gruen und blind -- die Falle, vor der + crew-adresse.js dreimal warnt. + + `siehtModis` BLEIBT UNVERAENDERT und wird anderswo weiter + gebraucht (Rechtefragen, z.B. in der Checkliste). "Darf ich das + sehen" und "welchen Ausschnitt sehe ich gerade" sind zwei Fragen; + wer sie in eine Funktion legt, kann spaeter nicht mehr sagen, + welche zugeschlagen hat. */ + if (person.haus === "agentur") { + return { + wo: `(${regel.wo}) AND ${ohneTeamDogi("d", ["creator_id", "hochgeladen_von"])}`, + werte: regel.werte, + }; + } if (siehtModis(person)) return regel; return { wo: `(${regel.wo}) AND ${ohneTeamDogi("d", ["creator_id", "hochgeladen_von"])}`, diff --git a/server/workspace-kalender.js b/server/workspace-kalender.js index 1916d982..9b9be27c 100644 --- a/server/workspace-kalender.js +++ b/server/workspace-kalender.js @@ -16,6 +16,7 @@ import { db, protokolliere, echteIp, sitzungLesen, istLeitung, istDogFather, siehtAlles, betreutWo, termineSichtbar, heuteLokal, kategorienFuer, externPruefen, externSql, betreuteIds, einladbareIds, verborgeneIds, ROLLEN_SORTIERUNG, ROLLEN_GRUPPE, + hausWo, ohneAnderesHaus, } from "./workspace.js"; import { nachfuellenAlle, serienPruefen, serieAnlegen, zuordnungErzwingen, @@ -486,7 +487,72 @@ kalenderRouter.get("/workspace/api/termine", (req, res) => { b.text = b.text ? String(b.text).slice(0, 180) : null; } - res.json({ termine, fristen, brettsachen, von, bis, checkliste_moeglich: moeglich }); + /* ==== WAS AUF DER ANDEREN SEITE SCHON BELEGT IST (24.09.2026) === + + Filipe, zur Frage, was aus der Kalender-Ausnahme vom 11.09. wird: + „Getrennt, aber ‚belegt' bleibt sichtbar." + + ZWEI ANSPRUECHE, EINE ANZEIGE. Der INHALT gehoert getrennt -- + kein Titel, kein Teilnehmer, keine Notiz aus dem anderen Haus. + Die ZEIT gehoert nicht getrennt: Wer die Haelfte seines Tages + nicht sieht, legt einen Call auf eine Stunde, in der er schon + woanders sitzt. Genau das war der Grund fuer die alte Ausnahme, + und der Grund gilt weiter. + + WAS HIER RUEBERGEHT, IST DAS MINIMUM: Beginn und Dauer. Kein + Titel, keine Nummer, keine Person -- nicht einmal die Art. Man + kann daraus ablesen, DASS etwas ist, und nichts darueber, WAS. + + NUR FUER DIE DOGFATHER-ROLLE, und das ist unveraendert sein Wort + vom 11.09.2026: „NUR IN DER DOGFATHER ROLLE BEI DOGFATHER: NICHT + BEI VANVAN." Die rechte Hand sieht von der anderen Seite nichts, + auch keine Bloecke. + + UND ES SIND SEINE EIGENEN TERMINE, nicht alle des anderen + Hauses. `eigen` fragt dasselbe wie die Liste darueber: angelegt, + zugeordnet, Gegenueber oder Teilnehmer. Ein Kalender, der die + Verabredungen fremder Leute als „belegt" zeigt, waere + unbrauchbar -- und ein Leck obendrein. */ + let belegt = []; + if (req.person.rolle === "admin" + && (req.person.haus === "crew" || req.person.haus === "agentur")) { + /* ==== DAS GEGENSTUECK ZUR LISTE, NICHT EINE ZWEITE REGEL ======= + + Der erste Entwurf fragte `t.haus = `. Das war eine + ZWEITE Vorstellung davon, was "die andere Seite" ist -- und + zwei Vorstellungen laufen auseinander. Gemessen ist genau das + sofort passiert: Termine ohne eingetragenes Haus (aus einem + Schreibweg, der es noch nicht setzt) wurden von der Liste + korrekt ueber die BETEILIGTEN getrennt, vom Block aber nicht + gefunden -- null Bloecke, und der Kalender waere wieder die + Falle vom 11.09. gewesen. + + Jetzt steht hier die Rechnung: MEINE Termine MINUS die, die + dieses Haus zeigt. Was die Liste verbirgt, wird zum Block -- + beides kann sich nicht mehr widersprechen, egal wie das Haus + einer Zeile bestimmt wird. + + `haus: null` HOLT NUR DEN "EIGEN"-TEIL: termineSichtbar haengt + die Hausbedingung nur an, wenn ein Haus dasteht. So muss die + Frage "was sind meine Termine" nicht ein zweites Mal + formuliert werden. */ + const SPALTEN_HAUS = ["creator_id", "teilnehmer_id", "erstellt_von"]; + const meine = termineSichtbar({ ...req.person, haus: null }, "t"); + const hierSichtbar = `${hausWo(req.person, "t")}` + + ` AND ${ohneAnderesHaus("t", req.person, SPALTEN_HAUS)}`; + belegt = db().prepare(` + SELECT t.beginn, t.dauer_min + FROM termine t + WHERE (${meine.wo}) + AND NOT (${hierSichtbar}) + AND t.erledigt = 0 + AND t.beginn >= ? AND t.beginn < ? + ORDER BY t.beginn`) + .all(...meine.werte, von, bis + "T23:59:59Z"); + } + + res.json({ termine, fristen, brettsachen, von, bis, + checkliste_moeglich: moeglich, belegt }); } catch (fehler) { console.error("[workspace] Termine lesen:", fehler?.message); res.status(503).json({ fehler: "nicht_verfuegbar" }); @@ -603,11 +669,12 @@ kalenderRouter.post("/workspace/api/termine", gleicheHerkunft, (req, res) => { const { lastInsertRowid } = db().prepare(` INSERT INTO termine (titel, beschreibung, art, beginn, dauer_min, ort, creator_id, teilnehmer_id, - teilnehmer_extern, erledigt, erstellt, erstellt_von) - VALUES (?,?,?,?,?,?,?,?,?,0,?,?)`).run( + teilnehmer_extern, erledigt, erstellt, erstellt_von, haus) + VALUES (?,?,?,?,?,?,?,?,?,0,?,?,?)`).run( aus.titel, aus.beschreibung ?? null, aus.art ?? "termin", aus.beginn, aus.dauer_min ?? 30, aus.ort ?? null, aus.creator_id ?? null, - aus.teilnehmer_id ?? null, aus.teilnehmer_extern ?? null, jetzt(), req.person.id); + aus.teilnehmer_id ?? null, aus.teilnehmer_extern ?? null, jetzt(), req.person.id, + req.person.haus || null); /* Die weiteren Teilnehmer. Das Haupt-Gegenueber kommt als `hauptId` mit hinein, damit die Liste vollstaendig ist -- an ihr haengt die diff --git a/server/workspace-serien.js b/server/workspace-serien.js index f9298f97..f310187b 100644 --- a/server/workspace-serien.js +++ b/server/workspace-serien.js @@ -266,8 +266,8 @@ export function serieNachfuellen(serieId) { INSERT INTO termine (titel, beschreibung, art, beginn, dauer_min, ort, creator_id, teilnehmer_id, teilnehmer_extern, erledigt, erstellt, erstellt_von, serie_id, serie_tag, - serie_beruehrt) - VALUES (?,?,?,?,?,?,?,?,?,0,?,?,?,?,0)`); + serie_beruehrt, haus) + VALUES (?,?,?,?,?,?,?,?,?,0,?,?,?,?,0,?)`); /* DIE TEILNEHMER DER SERIE ERBEN (05.09.2026). @@ -292,7 +292,12 @@ export function serieNachfuellen(serieId) { const { lastInsertRowid } = einfuegen.run( s.titel, s.beschreibung, s.art, `${tag}T${s.uhrzeit}`, s.dauer_min, s.ort, s.creator_id, s.teilnehmer_id, s.teilnehmer_extern ?? null, - stempel, s.erstellt_von, s.id, tag); + stempel, s.erstellt_von, s.id, tag, + /* AUS DER SERIE UND NICHT AUS DER ADRESSE: Der Nachfueller laeuft + auch ohne Mensch davor (beim Start, per Zeitgeber). Es gibt + dort keine Adresse, aus der man das Haus lesen koennte -- die + Regel weiss es. */ + s.haus || null); /* Haupt-Gegenueber und Serienteilnehmer -- dieselbe Regel wie beim einzelnen Termin: Die Liste ist die vollstaendige Antwort. */ for (const pid of new Set([...(s.teilnehmer_id ? [s.teilnehmer_id] : []), ...serienLeute])) { @@ -432,12 +437,12 @@ export function serieAnlegen(aus, person, teilnehmer = null) { const { lastInsertRowid } = d.prepare(` INSERT INTO termin_serien (titel, beschreibung, art, takt, start_tag, uhrzeit, ende_tag, dauer_min, ort, - creator_id, teilnehmer_id, teilnehmer_extern, aktiv, erstellt, erstellt_von) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,1,?,?)`).run( + creator_id, teilnehmer_id, teilnehmer_extern, aktiv, erstellt, erstellt_von, haus) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?)`).run( aus.titel, aus.beschreibung ?? null, aus.art ?? "termin", aus.takt, aus.start_tag, aus.uhrzeit, aus.ende_tag ?? null, aus.dauer_min ?? 30, aus.ort ?? null, aus.creator_id ?? null, aus.teilnehmer_id ?? null, aus.teilnehmer_extern ?? null, - jetztStempel(), person.id); + jetztStempel(), person.id, person.haus || null); const id = Number(lastInsertRowid); diff --git a/server/workspace-treffchat.js b/server/workspace-treffchat.js index db34a1b3..d0f4fec5 100644 --- a/server/workspace-treffchat.js +++ b/server/workspace-treffchat.js @@ -218,8 +218,20 @@ export function treffRaumId() { .get(TREFF_KATEGORIE); if (da) return da.id; - d.prepare(`INSERT INTO chat_raeume (art, name, kategorie, erstellt, erstellt_von) - VALUES ('kanal', ?, ?, ?, NULL)`) + /* DER TREFF GEHOERT ZU TEAM DOGI -- fest und nicht aus einer + Adresse abgeleitet (24.09.2026). Diese Funktion laeuft OHNE + Mensch davor: Sie legt den Raum beim ersten Bedarf an, notfalls + aus einem Zeitgeber heraus. `erstellt_von` steht deshalb auf + NULL, und ein `req.person.haus` gibt es hier nicht. + + Dass er ins Team-Haus gehoert, ist keine Annahme, sondern + Filipes Entscheidung vom 11.09.2026: „das soll keine app fuer + sich sein, das soll in der crew seite adaptiert werden." + Nachgemessen: Im Raum „Das Rudel" sitzen vier Gaeste, die rechte + und die linke Hand, vier Modis und DogFather -- alle aus dem + Team-Haus, keiner aus der Agentur. */ + d.prepare(`INSERT INTO chat_raeume (art, name, kategorie, erstellt, erstellt_von, haus) + VALUES ('kanal', ?, ?, ?, NULL, 'crew')`) .run(TREFF_NAME, TREFF_KATEGORIE, jetzt()); const id = d.prepare("SELECT last_insert_rowid() AS id").get().id; console.log(`[treff] Chat-Raum angelegt (Nummer ${id}).`); diff --git a/server/workspace-wissen.js b/server/workspace-wissen.js index f8839914..d4378d1f 100644 --- a/server/workspace-wissen.js +++ b/server/workspace-wissen.js @@ -145,6 +145,29 @@ function angemeldet(req, res, next) { const person = sitzungLesen(req); if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" }); req.person = person; + /* ==== DIE WISSENSABLAGE WOHNT IM AGENTURHAUS (24.09.2026) ======== + + Filipe, zur Frage, wohin die neun Dokumente gehoeren: "diese + komplette kachel soll nur im workspace sein." + + DIE KACHEL IST DRUEBEN SCHON WEG (siehe MODI_BEREICHE in + workspace.js). Das allein ist aber nur eine Bitte: `wissen.html` + und `/workspace/api/wissen` kann jeder aufrufen, der die Adresse + einmal gesehen hat -- und wer die Seite offen hatte, als die + Kachel noch da war, hat sie im Verlauf stehen. + + GENAU DIESE LUECKE HAT DAS HAUS SCHON ZWEIMAL GEHABT: Am 10.09. + die Zugangswand, am 21.09. `crew-haus.css` -- beide Male lieferte + express.static eine Datei aus, die "eigentlich" woanders hingehoert, + weil jemand ihren Namen nannte. Eine Absicht im Kommentar ist keine + Schranke. + + 404 UND KEINE 403: "Gibt es hier nicht" ist die Wahrheit -- die + Ablage gehoert zum anderen Haus. Ein "verboten" wuerde sagen, dass + es sie gibt und man nur nicht darf. */ + if (person.haus === "crew") { + return res.status(404).json({ fehler: "nicht_gefunden" }); + } next(); } function nurDogFather(req, res, next) { diff --git a/server/workspace.js b/server/workspace.js index b878140e..13d72c3d 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -36,10 +36,12 @@ import { hilfeTabellen } from "./hilfe-tabellen.js"; import { supportTabellen } from "./support-tabellen.js"; import { unterstuetzungTabellen } from "./unterstuetzung-tabellen.js"; import { sitzungPasstZurAdresse, istOhneModiAdresse, istCrewAdresse, istPruefAdresse, AUSSEN_ROLLEN, - TEAM_DOGI_ROLLEN, CREW_ADRESSE } from "./crew-adresse.js"; + TEAM_DOGI_ROLLEN, CREW_ADRESSE, WORKSPACE_ADRESSE, AGENTUR_ROLLEN, BEIDE_HAEUSER_ROLLEN, + hausVonRolle, rollenImHaus, nurAnderesHaus } from "./crew-adresse.js"; /* Weitergereicht, damit die Fachmodule sie wie alles andere aus workspace.js beziehen und nicht wissen muessen, wo sie wohnt. */ -export { TEAM_DOGI_ROLLEN }; +export { TEAM_DOGI_ROLLEN, AGENTUR_ROLLEN, BEIDE_HAEUSER_ROLLEN, + hausVonRolle, rollenImHaus, nurAnderesHaus }; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -580,8 +582,16 @@ export function darfAnlegen(person) { Quelle sowohl fuer die Pruefung in der Route als auch fuer die Auswahl in der Oberflaeche -- deshalb verschwinden die anderen Rollen dort von selbst, statt eine Absage zu bringen. */ - if (person.haus !== "crew") return alle; - return alle.filter((r) => HAUS_TEAM_ROLLEN.has(r)); + /* UND SEIT 24.09.2026 GILT DASSELBE ANDERSHERUM. Bis dahin stand + hier „auf der Agenturadresse alles" -- DogFather konnte dort also + einen Modi anlegen, der danach im Team-Haus auftauchte, ohne dass + er die Seite je gesehen haette. Ein Umbau auf der einen Seite darf + auf der anderen nichts bewirken; das Anlegen einer Person ist + genau so ein Umbau. */ + const haus = person.haus; + if (haus !== "crew" && haus !== "agentur") return alle; + const hier = rollenImHaus(haus); + return alle.filter((r) => hier.has(r)); } /** Wer darf die Rolle einer Person aendern, die schon da ist? @@ -744,8 +754,19 @@ export function ohneDogFather(praefix, spalten = ["creator_id", "erstellt_von"]) 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"]); + Ueberlegung wie bei OHNE_MODI_HOSTS in crew-adresse.js. + + SEIT 24.09.2026 STEHT SIE IN crew-adresse.js, nicht mehr hier. + Beim Bau der Haustrennung habe ich dort eine zweite, wortgleiche + Menge angelegt und es erst gemerkt, als Node den Doppelnamen + ablehnte -- also nur, weil zufaellig derselbe Name gewaehlt war. + Haette ich sie `AGENTUR` genannt, staenden jetzt zwei Listen da, die + beim naechsten Rollenwechsel auseinanderlaufen. Genau die Bauart, + die in diesem Haus schon viermal Daten gekostet hat. + + Sie wohnt jetzt bei TEAM_DOGI_ROLLEN und AUSSEN_ROLLEN, wo die + anderen beiden Haus-Mengen stehen -- crew-adresse.js hat keine + eigenen Importe und ist damit der einzige Ort ohne Kreisgefahr. */ /* Die gemeinsame Bauweise beider Filter. Sie stand bis zum 10.09.2026 nur einmal da, in ohneTeamDogi -- und beim zweiten Haus waere sie @@ -992,9 +1013,21 @@ const MODI_BEREICHE = [ { gruppe: "Für dich", gruppeUnter: "Deine Seite im Team", name: "Mein Steckbrief", unter: "Dein Bild und deine Kanäle", zeichen: "steckbrief", ton: 10, ziel: "steckbrief.html", szene: "halle" }, - { gruppe: "Für dich", gruppeUnter: "Deine Seite im Team", - name: "Wissen", unter: "Nachschlagen statt nachfragen", zeichen: "wissen", - ton: 9, ziel: "wissen.html", szene: "arena" }, + /* ==== DIE WISSENS-KACHEL IST HIER RAUS (24.09.2026) ============= + + Filipe, zur Frage, wohin die neun Dokumente gehören: „diese + komplette kachel soll nur im workspace sein." + + NICHT NUR DIE DOKUMENTE, DIE KACHEL. Die Ablage enthält die + Schulungen von Spicy Media („SPICY MEDIA 01–04"), OBS, TikTok + LIVE Studio — Material für Creator, nicht für die Moderation. + Eine Kachel stehenzulassen, hinter der nichts mehr liegt, wäre + eine Tür in einen leeren Raum: Man drückt sie jeden Tag einmal + und lernt nur, dass sie nichts tut. + + UND DIE ROUTE IST MITGESICHERT (`wissenNurAgentur` weiter unten). + Eine fehlende Kachel ist eine Bitte, keine Schranke — die Adresse + `wissen.html` kann jeder tippen, der sie einmal gesehen hat. */ ]; /** @@ -2698,11 +2731,6 @@ const TEAM_GRUPPE = { die sie ersetzt. ===================================================================== */ export function zusatzBereicheFuer(person) { - /* Auf der Team-Adresse liefert bereicheFuer() ohnehin die Kacheln des - Teams. Eine Tuer zu dem Haus, in dem man schon steht, waere ein - Knopf, der nichts tut. */ - if (person?.haus === "crew") return []; - /* NUR DOGFATHER UND VANVAN -- also `admin`. Der erste Entwurf gab die Tuer auch an TEAM_DOGI_ROLLEN (die @@ -2719,15 +2747,46 @@ export function zusatzBereicheFuer(person) { `admin` ist der einzige Fall, der wirklich zwei Haeuser hat. */ if (person?.rolle !== "admin") return []; + /* ==== DIE TUER GEHT JETZT IN BEIDE RICHTUNGEN (24.09.2026) ======== + + Filipe: "es soll nur fuer dogfather eine kachel geben wo er mit + einem einfachen klick von der einen auf die anderen seite kommt + aber sonst garnichts." + + Bis heute stand oben `if (person?.haus === "crew") return [];` -- + die Tuer gab es also nur im Agenturhaus. Auf der Team-Adresse gab + es keinen Weg zurueck; man musste die Adresse tippen. Am 21.09. + war das richtig, weil von dort aus ohnehin alles zusammenhing. + Seit die Haeuser hart getrennt sind, ist es eine Sackgasse -- und + zwar genau die Sorte, die derselbe Absatz weiter oben fuer die + andere Richtung schon als Fehler benennt. + + EINE KACHEL, NICHT ZWEI LISTEN: Welches Haus gerade gemeint ist, + entscheidet `person.haus`; Name, Ziel und Unterzeile leiten sich + daraus ab. Zwei getrennte Bloecke waeren zwei Fassungen desselben + Gedankens, und die naechste Aenderung ginge in nur eine davon. + + ZUR ANMELDUNG: Die beiden Adressen haben getrennte Sitzungen (der + Keks ist hostgebunden, `path=/workspace`, ohne `domain`). Beim + ERSTEN Klick meldet man sich drueben einmal an -- danach haelt es + 180 Tage. Den Keks auf `.dogfather-universe.com` zu setzen, um das + zu sparen, waere falsch: Dann ginge der Team-Keks bei jedem Aufruf + an die Agenturadresse mit, also genau die Verbindung, die hier + gerade getrennt wird -- und die verborgene Adresse haette eine + zweite Spur. */ + const nachCrew = person.haus !== "crew"; + return [{ - gruppe: "Team Dogi", + gruppe: nachCrew ? "Team Dogi" : "Creator Workspace", gruppeUnter: "Das andere Haus \u2014 eigene Seite, eigene Daten", - name: "Zu Team Dogi", - unter: "Das Rudel, das Team und die Moderation \u2014 auf ihrer eigenen Adresse", + name: nachCrew ? "Zu Team Dogi" : "Zum Creator Workspace", + unter: nachCrew + ? "Das Rudel, das Team und die Moderation \u2014 auf ihrer eigenen Adresse" + : "Creator, Scouts und die Agentur \u2014 auf ihrer eigenen Adresse", zeichen: "hinaus", ton: 40, szene: "portal", - ziel: CREW_ADRESSE + "/workspace/start.html", + ziel: (nachCrew ? CREW_ADRESSE : WORKSPACE_ADRESSE) + "/workspace/start.html", /* AUSSEN heisst: Diese Kachel fuehrt aus dem Haus hinaus. Ohne das Merkmal wuerde nurOffeneKacheln() sie wegfiltern -- es baut aus dem Ziel einen Pfad ("/workspace/" + ziel) und fragt die @@ -2938,6 +2997,262 @@ function checkListeErweitern(d, tabelle, spalte, marker, werte, jetztStempel) { } } +/* ===================================================================== + DAS HAUS DER ALTEN ZEILEN NACHTRAGEN (24.09.2026) + ===================================================================== + + Alles, was vor der Trennung entstanden ist, hat kein Haus. Diese + Funktion trägt es nach -- EINMAL, und danach findet sie nichts mehr + zu tun (sie fragt nur nach `haus IS NULL`). + + SIE LEITET AB, SIE RÄT NICHT. Für jede Zeile wird gesammelt, welche + MENSCHEN daran hängen, und aus deren Rollen das Haus bestimmt: + + * genau ein Haus unter den Beteiligten -> das ist es + * beide Häuser unter den Beteiligten -> KONFLIKT, bleibt leer + * nur DogFather (oder niemand) -> unbestimmbar, bleibt leer + + DOGFATHER ZÄHLT BEI DER BESTIMMUNG NICHT MIT, und das ist der ganze + Trick: Er wohnt in beiden Häusern. Zählte er mit, wäre jede zweite + Zeile ein "Konflikt". Er ist kein Beleg -- die anderen sind es. + + WAS LEER BLEIBT, WIRD GENANNT -- mit Tabelle, Nummer und Titel. Eine + Nachtragung, die schweigend eine Voreinstellung setzt, verschiebt + Termine zwischen Häusern, ohne dass es jemand merkt. Gemessen am + echten Stand sind es genau ZWEI Termine; die kann man von Hand + entscheiden. Eine Voreinstellung wäre für die 87 anderen richtig und + für diese zwei geraten -- und man wüsste nicht, welche zwei. + + BEI DEN EINTRÄGEN ENTSCHEIDET DER BEREICH und nicht der Ersteller. + `regeln`, `anschlag`, `highlight` sind Bretter von Team Dogi, + `agentur` gehört der Agentur -- das steht fest, unabhängig davon, + wer gerade etwas hineingeschrieben hat. Der Ersteller ist bei 20 von + 26 Einträgen DogFather und damit kein Beleg; der Bereich ist einer. + ===================================================================== */ +export function haeuserNachtragen(d) { + const meldung = { gesetzt: 0, konflikt: [], offen: [] }; + try { + /* Wer gehört wohin -- einmal gelesen, nicht je Zeile. Bei 19 + Personen und 250 Zeilen wären das sonst 250 Abfragen für eine + Antwort, die sich während des Laufs nicht ändert. */ + const hausJePerson = new Map(); + for (const p of d.prepare("SELECT id, rolle FROM personen").all()) { + hausJePerson.set(p.id, hausVonRolle(p.rolle)); + } + /* Aus einer Liste von Personennummern das Haus -- oder warum nicht. + "beide" (DogFather) und unbekannte Nummern zählen nicht mit. */ + const hausAus = (ids) => { + const gefunden = new Set(); + for (const i of ids) { + const h = hausJePerson.get(i); + if (h === "crew" || h === "agentur") gefunden.add(h); + } + if (gefunden.size === 1) return [...gefunden][0]; + return gefunden.size > 1 ? "konflikt" : null; + }; + + /* Je Tabelle: wie komme ich an die beteiligten Menschen? + Als Tafel und nicht als sieben Blöcke -- ein achter Eintrag ist + dann eine Zeile und keine Kopie. */ + const TAFEL = [ + ["chat_raeume", (r) => d.prepare( + "SELECT person_id AS p FROM chat_teilnehmer WHERE raum_id = ?") + .all(r.id).map((z) => z.p), "name"], + ["termine", (r) => [ + ...d.prepare("SELECT person_id AS p FROM termin_teilnehmer WHERE termin_id = ?") + .all(r.id).map((z) => z.p), + r.creator_id, r.teilnehmer_id, r.erstellt_von, + ].filter((x) => x != null), "titel"], + ["aufgaben", (r) => [r.creator_id, r.verantwortlich_id, r.erstellt_von] + .filter((x) => x != null), "titel"], + /* DIE DATEI ERBT VON DEM, WORAN SIE HAENGT. Ein Titelbild, das + DogFather an einen Eintrag von Team Dogi gehaengt hat, hat + ueber die Personen keinen Beleg -- ueber den Eintrag sehr + wohl. Gemessen: drei von acht Dateien waeren sonst offen + geblieben, obwohl ihr Haus eindeutig feststeht. + Die Eintraege werden WEITER UNTEN gesetzt; deshalb liest diese + Zeile `eintraege.haus` erst, wenn es schon dasteht -- die + Reihenfolge in dieser Funktion ist Absicht, nicht Zufall. */ + ["dateien", (r) => [r.creator_id, r.hochgeladen_von] + .filter((x) => x != null), "name_original", + (r) => hausVomEltern(d, r)], + /* DIE WISSENSABLAGE GEHOERT GANZ DER AGENTUR (24.09.2026, + Entscheidung Filipe). Deshalb steht hier ein fester Rückfall + und keine Ableitung: Alle neun Dokumente sind von DogFather + hochgeladen, an ihnen hängt kein zweiter Mensch — über die + Personen gäbe es NIE einen Beleg, und die Zeilen blieben auf + Dauer offen. Der Rückfall beantwortet das ein für alle Mal. + Käme morgen ein Dokument von einem Creator, zeigte die + Ableitung darüber ohnehin schon auf die Agentur. */ + ["wissen", (r) => [r.erstellt_von].filter((x) => x != null), "titel", + () => "agentur"], + ["material", (r) => [r.von_id].filter((x) => x != null), "dateiname"], + ["termin_serien", (r) => [r.creator_id, r.teilnehmer_id, r.erstellt_von] + .filter((x) => x != null), "titel"], + ]; + + /* DIE EINTRAEGE ZUERST -- die Dateien erben von ihnen. Stuenden + sie wie vorher danach, liefe die Vererbung ins Leere, und drei + Dateien blieben offen, obwohl der Beleg dagewesen waere. Eine + Reihenfolge, die man einer Schleife nicht ansieht: deshalb steht + sie hier als Satz und nicht nur als Position. */ + eintraegeNachHaus(d, meldung); + restzeilenZuordnen(d, meldung); + + for (const [tabelle, leute, titelSpalte, ausEltern] of TAFEL) { + let spalten; + try { + spalten = d.prepare(`PRAGMA table_info(${tabelle})`).all().map((x) => x.name); + } catch { continue; } + if (!spalten.includes("haus")) continue; + const schreib = d.prepare(`UPDATE ${tabelle} SET haus = ? WHERE id = ?`); + for (const zeile of d.prepare( + `SELECT * FROM ${tabelle} WHERE haus IS NULL`).all()) { + /* ZUERST DIE MENSCHEN, DANN DIE ELTERN. Ein Mensch ist der + unmittelbarere Beleg; erst wenn keiner etwas sagt, wird + gefragt, woran die Zeile haengt. */ + const h = hausAus(leute(zeile)) || (ausEltern ? ausEltern(zeile) : null); + const wer = `${tabelle}#${zeile.id} „${zeile[titelSpalte] || "ohne Titel"}"`; + if (h === "crew" || h === "agentur") { schreib.run(h, zeile.id); meldung.gesetzt++; } + else if (h === "konflikt") meldung.konflikt.push(wer); + else meldung.offen.push(wer); + } + } + + if (meldung.gesetzt) { + console.log(`[workspace] Haus nachgetragen: ${meldung.gesetzt} Zeilen.`); + } + /* DER DRITTE AUSGANG. Nicht "in Ordnung", nicht "kaputt", sondern + "konnte nicht nachsehen" -- mit Grund und mit Namen, damit ein + Mensch entscheiden kann. Eine Zeile ohne Haus verschwindet nicht: + Sie ist weiterhin für DogFather sichtbar und für sonst niemanden + (siehe hausWo). */ + for (const x of meldung.konflikt) { + console.log(`[workspace] Haus UNKLAR (beide Häuser beteiligt): ${x}`); + } + for (const x of meldung.offen) { + console.log(`[workspace] Haus OFFEN (nur DogFather beteiligt): ${x}`); + } + } catch (fehler) { + console.error("[workspace] Häuser nachtragen:", fehler?.message); + } + return meldung; +} + +/** Das Haus eines Eintrags -- aus seinem BEREICH, nicht aus dem + * Ersteller. `regeln`, `anschlag`, `highlight` sind Bretter von Team + * Dogi, `agentur` gehoert der Agentur; das steht fest, unabhaengig + * davon, wer gerade hineingeschrieben hat. Bei 20 von 26 Eintraegen + * ist der Ersteller DogFather und damit kein Beleg. */ +function eintraegeNachHaus(d, meldung) { + try { + const spalten = d.prepare("PRAGMA table_info(eintraege)").all().map((x) => x.name); + if (!spalten.includes("haus")) return; + const schreib = d.prepare("UPDATE eintraege SET haus = ? WHERE id = ?"); + for (const zeile of d.prepare( + "SELECT id, bereich FROM eintraege WHERE haus IS NULL").all()) { + schreib.run(MODI_BEREICHE_ERLAUBT.has(zeile.bereich) ? "crew" : "agentur", zeile.id); + meldung.gesetzt++; + } + } catch (f) { console.error("[workspace] Häuser (Einträge):", f?.message); } +} + +/* ===================================================================== + DIE VIER ZEILEN, DIE KEIN BELEG ERREICHT (24.09.2026) + ===================================================================== + + Nach allen Ableitungen blieben am echten Stand genau vier Zeilen + ohne Haus: drei Termine und ein Bild. An ihnen hängt nur DogFather, + und er wohnt in beiden Häusern — es GIBT dort nichts abzuleiten. + + WARUM SIE NAMENTLICH DASTEHEN UND NICHT ÜBER EINE REGEL LAUFEN. + Eine Regel bräuchte eine Annahme („was DogFather allein einträgt, + ist Team Dogi"), und die wäre für „Manager Meeting" und „Call mit + Cigdem" (Cigdem ist Spicy Media) falsch. Eine falsche Annahme, die + auf 250 Zeilen läuft, verschiebt Dinge, die niemand sucht. Vier + benannte Zeilen kann man nachlesen und widerrufen. + + UND SIE SIND DOPPELT GESICHERT: Nummer UND Titel müssen passen. + Trifft eine Datenbank die Nummer mit einem anderen Titel (eine + Kopie, ein anderer Stand), passiert nichts — statt dass ein + fremder Termin stillschweigend das Haus wechselt. + + Läuft genau einmal: danach ist `haus` gesetzt und die Bedingung + `haus IS NULL` findet nichts mehr. + ===================================================================== */ +function restzeilenZuordnen(d, meldung) { + /* ==== DER EINE KANAL, IN DEM BEIDE HAEUSER SASSEN (24.09.2026) ==== + + „Chat-Moderation": DogFather, die rechte und die linke Hand, vier + Modis -- und zwei Scouts. Der einzige Raum im ganzen Haus, der + ueber die Grenze ging (0 von 86 Terminen, 0 von 7 uebrigen + Raeumen taten das). + + Filipes Entscheidung: „Scouts raus, Kanal bleibt Team Dogi." + + GEMESSEN, BEVOR ENTSCHIEDEN WURDE: Die sieben Nachrichten in + diesem Kanal sind ALLE vom Team (VanVan 4x, Marina 2x, Diene 1x). + Die beiden Scouts haben dort nie geschrieben; sie kamen am + 11.09.2026 in derselben Sekunde hinein wie DogFather und VanVan, + also beim automatischen Anlegen -- bevor KANAL_ROLLEN auf die + Team-Rollen eingeschraenkt wurde. Es geht damit nichts verloren. + + `raus_am` UND NICHT LOESCHEN: Die Zeile bleibt stehen und traegt + ein Datum. Wer spaeter fragt, warum jemand nicht mehr im Kanal + ist, findet eine Antwort statt einer Luecke. */ + try { + const raum = d.prepare( + "SELECT id, haus FROM chat_raeume WHERE id = 3 AND name = 'Chat-Moderation'").get(); + if (raum && !raum.haus) { + const fremd = [...nurAnderesHaus("crew")].map((r) => `'${r}'`).join(", "); + const n = d.prepare( + `UPDATE chat_teilnehmer SET raus_am = ? + WHERE raum_id = 3 AND raus_am IS NULL + AND person_id IN (SELECT id FROM personen WHERE rolle IN (${fremd}))`) + .run(new Date().toISOString()).changes; + d.prepare("UPDATE chat_raeume SET haus = 'crew' WHERE id = 3").run(); + meldung.gesetzt++; + console.log(`[workspace] Kanal „Chat-Moderation" -> Team Dogi, ${n} aus dem anderen Haus herausgenommen.`); + } + } catch (f) { console.error("[workspace] Kanal 3:", f?.message); } + + const ZUORDNUNG = [ + ["termine", 3, "Manager Meeting", "agentur"], + ["termine", 5, "GrundRegeln-Schulung", "crew"], + ["termine", 179, "Call mit Cigdem", "agentur"], + ["material", 1, "community.png", "crew"], + ]; + for (const [tabelle, id, titel, haus] of ZUORDNUNG) { + try { + const spalte = tabelle === "material" ? "dateiname" : "titel"; + const n = d.prepare( + `UPDATE ${tabelle} SET haus = ? WHERE id = ? AND ${spalte} = ? AND haus IS NULL`) + .run(haus, id, titel).changes; + if (n) { + meldung.gesetzt += n; + console.log(`[workspace] Haus von Hand gesetzt: ${tabelle}#${id} „${titel}" -> ${haus}`); + } + } catch (f) { + console.error(`[workspace] Restzeile ${tabelle}#${id}:`, f?.message); + } + } +} + +/** Woran haengt diese Datei -- und welches Haus hat das? */ +function hausVomEltern(d, zeile) { + try { + if (zeile.eintrag_id) { + const e = d.prepare("SELECT haus FROM eintraege WHERE id = ?").get(zeile.eintrag_id); + if (e?.haus) return e.haus; + } + if (zeile.aufgabe_id) { + const a = d.prepare("SELECT haus FROM aufgaben WHERE id = ?").get(zeile.aufgabe_id); + if (a?.haus) return a.haus; + } + } catch { /* kein Beleg ist kein Fehler -- dann bleibt es offen */ } + return null; +} + function umstellungen(d) { /* MIT SEKUNDEN, nicht nur mit Minuten (07.09.2026). @@ -3719,6 +4034,46 @@ function umstellungen(d) { einer Tabelle. Ein REFERENCES darauf gaebe es nicht; geprueft wird beim Schreiben, an der Stelle, die den Katalog kennt. */ ["aufgaben", "aus_punkt", "TEXT"], + + /* ==== ZU WELCHEM HAUS GEHOERT DIESE ZEILE? (24.09.2026) ========== + + Filipe: „wenn ich bei der einen was mache soll nichts bei der + anderen passieren." + + WARUM EINE SPALTE UND NICHT EINE RECHNUNG. Bis heute wurde das + Haus aus den beteiligten Personen ABGELEITET (ohneAgentur / + ohneTeamDogi). Das trägt für jede Zeile, an der mindestens ein + Mensch aus genau einem Haus hängt -- und es trägt NICHT für die + Zeilen, an denen nur DogFather hängt. Er wohnt in beiden Häusern; + seine Zeilen standen deshalb in beiden. Gemessen am echten Stand: + 57 von 89 Terminen sind von ihm, 20 von 26 Einträgen. + + Eine Rechnung, die in 30 % der Fälle zwei Antworten gibt, ist + keine Grenze. Deshalb steht das Haus jetzt DA, geschrieben in dem + Moment, in dem die Zeile entsteht -- und zwar aus `person.haus`, + also aus der Adresse, auf der gearbeitet wurde. + + NULL IST ERLAUBT und heisst „noch nicht nachgetragen". Die + Nachtragung (haeuserNachtragen) füllt es aus dem vorhandenen + Bestand; was sie nicht eindeutig bestimmen kann, sagt sie laut, + statt zu raten. + + ALTER TABLE ADD COLUMN UND KEIN TABELLENUMBAU: Am 11.09.2026 hat + ein Neubau der Tabelle `eintraege` drei Spalten samt Inhalt + verschluckt, ohne Fehlermeldung. ADD COLUMN kann das nicht. */ + ["chat_raeume", "haus", "TEXT"], + ["termine", "haus", "TEXT"], + ["aufgaben", "haus", "TEXT"], + ["eintraege", "haus", "TEXT"], + ["dateien", "haus", "TEXT"], + ["wissen", "haus", "TEXT"], + ["material", "haus", "TEXT"], + /* Die WIEDERHOLUNGSREGEL braucht es auch: Der Nachfueller erzeugt + daraus Termine, und die muessen wissen, wohin sie gehoeren. Ohne + sie erbten sie `null` -- und ein `null` ist in beiden Haeusern + sichtbar (siehe hausWo). Eine woechentliche Serie haette die + Trennung damit Woche fuer Woche neu unterlaufen. */ + ["termin_serien", "haus", "TEXT"], ]) { try { const vorhanden = d.prepare(`PRAGMA table_info(${tabelle})`).all().map((s) => s.name); @@ -3731,6 +4086,11 @@ function umstellungen(d) { } } + /* Die Häuser nachtragen -- direkt nachdem die Spalte da ist. Steht + der Aufruf weiter unten, läuft er beim allerersten Start auf eine + Tabelle ohne Spalte und meldet einen Fehler, den niemand braucht. */ + haeuserNachtragen(d); + /* Der Nachfüller fragt bei jedem Lauf "welche Ausprägungen dieser Serie gibt es schon?". Ohne diesen Verbund-Index liest SQLite dafür die ganze Termintabelle. Er steht hier unten und nicht oben im @@ -5651,8 +6011,38 @@ export function sitzungLesen(req) { 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. */ + /* ==== PRUEFADRESSEN BEKOMMEN KEIN HAUS (24.09.2026) =========== + + Hier stand `istCrewAdresse(...) ? "crew" : "agentur"` -- also + bekam auch `127.0.0.1` ein Haus, naemlich "agentur". Das war + harmlos, solange auf der Agenturseite ohnehin nicht gefiltert + wurde. Seit die Grenze in BEIDE Richtungen gilt, ist es das + Gegenteil von harmlos: + + GEMESSEN am selben Nachmittag: `pruef-chat-kanaele` fiel mit 13 + Fehlschlaegen um. Nicht weil etwas kaputt war, sondern weil + DogFather auf 127.0.0.1 ploetzlich im Agenturhaus sass und + deshalb keinen Modi mehr in einen Kanal einladen durfte. Die + naechsten Pruefungen waeren still gruen geblieben und haetten + nichts mehr gemessen. + + GENAU DAVOR WARNT crew-adresse.js DREIMAL (Falle 2 in ihrem + Kopf): "Ein gruener Lauf, der nichts mehr prueft, ist schlimmer + als ein roter." Ich bin trotzdem hineingelaufen -- der Kommentar + stand da, gelesen habe ich ihn erst, als es rot wurde. + + `null` IST DER DRITTE WERT, und alle Hausfunktionen kennen ihn + schon: nurHaus, hausBedingung, hausWo und ohneAnderesHaus + filtern nur bei "crew" oder "agentur". Eine Pruefadresse + verhaelt sich damit exakt wie vor dem Umbau. + + EINE ERFUNDENE ADRESSE BLEIBT AGENTUR. istPruefAdresse zaehlt + localhost und 127.* auf -- nicht "alles, was nicht crew ist". + `crew.boese.de` faellt also weiterhin ins Agenturhaus und + oeffnet nichts Drittes. */ const haus = istCrewAdresse(req.get?.("host")) ? "crew" - : "agentur"; + : istPruefAdresse(req.get?.("host")) ? null + : "agentur"; /* DER AUSSCHLUSS WIRKT HIER -- an derselben Stelle wie alles andere (11.09.2026). @@ -7275,9 +7665,44 @@ export function termineSichtbar(person, praefix = "t") { Adresse mit dem Team, und die Termine der Agentur gehen sie dort nichts an. Geprueft wird die ROLLE und nicht der Name -- ein Name waere die Stelle, an der es beim naechsten Menschen bricht. */ - if (person.haus === "crew" && person.rolle !== "admin") { + /* ==== DIE AUSNAHME FUER DOGFATHER IST UMGEBAUT (24.09.2026) ======= + + Hier stand `person.rolle !== "admin"` -- also: Jeder auf der + Team-Adresse sieht nur Team-Termine, DogFather aber ALLE, quer + ueber beide Haeuser. Das war sein Wunsch vom 11.09.2026, mit + einer Begruendung, die weiterhin richtig ist: Wer die Haelfte + seines Tages nicht sieht, legt einen Call auf eine Zeit, in der + er schon woanders sitzt. + + Am 24.09.2026 hat er die Trennung verlangt -- und auf die + Nachfrage, was mit dieser Ausnahme geschieht, entschieden: + „Getrennt, aber ‚belegt' bleibt sichtbar." + + DAS IST NICHT DERSELBE WUNSCH ZWEIMAL, SONDERN ZWEI VERSCHIEDENE + ANSPRUECHE AN DIESELBE ANZEIGE: Der INHALT gehoert getrennt (kein + fremder Titel, kein fremder Teilnehmer auf der falschen Seite), + die ZEIT nicht (sonst doppelt man sich). Beides geht: Hier wird + hart getrennt, und die Kalenderroute legt die fremden Zeiten + zusaetzlich als titellose „belegt"-Bloecke daneben. Nur fuer die + DogFather-Rolle, wie 2026-09-11 festgelegt -- VanVan sieht + weiterhin nichts von der anderen Seite. + + ZWEI BEDINGUNGEN UND NICHT EINE: `hausWo` fragt die Spalte (sie + weiss es auch bei einem Termin, an dem nur DogFather haengt -- + gemessen 57 von 89), `ohneAnderesHaus` fragt die Beteiligten (sie + greift auch dort, wo noch kein Haus eingetragen ist). Wer nur eine + nimmt, hat je nach Zeile eine Luecke. + + SERIEN HABEN KEINE HAUS-SPALTE (`praefix === "s"` zeigt auf + termin_serien). Dort bleibt es bei der Ableitung ueber die + Beteiligten -- eine Bedingung auf eine Spalte, die es nicht gibt, + waere ein SQL-Fehler und damit eine leere Seite. */ + const haus = person.haus; + if (haus === "crew" || haus === "agentur") { + const spalten = ["creator_id", "teilnehmer_id", "erstellt_von"]; + const ausSpalte = p === "t" ? ` AND ${hausWo(person, p)}` : ""; return { - wo: `(${eigen}) AND ${ohneAgentur(p, ["creator_id", "teilnehmer_id", "erstellt_von"])}`, + wo: `(${eigen})${ausSpalte} AND ${ohneAnderesHaus(p, person, spalten)}`, werte, }; } @@ -7627,12 +8052,36 @@ export function protokollLesen(anzahl = 20) { * -- und das sagt bei der Community ueberall Nein. Zwei verschiedene * Fragen, zwei verschiedene Funktionen; wer sie in eine legt, kann * spaeter nicht mehr sagen, welche gerade zugeschlagen hat. */ -const HAUS_TEAM_ROLLEN = new Set(["admin", "hand", "linke", "modi", "gast"]); +/* BIS ZUM 24.09.2026 STAND HIER EINE EIGENE MENGE + (`admin, hand, linke, modi, gast`). Sie war wortgleich mit dem, was + `rollenImHaus("crew")` liefert -- und damit die zweite Liste, die + beim naechsten Rollenwechsel haette nachgezogen werden muessen. Sie + ist weg; gefragt wird jetzt die eine Menge in crew-adresse.js. */ +/* ===================================================================== + DIE GRENZE GILT IN BEIDE RICHTUNGEN (24.09.2026) + + Bis heute stand hier `if (person?.haus !== "crew") return ids;` -- + also: Auf der Team-Adresse wird gefiltert, auf der Agenturadresse + NICHT. Das war richtig, solange „getrennt wird das Aussehen, nicht + der Bestand" galt. Filipe hat das am 24.09.2026 umgedreht: „wenn ich + bei der einen was mache soll nichts bei der anderen passieren." + + WAS DIE EINSEITIGKEIT KONKRET BEDEUTET HAT: DogFather arbeitet in + beiden Haeusern. Auf crew. sah er nur Team Dogi -- auf + workspace.dogfather-universe.com aber ALLES, Team Dogi eingerechnet. + In seiner Gespraechsliste standen dort die Raeume beider Haeuser + nebeneinander, ohne dass etwas sie unterschieden haette. + + EIN UNBEKANNTES HAUS FILTERT NICHT, und das ist Absicht: Auf einer + Pruefadresse setzt sitzungLesen() „agentur"; ein drittes Haus gibt + es nicht. Stuende hier ein `else` mit einer Annahme, waere die + naechste Adresse stillschweigend einem Haus zugeschlagen. */ function nurHaus(ids, person) { - if (person?.haus !== "crew") return ids; + const haus = person?.haus; + if (haus !== "crew" && haus !== "agentur") return ids; try { - const liste = [...HAUS_TEAM_ROLLEN].map((r) => `'${r}'`).join(", "); + const liste = [...rollenImHaus(haus)].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 @@ -7664,11 +8113,48 @@ function nurHaus(ids, person) { 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(", "); + const haus = person?.haus; + if (haus !== "crew" && haus !== "agentur") return ""; + const liste = [...rollenImHaus(haus)].map((r) => `'${r}'`).join(", "); return ` AND ${spalte} IN (${liste})`; } +/* ===================================================================== + DAS HAUS EINER ZEILE -- ALS SQL-BEDINGUNG (24.09.2026) + + Fuer die sieben Tabellen mit eigener `haus`-Spalte (chat_raeume, + termine, aufgaben, eintraege, dateien, wissen, material). + + WARUM DIE SPALTE UND NICHT DIE PERSONEN. Die Ableitung ueber die + Beteiligten (ohneAgentur / ohneTeamDogi) traegt fuer jede Zeile, an + der mindestens ein Mensch aus genau einem Haus haengt -- und sie + traegt NICHT fuer die Zeilen, an denen nur DogFather haengt. Gemessen + am echten Stand: 57 von 89 Terminen. Die standen in beiden Haeusern. + + `haus IS NULL` KOMMT MIT, und das ist eine bewusste Uebergangsregel: + Beim Nachtragen bleiben Zeilen offen, deren Haus sich aus dem + Bestand nicht belegen laesst (gemessen: 14 von 252). Wuerden sie + herausfallen, verschwaende die halbe Wissensablage, ohne dass jemand + einen Fehler saehe -- die unangenehmste Sorte Datenverlust. Sie + bleiben deshalb sichtbar, und `haeuserNachtragen` nennt jede einzeln + beim Namen, damit sie zugeordnet werden koennen. Ist die Liste leer, + hat diese Zeile keine Wirkung mehr. + + @param {string} praefix Tabellenkuerzel im SQL, z.B. "t" fuer termine + ===================================================================== */ +export function ohneAnderesHaus(praefix, person, spalten) { + const haus = person?.haus; + if (haus !== "crew" && haus !== "agentur") return "1=1"; + return ohneRollen(praefix, spalten, nurAnderesHaus(haus)); +} + +export function hausWo(person, praefix) { + const haus = person?.haus; + if (haus !== "crew" && haus !== "agentur") return "1=1"; + const p = praefix ? `${praefix}.` : ""; + return `(${p}haus = '${haus}' OR ${p}haus IS NULL)`; +} + /* EINE LISTE VON CREATOR-NUMMERN DARF NUR CREATOR ENTHALTEN (11.09.2026). Klingt selbstverstaendlich, war es nicht. `ohneVerborgene` schreibt diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index 65e74f4d..91b81838 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,15 +16,15 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +