Filipe: "die kachel von der kategorie, reaktion, soll nach der kachel, dogi-move, erscheinen bitte und danke." EINE KACHEL "DOGI-MOVE" GIBT ES NICHT -- das Wort kommt im ganzen Haus nicht vor. Die einzige, die passt, ist "Dogi-Media" (Bilder und Videos zum Posten). Dorthin ist sie gesetzt; sollte etwas anderes gemeint gewesen sein, ist es eine Zeile zurueck. Die Reihenfolge der Community-Kacheln ist damit: Willkommen - Rudel-Chat - Highlights - Anschlagbrett - Was ansteht - Wunschliste - Dogi-Media - REACTION - Draussen Geprueft: reaktion 132, kachelraster 24, kachel-universum 13 -- alle 0 Fehler. Die angefangene Arbeit an der Host-Steuerung (Kamera- und Chat-Spalten) bleibt bewusst im Arbeitsstand: Sie ist unfertig und hat in dieser Lieferung nichts zu suchen.
8623 lines
416 KiB
JavaScript
8623 lines
416 KiB
JavaScript
/* =====================================================================
|
||
workspace.js — Anmeldung und Sitzungen für den Creator Workspace
|
||
(/workspace). Eigenständiger Router, nach dem Muster von gate.js und
|
||
webdesign-gate.js.
|
||
|
||
BEWUSST OHNE NEUE ABHÄNGIGKEITEN. Node 24 bringt `node:sqlite` mit, das
|
||
Hashen und die Zufallswerte kommen aus `node:crypto`. Damit gibt es
|
||
nichts zu kompilieren, nichts zu aktualisieren und keine fremde
|
||
Lieferkette in einem Bereich, in dem es um Zugangsdaten geht.
|
||
|
||
WICHTIG — dieses Modul darf die Website niemals mitreißen. Es läuft im
|
||
selben Prozess wie dogfather-universe.com. Deshalb:
|
||
* beim Laden wird KEINE Datenbank geöffnet (siehe `db()` weiter unten),
|
||
* jede Route fängt ihre Fehler selbst ab,
|
||
* schlägt die Datenbank fehl, antwortet nur /workspace/api/* mit 503 —
|
||
die restliche Seite merkt davon nichts.
|
||
|
||
Sicherheitsentscheidungen und ihre Gründe stehen jeweils direkt an der
|
||
betreffenden Stelle.
|
||
===================================================================== */
|
||
|
||
import express from "express";
|
||
import {
|
||
randomBytes, scryptSync, timingSafeEqual, createHash, createHmac,
|
||
} from "node:crypto";
|
||
import { dirname, join } from "node:path";
|
||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||
import { mkdirSync } from "node:fs";
|
||
import { createRequire } from "node:module";
|
||
import { darfSeite, OHNE_ANMELDUNG, seitenFuer } from "./rechte.js";
|
||
/* Ein Modul ohne eigene Importe -- deshalb entsteht hier kein Kreis,
|
||
obwohl workspace-treff.js seinerseits aus dieser Datei importiert.
|
||
Begruendung im Kopf von treff-tabellen.js. */
|
||
import { treffTabellen, treffSperreLesen, MINDESTALTER } from "./treff-tabellen.js";
|
||
import { hilfeTabellen } from "./hilfe-tabellen.js";
|
||
import { supportTabellen } from "./support-tabellen.js";
|
||
import { notizTabellen } from "./notiz-tabellen.js";
|
||
import { reaktionTabellen } from "./reaktion-tabellen.js";
|
||
import { unterstuetzungTabellen } from "./unterstuetzung-tabellen.js";
|
||
import { sitzungPasstZurAdresse, istOhneModiAdresse, istCrewAdresse, istPruefAdresse, AUSSEN_ROLLEN,
|
||
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, AGENTUR_ROLLEN, BEIDE_HAEUSER_ROLLEN,
|
||
hausVonRolle, rollenImHaus, nurAnderesHaus };
|
||
|
||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||
|
||
/* `node:sqlite` wird bewusst NICHT oben importiert, sondern erst in db()
|
||
nachgeladen. Ein fehlgeschlagener Import an dieser Stelle würde den
|
||
gesamten Website-Prozess beim Start abbrechen -- also auch
|
||
dogfather-universe.com. So bleibt der Schaden auf /workspace/api/
|
||
beschränkt. (Vorhanden ab Node 22; auf dem Server läuft Node 24.) */
|
||
const require = createRequire(pathToFileURL(__dirname + "/"));
|
||
|
||
/* Die Datenbank liegt bewusst AUSSERHALB des Repo-Ordners. Zwei Gründe:
|
||
express.static liefert den Repo-Ordner aus (eine .db darin wäre über das
|
||
Netz erreichbar), und ein `git pull` darf Nutzdaten nie anfassen. */
|
||
/* Wird ausgegeben, weil die Sicherung sowohl die Datei selbst als auch
|
||
ihr WAL nachschlagen muss. Aus DATEN_ORDNER + "workspace.db" laesst
|
||
sich das NICHT ableiten: Steht WORKSPACE_DB auf einem anderen Namen
|
||
(so laufen alle Pruefungen), zeigte die Sicherung auf eine Datei, die
|
||
es gar nicht gibt -- und meldete brav 0 Bytes. */
|
||
export const DB_PFAD = process.env.WORKSPACE_DB
|
||
|| join(__dirname, "..", "..", "workspace-daten", "workspace.db");
|
||
|
||
/* Ordner fuer hochgeladene Dateien -- neben der Datenbank, also ebenfalls
|
||
ausserhalb des Repos. Waeren sie im Repo, wuerde express.static sie
|
||
ungeschuetzt ausliefern. */
|
||
export const DATEN_ORDNER = dirname(DB_PFAD);
|
||
|
||
const COOKIE = "dfw_sitzung";
|
||
/* ===== WIE LANGE GILT EINE ANMELDUNG? (20.09.2026) ==================
|
||
|
||
Filipe: "kannst du bitte machen dass die leute sich nur einmal
|
||
anmelden muessen und dan nur noch abgemeldet werden wenn sie sich
|
||
selbst abmelden. damit sie auch die benarichtigungen sofort kriegen
|
||
... und auch sofort die anrufe annehmen koennen."
|
||
|
||
VORHER: zwoelf Stunden, feste Frist ab dem Anmelden. Wer morgens um
|
||
acht anfing, flog abends um acht raus -- mitten im Betrieb. Und wer
|
||
abgemeldet ist, hat die Seite nicht offen; ein Anruf erreicht ihn
|
||
dann nur noch ueber die Benachrichtigung, und bis er sich wieder
|
||
angemeldet hat, ist das Klingeln vorbei.
|
||
|
||
JETZT: ein GLEITENDES Fenster. Wer die Seite benutzt, bleibt
|
||
angemeldet -- ohne Ende, ohne Nachfrage. Abgemeldet wird nur, wer
|
||
sich selbst abmeldet.
|
||
|
||
WARUM NICHT "unendlich": Ein Zugang, der nie ablaeuft, ist auf einem
|
||
verlorenen Handy fuer immer offen -- und in diesem Haus liegen
|
||
vertrauliche Meldungen ueber Menschen. Wer ein halbes Jahr nicht da
|
||
war, meldet sich neu an; das ist niemandem zuzumuten zu viel und
|
||
schliesst das verlorene Geraet.
|
||
|
||
WARUM NICHT BEI JEDEM AUFRUF NEU SCHREIBEN: Das waere ein
|
||
Schreibzugriff auf die Datenbank bei jedem Bild, jeder Abfrage,
|
||
jedem Herzschlag des Ereignisstroms. Verlaengert wird erst, wenn
|
||
weniger als die Haelfte des Fensters uebrig ist -- also hoechstens
|
||
einmal alle drei Monate je Geraet.
|
||
==================================================================== */
|
||
const SITZUNG_TAGE = 180;
|
||
const SITZUNG_MS = SITZUNG_TAGE * 86400_000;
|
||
const VERSUCHE_MAX = 8; // pro IP
|
||
const VERSUCHE_FENSTER_MIN = 10;
|
||
const ROLLEN = new Set(["spicy", "admin", "manager", "scout", "creator",
|
||
"hand", "linke", "modi", "gast"]);
|
||
|
||
/* ===== DIE LINKE HAND (21.09.2026) ==================================
|
||
|
||
Filipe, aus dem Auftrag: *"Die linke Hand erhaelt grundsaetzlich
|
||
dieselben Rechte wie die rechte Hand in allen Bereichen, die die
|
||
Modis, Aufgaben, Aufgabenzuweisungen, Uebersichten, Kommunikation
|
||
und Auswertungen betreffen. Die linke Hand darf keine neuen Personen
|
||
oder Accounts hinzufuegen. Die linke Hand darf keine privaten bzw.
|
||
persoenlichen Bereiche oder Informationen von Dogfather sehen."*
|
||
|
||
DESHALB EINE MENGE UND NICHT DREISSIG ABSCHRIFTEN. Die Rolle "hand"
|
||
steht an 35 Stellen im Servercode. Haette ich ueberall ein "linke"
|
||
danebengeschrieben, waere die naechste Stelle, die jemand
|
||
hinzufuegt, die erste, die es vergisst -- und eine vergessene Stelle
|
||
heisst hier: Die linke Hand sieht etwas nicht, ohne dass jemand
|
||
merkt, warum. Genau diese Falle hat das Haus am 11.09. schon einmal
|
||
Datenspalten gekostet.
|
||
|
||
WAS AUSDRUECKLICH NICHT DAZUGEHOERT -- und warum jedes einzeln:
|
||
|
||
- Personen anlegen (ANLEGBAR) ausdruecklich im Auftrag
|
||
- Bewerbungen lesen das ist der Weg, auf dem
|
||
Personen hinzukommen
|
||
- Rollen aendern Kontoverwaltung, nicht
|
||
Aufgabenverwaltung
|
||
- der vertrauliche Meldeweg das Empfindlichste im Haus;
|
||
im Zweifel das kleinere Recht.
|
||
Ein Wort von Filipe kehrt das
|
||
um -- eine versehentlich
|
||
gelesene Meldung nicht.
|
||
|
||
Die Menge steht hier oben, VOR allem, was sie benutzt. */
|
||
export const WIE_RECHTE_HAND = new Set(["hand", "linke"]);
|
||
|
||
/** Hat diese Person die Team-Rechte einer Hand (rechts oder links)? */
|
||
export const istHand = (person) => !!person && WIE_RECHTE_HAND.has(person.rolle);
|
||
|
||
/* ===== WER FUEHRT DIE ZUGAENGE? (25.09.2026) ========================
|
||
|
||
Filipe, zum wiederholten Mal: „die rechte hand sieht das immer noch
|
||
nicht obwohl ich will dass die rechte hand das auch sieht." Dazu ein
|
||
Bildschirmfoto der Personenseite mit den Rollenkarten und „Letzte
|
||
Ereignisse".
|
||
|
||
NACHGEMESSEN, NICHT GERATEN (mess-hand-personen.mjs): Sie bekam vom
|
||
Server alle acht Personen (HTTP 200) -- und sah auf dem Bildschirm
|
||
NICHTS davon. Zwei Stellen hielten sie auf, keine davon eine
|
||
Sicherung:
|
||
|
||
1. personen.js entschied die Ausbaustufe mit `ich.rolle !== "admin"`.
|
||
Ein Rollenname im Browser -- genau die zweite Wahrheit, die in
|
||
dieser Datei am 22. und am 24.09. schon zweimal veraltet ist.
|
||
2. personen.css blendet bei `data-nur-anlegen` die Liste aus. Die
|
||
Regel stammt vom 07.09. und war fuer Manager und Spicy Media
|
||
gedacht; die rechte Hand ist erst danach dazugekommen und fiel
|
||
stillschweigend mit hinein.
|
||
|
||
Ihr Protokoll war zusaetzlich am Server zu (HTTP 404).
|
||
|
||
WARUM ES EINE EIGENE FUNKTION IST UND KEIN ROLLENVERGLEICH: Dieselbe
|
||
Frage wird an DREI Stellen gestellt -- die Tuer zum Protokoll, die
|
||
Auskunft an die Oberflaeche und die Ausbaustufe der Seite. Drei
|
||
Abschriften waeren drei Gelegenheiten, dass die naechste Aenderung
|
||
nur zwei davon trifft. Genau so ist dieser Fehler entstanden.
|
||
|
||
`istHand` WAERE FALSCH. Es fasst beide Haende zusammen, und fuer die
|
||
linke gilt ausdruecklich das Gegenteil: „Kann aber keine Personen
|
||
anlegen, keine Rollen aendern und sieht weder Bewerbungen noch den
|
||
vertraulichen Meldeweg." Wer hier den Sammelbegriff nimmt, dreht
|
||
eine ausgesprochene Entscheidung stillschweigend um. */
|
||
export const fuehrtDieZugaenge = (person) =>
|
||
!!person && (person.rolle === "admin" || person.rolle === "hand");
|
||
|
||
/* =====================================================================
|
||
WER EINE KATALOG-VORLAGE BEKOMMEN KANN (24.09.2026)
|
||
=====================================================================
|
||
|
||
Filipe: „wenn ich eine aufgabe an alle verteile will ich dass
|
||
dogfather und die rechte hand individuel von jedem sehen wer es
|
||
gemacht hat oder nicht."
|
||
|
||
WARUM DIESE MENGE HIER STEHT UND NICHT DREIMAL VERSTREUT. Bis heute
|
||
gab es zwei Listen, die dasselbe meinten und es nicht taten:
|
||
|
||
* Die „An wen"-Reihe im Browser bekam ihre Leute aus
|
||
`KARTEN_ROLLEN` (hand, linke, modi) -- gemessen am echten Stand
|
||
SECHS Personen.
|
||
* Der Katalog-Weg im Server nahm `rolle = 'modi'` -- VIER.
|
||
|
||
Folge: „Alle" versprach sechs und belieferte vier; die beiden
|
||
Haende gingen leer aus, ohne ein Wort. Und wer einzeln auf „An
|
||
Rechte Hand" drueckte, bekam vom Server 404 und auf dem Bildschirm
|
||
„Das gibt es nicht mehr -- wahrscheinlich hat es jemand geloescht".
|
||
Eine Meldung, die in die Irre fuehrt: Die Person gibt es sehr wohl.
|
||
|
||
ES IST EINE MENGE UND KEINE ABSCHRIFT. Wer sie erweitert, erweitert
|
||
damit die SQL-Abfrage, die Annahme-Pruefung und die Liste im
|
||
Browser zugleich -- die drei koennen gar nicht mehr auseinander-
|
||
laufen. Genau das war der Fehler, nicht die Zahl.
|
||
|
||
WARUM MODI UND LINKE HAND. Filipes Regel vom 22.09.2026: „die modis
|
||
und linke hand sollen ... sich fuer aufgaben bewerben koennen aber
|
||
die rechte hand oder dogfather muessen annehmen oder ablehnen".
|
||
Also sind das die beiden Rollen, die Aufgaben BEKOMMEN -- und
|
||
DogFather und die rechte Hand die, die verteilen und zusehen. */
|
||
export const KATALOG_EMPFAENGER = new Set(["modi", "linke"]);
|
||
|
||
/** Kann diese Rolle eine Vorlage aus dem Katalog bekommen? */
|
||
export const darfKatalogBekommen = (rolle) => KATALOG_EMPFAENGER.has(rolle);
|
||
|
||
/** Die Rollen als SQL-Platzhalterliste -- abgeleitet, nicht getippt.
|
||
* Eine Abfrage, die die Namen selbst noch einmal enthielte, waere die
|
||
* vierte Liste und damit genau das Problem von vorn. */
|
||
export const KATALOG_EMPFAENGER_SQL = [...KATALOG_EMPFAENGER];
|
||
|
||
/* DIE EINE REIHENFOLGE, IN DER ROLLEN UEBERALL ERSCHEINEN.
|
||
|
||
Sie steht hier einmal, damit keine Liste eine eigene erfindet.
|
||
|
||
DIE RECHTE HAND STEHT AN ZWEITER STELLE (Filipe, 11.09.2026:
|
||
"rechte hand soll immer als erstes sein. ueberall. ... ausser bei
|
||
personen und zugaengen soll sie ueber jedem aber unter dogfather
|
||
sein. ich will dass auch ueberall immer nach der hirarchie
|
||
gearbeitet wird.").
|
||
|
||
EINE ZAHL STATT ZWEIER REGELN: Er hat zwei Saetze gesagt, aber es
|
||
braucht nur eine Reihenfolge. In den Team-Listen kommt DogFather
|
||
gar nicht vor (er beurteilt, er wird nicht beurteilt) -- dort steht
|
||
sie damit automatisch ganz vorn. In "Personen & Zugaenge" steht er
|
||
drin, also steht sie dort hinter ihm. Zwei Sonderfaelle waeren zwei
|
||
Stellen, an denen es auseinanderlaufen kann.
|
||
|
||
SPICY MEDIA BLEIBT DAVOR -- seine ausdrueckliche Entscheidung auf
|
||
Nachfrage am 11.09.2026. Die Agentur ist keine Stufe in seinem Team,
|
||
sondern steht daneben; an Rechten aendert die Reihenfolge ohnehin
|
||
nichts, sie sortiert nur Listen.
|
||
|
||
'gast' (Community) steht mit Absicht NICHT in der Liste: Sie faellt
|
||
dadurch ans Ende (siehe ELSE unten), und das ist richtig -- die
|
||
Community ist keine Stufe im Team. */
|
||
export const ROLLEN_REIHE = ["spicy", "admin", "hand", "linke", "manager",
|
||
"scout", "creator", "modi"];
|
||
|
||
/* DIE ROLLEN MIT EINER KACHEL AUF DER ANMELDESEITE.
|
||
|
||
'modi' steht hier NICHT drin, und das ist der Kern des verborgenen
|
||
Zugangs: Es gibt keine sechste Kachel, und es laesst sich auch keine
|
||
erzwingen. Wer von aussen `rolle: "modi"` schickt, bekommt genau
|
||
dieselbe Antwort wie bei einer erfundenen Rolle -- er erfaehrt also
|
||
nicht einmal, dass es sie gibt.
|
||
|
||
Zwei Listen statt einer, weil es zwei verschiedene Fragen sind:
|
||
ROLLEN sagt, welche Rollen es GIBT (die Datenbank laesst nur diese
|
||
zu). Diese hier sagt, mit welchen man sich ANMELDEN kann, indem man
|
||
sie anklickt. Waere es eine Liste, haette 'modi' entweder eine Kachel
|
||
-- oder es gaebe die Rolle gar nicht. */
|
||
const ROLLEN_KACHEL = new Set(["spicy", "admin", "manager", "scout", "creator"]);
|
||
|
||
|
||
/** Die Rollen mit einer Kachel auf der Zugangswand von crew.
|
||
*
|
||
* DREI, seit Filipe am 10.09.2026 sagte: "3 rollen. dogfather. rechte
|
||
* hand und modis." Auf dieser Adresse gibt es also wieder etwas zu
|
||
* waehlen -- anders als in der Fassung davor, wo nur Modis
|
||
* hereinkamen und eine Kachelreihe mit einem Eintrag eine Huerde
|
||
* gewesen waere.
|
||
*
|
||
* DogFather steht auf BEIDEN Waenden. Er ist die einzige Person, die
|
||
* in beiden Welten arbeitet; das ist keine Ausnahme von der Trennung,
|
||
* sondern ihr Sinn.
|
||
*
|
||
* VIER, SEIT DER TREFF HIER WOHNT (Entscheidung Filipe, 11.09.2026:
|
||
* "das soll keine app fuer sich sein, das soll in der crew seite
|
||
* adaptiert werden" -- und auf die Frage, wie die Community durch
|
||
* diese Wand kommt: "vierte Kachel Community").
|
||
*
|
||
* WAS DAS KOSTET, und es gehoert hierhin und nicht in eine Fussnote:
|
||
* Ein Mitglied der Community liest vor seiner Anmeldung die Namen der
|
||
* drei Team-Rollen. Die Alternative waere gewesen, die Kachelreihe
|
||
* ganz abzuschaffen und allein den Code entscheiden zu lassen -- dann
|
||
* haette niemand eine Rolle gesehen, aber Filipes Entscheidung vom
|
||
* 10.09. waere wieder umgeworfen worden. Er hat sich fuer die vierte
|
||
* Kachel entschieden; der Preis ist damit bewusst bezahlt und nicht
|
||
* uebersehen. */
|
||
/* Die linke Hand meldet sich ueber dieselbe Wand an wie die rechte. */
|
||
const CREW_KACHEL = new Set(["admin", "hand", "linke", "modi", "gast"]);
|
||
|
||
/** Die Rollen mit einer Kachel auf der Zugangswand von treff.
|
||
*
|
||
* EINE, und deshalb zeigt die Wand dort gar keine Kachelreihe: Eine
|
||
* Auswahl mit einem einzigen Eintrag ist keine Auswahl, sondern eine
|
||
* Huerde. Dieselbe Ueberlegung wie bei der ersten Fassung der
|
||
* Crew-Wand -- nur ist sie hier dauerhaft richtig, weil es im Treff
|
||
* auch kuenftig nur eine Rolle gibt.
|
||
*
|
||
* Die Menge steht trotzdem hier: Die Anmeldung prueft gegen sie, und
|
||
* eine Wand ohne Kacheln darf nicht heissen, dass jede Rolle
|
||
* durchkommt. */
|
||
|
||
|
||
/* Als SQL-Ausdruck fuer ORDER BY. "ORDER BY rolle" waere alphabetisch
|
||
(admin, creator, manager, scout, spicy) -- also fast genau falsch
|
||
herum.
|
||
|
||
ABGELEITET UND NICHT ABGESCHRIEBEN (11.09.2026). Bis heute stand die
|
||
Reihenfolge zweimal da: einmal als Liste, einmal als CASE mit
|
||
Zahlen. Beim Hochziehen der rechten Hand haetten beide geaendert
|
||
werden muessen -- und eine gepflegte Liste, die mit einer zweiten
|
||
uebereinstimmen muss, ist genau die Bauart, an der im Projekt schon
|
||
dreimal etwas verlorengegangen ist (zuletzt drei Spalten beim
|
||
Tabellenumbau). Eine Liste, die niemand pflegt, kann nicht veralten.
|
||
|
||
Das Wort "rolle" kommt hier genau EINMAL vor -- in "CASE rolle".
|
||
Darauf verlassen sich die Aufrufer, die es per .replace() auf
|
||
"p.rolle" umstellen; kein Rollenname enthaelt die Zeichenfolge. */
|
||
export const ROLLEN_SORTIERUNG = "CASE rolle "
|
||
+ ROLLEN_REIHE.map((r, i) => `WHEN '${r}' THEN ${i}`).join(" ")
|
||
+ ` ELSE ${ROLLEN_REIHE.length} END`;
|
||
|
||
/* LEITUNG = DogFather und Manager. Ein Manager darf alles, was
|
||
DogFather darf -- mit genau zwei Ausnahmen, die in
|
||
workspace-personen.js stehen: Er kann keine Leitung anlegen und keine
|
||
Leitung veraendern. Sonst koennte er sich selbst zum DogFather machen
|
||
oder den echten aussperren. "Nur DogFather hat alle endgueltigen
|
||
Rechte" heisst genau das. */
|
||
const LEITUNG = new Set(["spicy", "admin", "manager"]);
|
||
export const istLeitung = (person) => !!person && LEITUNG.has(person.rolle);
|
||
export const istDogFather = (person) => !!person && person.rolle === "admin";
|
||
|
||
/* ===== WER DARF AUFGABEN AN ANDERE VERTEILEN? (20.09.2026) ==========
|
||
|
||
Filipe: "jeder soll sich selber aufgabe vergeben koennen aber nur die
|
||
rechte hand und dogfather aufgaben an andere verteilen."
|
||
|
||
WARUM EINE EIGENE REGEL UND NICHT `hand` IN DIE LEITUNG:
|
||
`istLeitung` wird an 57 Stellen in 19 Dateien gefragt -- unter
|
||
anderem im vertraulichen Meldeweg, in der Personenverwaltung und in
|
||
den Auswertungen ueber Menschen. Die rechte Hand dort ueberall
|
||
mitzunehmen waere eine Entscheidung ueber Einsicht in fremde
|
||
Meldungen, und die hat niemand getroffen.
|
||
|
||
Verteilen ist eine Frage fuer sich. Sie bekommt deshalb ihren
|
||
eigenen Namen: Man sieht an der Aufrufstelle, worum es geht, und
|
||
wer sie spaeter aendert, aendert genau diese eine Sache.
|
||
|
||
spicy und manager bleiben drin: Auf der Agenturseite verteilen sie
|
||
seit jeher Aufgaben, und daran soll sich nichts aendern. */
|
||
/** Wer eine Aufgabe ANLEGEN darf.
|
||
*
|
||
* IM TEAM DOGI KOMMEN AUFGABEN VON DER LEITUNG (22.09.2026). Filipe:
|
||
* "sie sich nicht selber aufgaben geben" und "die modis sollen nur
|
||
* diese aufgaben wo sie von den drauf markiert werden annehmen
|
||
* koennen".
|
||
*
|
||
* Das gilt AUSDRUECKLICH NUR im Team Dogi. In der Agentur legt
|
||
* weiterhin jeder fuer sich selbst an -- dort ist eine Aufgabe eine
|
||
* Notiz an sich selbst, kein Auftrag von jemandem. Eine Regel, die
|
||
* beide Haeuser ueber einen Kamm schert, waere hier falsch. */
|
||
export const darfAufgabenAnlegen = (person) => {
|
||
if (!person) return false;
|
||
if (TEAM_DOGI_ROLLEN.has(person.rolle)) return darfAufgabenVerteilen(person);
|
||
return true;
|
||
};
|
||
|
||
export const darfAufgabenVerteilen = (person) =>
|
||
istLeitung(person) || istHand(person);
|
||
|
||
/* =====================================================================
|
||
WER ENTSCHEIDET, WER EINE AUFGABE MACHT (22.09.2026)
|
||
=====================================================================
|
||
Filipe: „die modis und linke hand sollen da nichts uebernehmen
|
||
koennen von aufgaben, ueberhaupt ueberall sollen die keine aufgaben
|
||
selber uebernehmen die ihnen nicht zugetragen sind. ich will dass die
|
||
sich fuer aufgaben bewerben koennen aber die rechte hand oder
|
||
dogfather muessen annehmen oder ablehnen koennen und das mit einem
|
||
kommentar als moeglichkeit sogar noch zum hinzufuegen."
|
||
|
||
DAS IST EINE ANDERE FRAGE ALS `darfAufgabenVerteilen`, und die zwei
|
||
auseinanderzuhalten ist der ganze Punkt:
|
||
|
||
verteilen -> eine Aufgabe an ANDERE geben
|
||
entscheiden -> bestimmen, wer sie am Ende macht
|
||
|
||
Die linke Hand darf verteilen (sein Wort vom selben Tag: „rechte hand
|
||
linke hand und dogfather ... koennen alle verteilen"), aber NICHT
|
||
entscheiden, ob sie selbst eine bekommt. Sie bewirbt sich wie ein
|
||
Modi, und die rechte Hand oder DogFather sagen ja oder nein.
|
||
|
||
GEMESSEN, WARUM ES NOETIG WAR: `istHand()` fasst die rechte UND die
|
||
linke Hand -- die linke konnte sich damit bisher Aufgaben aus dem
|
||
Pool selbst nehmen und sich beim Verteilen selbst eintragen.
|
||
|
||
Rolle istHand darfVerteilen (vorher)
|
||
admin false true
|
||
hand true true
|
||
linke true true <-- genau das soll weg
|
||
modi false false
|
||
|
||
DREI STELLEN HAENGEN DARAN, und alle drei fragen ab hier dieselbe
|
||
Funktion:
|
||
1. aus dem Pool uebernehmen
|
||
2. sich beim Verteilen selbst eintragen
|
||
3. ueber eine Bewerbung entscheiden
|
||
|
||
Eine Aufzaehlung der Rollen an drei Stellen waere die, bei der die
|
||
dritte beim naechsten Umbau vergessen wird. */
|
||
export const entscheidetUeberAufgaben = (person) =>
|
||
istLeitung(person) || person?.rolle === "hand";
|
||
|
||
/* ===== WER DARF EINE ROLLE AENDERN -- UND ZU WELCHER? (20.09.2026) ==
|
||
|
||
Filipe: "ich will dass ich da auch die rollen der leute wechseln kann
|
||
ohne dass ich denen einen neuen account machen muss mit einem neuen
|
||
code. einfach rolle wechseln und gut ist. perfektionier das fuer die
|
||
rolle dogfather und rechte hand. nur die sollen das machen koennen."
|
||
|
||
Das Wechseln selbst gab es schon -- der Zugangscode bleibt dabei
|
||
derselbe, das stand auch in der Rueckfrage. Es konnte nur DogFather.
|
||
|
||
DREI GRENZEN, UND JEDE HAT EINEN GRUND:
|
||
|
||
(1) NIEMAND WIRD ZU "admin". Das gilt seit dem 11.09.2026 fuer alle,
|
||
auch fuer DogFather selbst -- siehe ANLEGBAR. Hier aendert sich
|
||
daran nichts.
|
||
|
||
(2) DIE RECHTE HAND FASST KEINE ROLLE AN, DIE UEBER ODER NEBEN IHR
|
||
STEHT. Sie kann also einen Modi zur Community machen und
|
||
umgekehrt -- aber keinen DogFather antasten und keine zweite
|
||
rechte Hand ernennen. Wer jemanden auf die eigene Ebene hebt,
|
||
vergibt Vertrauen, das ihm nicht gehoert; das bleibt bei
|
||
DogFather. Und wer eine Rolle UEBER sich aendern koennte,
|
||
koennte sich selbst befoerdern, indem er zuerst den anderen
|
||
herabstuft.
|
||
|
||
(3) NIEMAND AENDERT DIE EIGENE ROLLE. Sonst waere jede Grenze oben
|
||
nur ein Umweg: erst sich selbst hochstufen, dann alles duerfen.
|
||
|
||
Was sie danach vergeben darf, steht in `rollenZumAendern` -- an
|
||
EINER Stelle, damit die Oberflaeche keine zweite Liste fuehrt. Genau
|
||
daran ist es am 10.09.2026 schon einmal gescheitert. */
|
||
const ROLLEN_ZUM_AENDERN = {
|
||
/* DogFather: alles, was er auch anlegen darf. */
|
||
admin: null, // null = dieselbe Liste wie ANLEGBAR
|
||
/* SPICY MEDIA STAND HIER SCHON, BEVOR ES DIESE TABELLE GAB -- sie
|
||
durfte Rollen wechseln, seit es den Weg gibt. Sie muss deshalb
|
||
drinstehen, sonst nimmt die neue Regel ihr etwas weg, das niemand
|
||
ihr wegnehmen wollte. `null` heisst: dasselbe wie beim Anlegen,
|
||
also Agenturrollen. */
|
||
spicy: null,
|
||
hand: ["modi", "gast"],
|
||
};
|
||
|
||
/** Welche Rollen darf diese Person an ANDERE vergeben? */
|
||
export function rollenZumAendern(person) {
|
||
if (!person) return [];
|
||
const eigen = ROLLEN_ZUM_AENDERN[person.rolle];
|
||
if (eigen === undefined) return [];
|
||
return eigen === null ? darfAnlegen(person) : [...eigen];
|
||
}
|
||
|
||
/** Darf diese Person die Rolle DIESES Menschen anfassen? */
|
||
export function darfRolleAendern(person, ziel) {
|
||
if (!person || !ziel) return false;
|
||
if (person.id === ziel.id) return false; // (3)
|
||
if (!rollenZumAendern(person).length) return false;
|
||
/* DOGFATHER DARF JEDEN ANFASSEN -- auch einen zweiten DogFather.
|
||
Hier stand `return ziel.rolle !== "admin"`, und das war zu scharf:
|
||
Es machte aus "DogFather ist nicht mehr VERGEBBAR" ein "an einem
|
||
DogFather ist nichts mehr zu aendern". Gemessen hat es
|
||
pruef-haus-trennung -- "solange es zwei gibt, darf einer wechseln"
|
||
bekam 403 statt 200. Ein zweiter Zugang liess sich damit nicht mehr
|
||
zurueckstufen; er waere fuer immer DogFather geblieben.
|
||
|
||
Die beiden echten Gefahren haengen woanders und bleiben: Niemand
|
||
KANN 'admin' vergeben (steht nicht in rollenZumAendern), und der
|
||
LETZTE DogFather laesst sich nicht herabstufen (Sicherung in der
|
||
Route). Die eigene Zeile ist drei Zeilen darueber schon
|
||
ausgeschlossen. */
|
||
if (person.rolle === "admin") return true;
|
||
/* Spicy Media wie bisher: das ganze Haus ausser DogFather. */
|
||
if (person.rolle === "spicy") return ziel.rolle !== "admin";
|
||
if (person.rolle === "hand") {
|
||
/* (2): nur unter sich -- nicht admin, nicht eine andere hand. */
|
||
return ziel.rolle !== "admin" && ziel.rolle !== "hand";
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/* =======================================================================
|
||
WER DARF WEN ANLEGEN — die einzige Liste dazu (10.09.2026)
|
||
|
||
Filipe, mit Bildschirmfoto der Zugaenge-Seite: "die spicy rolle soll
|
||
auch manager und scouts hinzufuegen koennen."
|
||
|
||
Sie konnte es nicht. Der Grund war NICHT ein fehlendes Recht, sondern
|
||
ZWEI Listen, die einander widersprachen — in derselben Funktion, drei
|
||
Zeilen auseinander (personen.js):
|
||
|
||
const darf = ... : ich.rolle === 'spicy' ? ['manager', 'creator']
|
||
...
|
||
if ((r.wert === 'admin' || r.wert === 'manager') && ich.rolle !== 'admin') continue;
|
||
|
||
Die erste Zeile erlaubt Spicy Media einen Manager, die zweite nimmt
|
||
ihn wieder weg. Uebrig blieb genau ein Knopf: Creator. Serverseitig
|
||
war der Weg fuer den Manager die ganze Zeit offen — es gab nur keinen
|
||
Knopf dafuer.
|
||
|
||
Das ist im Haus die immer gleiche Sorte Fehler: zwei Stellen fuer
|
||
dieselbe Aussage, und die spaetere gewinnt still. Deshalb steht die
|
||
Antwort ab jetzt EINMAL hier, und alle fragen sie:
|
||
|
||
- die drei Anlege-Wege in workspace-personen.js
|
||
- die Oberflaeche, ueber `darf_anlegen` in /workspace/api/ich
|
||
|
||
Die Oberflaeche hat damit gar keine eigene Liste mehr. Sie kann
|
||
deshalb auch nicht mehr von der des Servers abweichen — weder zu
|
||
streng (ein Recht, das niemand findet) noch zu grosszuegig (ein Knopf,
|
||
der eine Absage bringt).
|
||
|
||
WARUM SPICY MEDIA SCOUTS ANLEGEN DARF, ABER KEINE LEITUNG:
|
||
Ein Scout und ein Manager arbeiten unter Spicy Media — sie
|
||
einzustellen ist genau die Aufgabe dieser Rolle. Einen DogFather
|
||
anzulegen ist es nicht: Das waere ein zweiter Zugang mit allen
|
||
endgueltigen Rechten, und den vergibt nur DogFather selbst.
|
||
======================================================================= */
|
||
const ANLEGBAR = {
|
||
/* DogFather: alles ausser der eigenen Rolle.
|
||
|
||
DIE ROLLE "admin" IST SEIT DEM 11.09.2026 NICHT MEHR VERGEBBAR --
|
||
von niemandem, auch nicht von DogFather selbst. Filipe: "dogfather
|
||
soll man nicht auswaehlen koennen. das ist die einzige die man
|
||
nicht auswaehlen kann bitte."
|
||
|
||
WAS DAS BEDEUTET, damit es niemand spaeter sucht: Es laesst sich
|
||
kein zweiter DogFather-Zugang mehr anlegen, und niemand laesst
|
||
sich zu einem befoerdern. Der bestehende bleibt unberuehrt und ist
|
||
durch Sicherung 3 (nie den letzten DogFather herabstufen)
|
||
geschuetzt -- er kann also nicht versehentlich verschwinden.
|
||
Zurueckdrehen laesst sich das nur hier in dieser Liste.
|
||
|
||
Nebenwirkung, und sie ist erwuenscht: Damit gibt es keinen Weg
|
||
mehr, sich ueber die Oberflaeche zur hoechsten Rolle zu machen. */
|
||
/* DASS AUCH "hand", "modi" UND "gast" DARIN STEHEN, IST ENTSCHIEDEN --
|
||
nicht uebersehen (11.09.2026, ausdrueckliche Nachfrage, Antwort
|
||
"so lassen").
|
||
|
||
Sie gehoeren zu Team Dogi. Wer damit auf workspace. jemanden
|
||
umstellt, schiebt ihn ins andere Haus: Er faellt aus der
|
||
Agenturliste und kommt an dieser Adresse nicht mehr herein. Das
|
||
ist kein Fehler, das ist der Preis dafuer, dass DogFather es von
|
||
hier aus auch kann.
|
||
|
||
WER DAS SPAETER "AUFRAEUMEN" WILL: Die symmetrische Regel waere
|
||
ein Filter wie unten fuer das Crew-Haus, nur andersherum. Sie
|
||
wurde angeboten und abgelehnt. Zwei Pruefungen halten den Stand
|
||
fest (pruef-personen-formular, pruef-creator-anlegen) -- wer ihn
|
||
aendert, macht dort rot, und das soll er auch. */
|
||
admin: [...ROLLEN].filter((r) => r !== "admin"),
|
||
/* Spicy Media: das ganze Team -- und seit dem 11.09.2026 auch die
|
||
eigene Rolle.
|
||
|
||
Filipe: "ich will dass die rolle spicy und dogfather, auch die
|
||
rollen wechseln koennen wenn die personen schon drin sind. von
|
||
alle kategorien, creator, scouts, manager spicy."
|
||
|
||
Dass jemand seine eigene Rolle weitergeben kann, ist eine
|
||
Entscheidung und kein Versehen: Spicy Media fuehrt die Agentur.
|
||
DogFather bleibt trotzdem ausserhalb der Liste -- niemand hebt
|
||
sich ueber die Rolle, die ihn eingesetzt hat. */
|
||
spicy: ["spicy", "manager", "scout", "creator"],
|
||
/* Ein Manager stellt Creator ein, die er dann auch betreut. */
|
||
manager: ["creator"],
|
||
/* DIE RECHTE HAND LEGT PERSONEN AN (22.09.2026).
|
||
|
||
Filipe: "dan will ich dass die rechte hand auch neue personen
|
||
hinzufuegen kann. also neue erstellen kann und die codes genau so
|
||
sieht wie dogfather, damit sie das auch machen kann wenn er live
|
||
ist."
|
||
|
||
DIESELBE LISTE, DIE SIE AUCH VERGEBEN DARF -- abgeleitet aus
|
||
ROLLEN_ZUM_AENDERN, nicht danebengeschrieben. Zwei Listen mit
|
||
denselben zwei Rollen waeren zwei Gelegenheiten, eine davon zu
|
||
aendern und die andere zu vergessen; dann duerfte sie eine Rolle
|
||
anlegen, aber nicht vergeben, und niemand wuesste warum.
|
||
|
||
WAS DAMIT AUSDRUECKLICH NICHT GEHT: eine zweite rechte Hand, eine
|
||
linke Hand oder einen zweiten DogFather. Sie kann sich also auch
|
||
ueber den Umweg "neuen Zugang anlegen" keine hoeheren Rechte
|
||
verschaffen. */
|
||
hand: ROLLEN_ZUM_AENDERN.hand,
|
||
/* DIE LINKE HAND LEGT NIEMANDEN AN (21.09.2026) -- ausdruecklich im
|
||
Auftrag. Sie stuende auch ohne diese Zeile auf [] (darfAnlegen
|
||
nimmt `?? []`), aber dann saehe es aus wie vergessen. Eine
|
||
ausdrueckliche leere Liste ist eine Entscheidung, eine fehlende
|
||
Zeile ist eine Luecke. */
|
||
linke: [],
|
||
};
|
||
|
||
/** Welche Rollen darf diese Person anlegen? Immer ein Feld, nie null —
|
||
* wer nichts darf, bekommt eine leere Liste und keinen Sonderfall. */
|
||
export function darfAnlegen(person) {
|
||
if (!person) return [];
|
||
const alle = [...(ANLEGBAR[person.rolle] ?? [])];
|
||
/* AUF DER TEAM-ADRESSE NUR TEAM-ROLLEN (10.09.2026).
|
||
|
||
Filipe: "bitte nur basiert auf diese seite." Wer dort jemanden
|
||
anlegt, meint jemanden aus dem Team -- einen Creator dort
|
||
einzutragen waere ein Versehen, das man erst auf der anderen Seite
|
||
bemerkt.
|
||
|
||
DIESELBE AUSKUNFT BAUT AUCH DIE KNOEPFE. `darfAnlegen` ist die
|
||
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. */
|
||
/* 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?
|
||
*
|
||
* (11.09.2026) Filipe: "ich will dass die rolle spicy und dogfather,
|
||
* auch die rollen wechseln koennen wenn die personen schon drin
|
||
* sind."
|
||
*
|
||
* EINE REGEL, DREI BENUTZER: die Schranke in nurAdmin, der Knopf in
|
||
* der Oberflaeche (ueber `darf_rollen_wechseln` in /api/ich) und die
|
||
* Route selbst. Stuende sie dreimal da, waere die dritte Abschrift
|
||
* die, die eine Rolle vergisst -- genau so ist am selben Tag im Chat
|
||
* ein Knopf unsichtbar geblieben, obwohl das Recht stimmte.
|
||
*
|
||
* WAS SIE NICHT ENTSCHEIDET: WELCHE Rolle vergeben werden darf. Das
|
||
* steht in ANLEGBAR und ist bewusst getrennt -- "darf ueberhaupt
|
||
* wechseln" und "darf DIESE Rolle vergeben" sind zwei Fragen, und
|
||
* ihre Antworten laufen auseinander, sobald eine Rolle dazukommt.
|
||
*/
|
||
/* WER DARF DEN WEG "Rolle aendern" UEBERHAUPT BETRETEN?
|
||
*
|
||
* DIE RECHTE HAND KAM AM 20.09.2026 DAZU. Filipe: "perfektionier das
|
||
* fuer die rolle dogfather und rechte hand. nur die sollen das machen
|
||
* koennen."
|
||
*
|
||
* DIESE ZEILE OEFFNET NUR DIE TUER. Wen sie anfassen darf und wozu,
|
||
* entscheidet darfRolleAendern() / rollenZumAendern() weiter oben --
|
||
* dort steht auch, warum die rechte Hand keine zweite rechte Hand
|
||
* ernennen kann. Zwei getrennte Fragen, zwei getrennte Regeln: Wer
|
||
* hier etwas aendert, oeffnet keine Grenze versehentlich mit. */
|
||
export const darfRollenWechseln = (person) =>
|
||
!!person && (person.rolle === "admin" || person.rolle === "spicy"
|
||
|| person.rolle === "hand");
|
||
/* OHNE "linke" -- UND DAS IST ABSICHT, KEIN VERGESSEN (21.09.2026).
|
||
Eine Rolle zu aendern ist Kontoverwaltung. Der Auftrag sagt zur
|
||
linken Hand ausdruecklich "keine neuen Personen oder Accounts
|
||
hinzufuegen"; wer Rollen vergeben kann, kann sich selbst und andere
|
||
zu allem machen, was es gibt. Deshalb hier die kleinere Antwort. */
|
||
|
||
/* WER FUEHRT TEAM DOGI? (10.09.2026)
|
||
*
|
||
* DogFather und seine rechte Hand -- und sonst niemand. Spicy Media und
|
||
* die Manager stehen bewusst NICHT darin: Sie fuehren die Agentur, nicht
|
||
* das Team.
|
||
*
|
||
* WARUM DAS HIER STEHT UND NICHT DORT, WO ES GEBRAUCHT WIRD: Es wurde
|
||
* schon zweimal gebraucht -- im Eingang (`darfEingang`) und jetzt bei
|
||
* den Kategorie-Kanaelen. Beim dritten Mal waere es dreimal
|
||
* hingeschrieben, und die dritte Abschrift ist die, die eine Rolle
|
||
* vergisst. Der Eingang ruft ab jetzt hierher.
|
||
*/
|
||
export const fuehrtTeamDogi = (person) =>
|
||
!!person && (person.rolle === "admin" || istHand(person));
|
||
|
||
/* =======================================================================
|
||
SPICY MEDIA (07.09.2026)
|
||
|
||
Wunsch Filipe: "eine neue rolle ... die den namen traegt, spicy media,
|
||
und soll die gleichen rechte haben wie dogfather ausser, bei
|
||
automationen sollen die nicht sehen und die kategorie personen und
|
||
zugaenge sollen die nur leute hinzufuegen koennen ... aber die sollen
|
||
meine privaten chats und so nicht sehen."
|
||
|
||
ZWEI FRAGEN, DIE MAN AUSEINANDERHALTEN MUSS -- und genau daran haette
|
||
man diesen Umbau kaputtmachen koennen:
|
||
|
||
WER SIEHT ALLES? -> siehtAlles() = DogFather ODER Spicy Media
|
||
WER ENTSCHEIDET? -> istDogFather() = nur DogFather
|
||
|
||
Die erste Frage stellt sich bei Listen, Uebersichten und Auswertungen:
|
||
Spicy Media soll alle Manager, Scouts und Creator sehen. Die zweite
|
||
bei allem Endgueltigen: loeschen, Rollen aendern, Codes neu setzen,
|
||
Sicherungen, die KI abschalten. Da bleibt DogFather allein.
|
||
|
||
Es waere viel weniger Arbeit gewesen, istDogFather() einfach um
|
||
"spicy" zu erweitern -- und genau das waere der Fehler: Spicy Media
|
||
koennte dann DogFather loeschen. Zwei Namen, zwei Bedeutungen, und an
|
||
jeder Stelle steht sichtbar, welche gemeint ist.
|
||
|
||
DER CHAT BRAUCHTE NICHTS. Er haengt ausschliesslich an der
|
||
Teilnehmerliste (chat_teilnehmer) und kennt kein "das Management sieht
|
||
alles". Spicy Media sieht fremde Gespraeche also nicht, weil es dafuer
|
||
gar keinen Weg gibt -- nicht, weil eine Abfrage es verbietet.
|
||
======================================================================= */
|
||
export const istSpicy = (person) => !!person && person.rolle === "spicy";
|
||
|
||
/* =======================================================================
|
||
SPICY MEDIA SIEHT ALLES -- AUSSER DEM, WAS DOGFATHER GEHOERT
|
||
(07.09.2026, Nachtrag am selben Tag)
|
||
|
||
Filipe: "wieso sieht die spicy rolle meine daten die ich habe und
|
||
speichere, meins sollen die nicht sehen von der rolle dogfather."
|
||
|
||
Beim ersten Anlauf bekam Spicy Media dieselbe Regel wie DogFather:
|
||
`1=1`. Damit stimmte der Ueberblick ueber Manager, Scouts und Creator
|
||
-- und nebenbei standen DogFathers eigene Termine, Aufgaben, Dateien
|
||
und Eintraege mit drin. Der Chat war ausgenommen (er haengt an der
|
||
Teilnehmerliste), alles andere nicht.
|
||
|
||
DIESE FUNKTION IST DIE EINE STELLE DAFUER. Sie baut die Bedingung
|
||
"gehoert keinem DogFather" fuer eine beliebige Tabelle. Jede
|
||
Sichtbarkeitsregel im Haus benutzt sie -- eine zweite, abgeschriebene
|
||
Fassung waere die Stelle, an der es beim naechsten Modul wieder
|
||
durchsickert.
|
||
|
||
ZWEI SPALTEN, NICHT EINE. `creator_id` sagt, UM WEN es geht;
|
||
`erstellt_von` sagt, WER es geschrieben hat. Beide muessen gepruegt
|
||
werden: Ein Termin, den DogFather fuer sich selbst anlegt, hat
|
||
moeglicherweise gar keine creator_id -- er haenge dann nur an
|
||
erstellt_von. Und eine Akte UEBER einen DogFather traegt seine
|
||
creator_id, auch wenn ein Manager sie geschrieben hat.
|
||
|
||
`IS NULL OR NOT IN` und nicht bloss `NOT IN`: In SQL ist
|
||
`NULL NOT IN (...)` weder wahr noch falsch, sondern NULL -- die Zeile
|
||
fiele stillschweigend heraus. Genau so verschwinden Daten, ohne dass
|
||
jemand einen Fehler sieht.
|
||
======================================================================= */
|
||
export function ohneDogFather(praefix, spalten = ["creator_id", "erstellt_von"]) {
|
||
return spalten
|
||
.map((sp) => `(${praefix}.${sp} IS NULL OR ${praefix}.${sp} NOT IN `
|
||
+ `(SELECT id FROM personen WHERE rolle = 'admin'))`)
|
||
.join(" AND ");
|
||
}
|
||
/* =====================================================================
|
||
WAS EINEM MODI GEHOERT, SIEHT SONST NIEMAND (10.09.2026)
|
||
|
||
Das Gegenstueck zu ohneDogFather() -- und es entstand aus einem
|
||
gemessenen Leck, nicht aus einer Ueberlegung.
|
||
|
||
WAS PASSIERT WAR: Fuer PERSONEN ist die Regel zentral
|
||
(verborgeneIds). Fuer ZEILEN gibt es sie dreimal -- in
|
||
workspace-aufgaben.js, workspace-bereiche.js und
|
||
workspace-dateien.js -- und alle drei geben Spicy Media dasselbe:
|
||
"alles ausser dem, was DogFather gehoert". Ein Modi-Eintrag gehoert
|
||
ihm aber nicht. Also sah Spicy Media die Aufgaben, die Eintraege und
|
||
die Dateien der Modis, waehrend die Modis selbst in jeder Namensliste
|
||
sauber verborgen waren. Die halbe Verborgenheit ist keine.
|
||
|
||
Aufgefallen ist es nicht beim Lesen des Codes, sondern durch eine
|
||
Pruefung, die etwas ANLEGT und danach mit fremden Augen nachsieht.
|
||
Manager, Scout und Creator waren nie betroffen -- ihre Regeln sind
|
||
ohnehin enger. Der Kalender auch nicht: termineSichtbar() gibt jedem
|
||
nur Eigenes. Nachgemessen, nicht angenommen.
|
||
|
||
SPALTEN MUESSEN BEIDE RICHTUNGEN ABDECKEN. `creator_id` sagt, UM WEN
|
||
es geht; `erstellt_von`/`hochgeladen_von`/`verantwortlich_id` sagen,
|
||
an WEM es haengt. Eine Modi-Aufgabe hat gar keine creator_id -- sie
|
||
haengt allein an verantwortlich_id. Wer nur eine Spalte prueft, hat
|
||
nichts geprueft.
|
||
|
||
`IS NULL OR NOT IN` und nicht bloss `NOT IN`: In SQL ist
|
||
`NULL NOT IN (...)` weder wahr noch falsch, sondern NULL -- die Zeile
|
||
fiele stillschweigend heraus. Genau so verschwinden Daten, ohne dass
|
||
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.
|
||
|
||
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
|
||
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
|
||
Hinzukommen der rechten Hand waere das eine stille Luecke geworden:
|
||
Ihre Zeilen waeren fuer Spicy Media wieder sichtbar gewesen, ohne
|
||
dass irgendwo etwas rot wird.
|
||
|
||
Als Aufzaehlung in den SQL-Text eingesetzt und nicht als Parameter,
|
||
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. */
|
||
return ohneRollen(praefix, spalten, TEAM_DOGI_ROLLEN);
|
||
}
|
||
|
||
/** Gehoert dieser Mensch zu Team Dogi -- DogFather eingerechnet?
|
||
*
|
||
* DAS SIND DIE VIER, DIE FILIPE IMMER WIEDER NENNT: „nur die modis
|
||
* rechte linke hand und dogfather". Genau diese Aufzaehlung stand
|
||
* hier dreimal wortgleich als Ausdruck da (siehtModis, kanaeleFuer,
|
||
* kategorienFuer) und waere mit dem naechsten Wunsch ein viertes Mal
|
||
* dazugekommen. Drei gleiche Ausdruecke sind drei Gelegenheiten, dass
|
||
* einer beim naechsten Rollenzuschnitt nicht mitgeht.
|
||
*
|
||
* ABGELEITET, NICHT ABGESCHRIEBEN: TEAM_DOGI_ROLLEN (hand, linke,
|
||
* modi) ist die eine Liste des Hauses; DogFather kommt dazu, weil er
|
||
* dazugehoert -- er steht nicht darueber, er ist dabei. Kaeme morgen
|
||
* eine weitere Team-Rolle, waere sie hier von selbst dabei.
|
||
*/
|
||
export const istTeamDogi = (person) => !!person
|
||
&& (person.rolle === "admin" || TEAM_DOGI_ROLLEN.has(person.rolle));
|
||
|
||
/** Darf diese Person ueberhaupt etwas sehen, das einem Modi gehoert?
|
||
* Nur die DogFather-Rolle und die Modis selbst. */
|
||
export const siehtModis = istTeamDogi;
|
||
|
||
export const siehtAlles = (person) => !!person
|
||
&& (person.rolle === "admin" || person.rolle === "spicy");
|
||
|
||
/* Der Rollenschluessel bleibt "admin" -- er steckt in der CHECK-Regel der
|
||
Datenbank, in jeder Sitzung und in jeder Rechteabfrage. Umbenannt wird
|
||
nur, was man LIEST. Diese Zuordnung ist die einzige Stelle dafuer:
|
||
Stand der Anzeigename in elf Dateien, waere er beim naechsten Mal in
|
||
zehn davon geaendert.
|
||
|
||
Hinweis fuer spaeter: In den Kommentaren der Fachmodule heisst die
|
||
Rolle "admin" weiterhin "das Management". Gemeint ist dieselbe Rolle,
|
||
die in der Oberflaeche "DogFather" heisst. */
|
||
export const ROLLEN_NAME = {
|
||
spicy: "Spicy Media",
|
||
admin: "DogFather",
|
||
manager: "Manager",
|
||
scout: "Scout",
|
||
creator: "Creator",
|
||
hand: "Rechte Hand",
|
||
/* DIE LINKE HAND (21.09.2026). Ohne diesen Eintrag heisst sie
|
||
ueberall dort, wo der Rollenname steht -- im Treff, an einem
|
||
Beitrag, in der Personenliste -- schlicht "linke". Gefunden hat
|
||
das pruef-rechtetafel, weil sie diese Tabelle gegen ROLLEN haelt
|
||
und rot wird, sobald eine Rolle nur in einer der beiden steht. */
|
||
linke: "Linke Hand",
|
||
modi: "Modi",
|
||
/* DIE COMMUNITY (11.09.2026). Der Name steht hier und NICHT in einer
|
||
Datei, die jeder herunterlaedt -- ausgeliefert wird er nur an den,
|
||
der ihn selbst traegt, und an die, die ihn sehen duerfen.
|
||
|
||
Filipe hat ausdruecklich gewaehlt, dass im Treff der volle
|
||
Rollenname neben einem Beitrag steht ("Modi", "Rechte Hand",
|
||
"DogFather"). Das bleibt damit vereinbar: Der Name kommt als DATEN
|
||
mit der Antwort, nicht als Konstante im Browser. */
|
||
gast: "Community",
|
||
};
|
||
|
||
/* DIE UEBERSCHRIFT UEBER EINER PERSONENLISTE (10.09.2026).
|
||
*
|
||
* Sie ist NICHT dasselbe wie das Namensschild: Ueber einer Liste steht
|
||
* die Mehrzahl ("Scouts"), am einzelnen Menschen die Einzahl ("Scout").
|
||
* Der Browser hat diese Zuordnung in bereiche.js ebenfalls -- aber nur
|
||
* fuer die fuenf Rollen, die dort stehen duerfen.
|
||
*
|
||
* WARUM SIE HIER GEBRAUCHT WIRD: In der Personenauswahl des Chats
|
||
* wurde nach `ROLLENFOLGE` aus bereiche.js gezeichnet. Wer dort nicht
|
||
* vorkommt, wurde nicht gezeichnet -- ohne Fehler, ohne Luecke, ohne
|
||
* Hinweis. Team Dogi kommt dort nicht vor und darf es auch nicht (der
|
||
* Rollenname gehoert in keine Datei, die jeder herunterlaedt). Folge:
|
||
* DogFather konnte ueber die Auswahl niemandem aus seinem Team
|
||
* schreiben und keinen in einen Kanal setzen. Aufgefallen ist es nicht
|
||
* beim Lesen, sondern beim Hinsehen: Der Kanal hatte hinterher zwei
|
||
* Leute statt vier.
|
||
*
|
||
* Jetzt schickt der Server die Ueberschrift mit. Der Browser braucht
|
||
* dafuer keinen Rollennamen zu kennen -- er bekommt einen Text.
|
||
*/
|
||
export const ROLLEN_GRUPPE = {
|
||
...ROLLEN_NAME,
|
||
scout: "Scouts",
|
||
/* Beide unter EINER Ueberschrift: In der Auswahl sucht man einen
|
||
Menschen, keine Rangordnung -- und zwei Ueberschriften mit je
|
||
einem Namen darunter waeren mehr Gliederung als Inhalt. */
|
||
hand: "Team Dogi",
|
||
/* DIE LINKE HAND GEHOERT DAZU (23.09.2026). Ohne diesen Eintrag
|
||
faellt sie auf ROLLEN_NAME zurueck und bekommt in der Auswahl eine
|
||
eigene Ueberschrift „Linke Hand" mit genau einem Namen darunter --
|
||
also dieselbe Rangordnung, die zwei Zeilen hoeher ausdruecklich
|
||
vermieden werden sollte. Aufgefallen beim Nachgehen des Befundes
|
||
zum Steckbrief; dort fehlte sie ebenfalls. */
|
||
linke: "Team Dogi",
|
||
modi: "Team Dogi",
|
||
};
|
||
|
||
/* =====================================================================
|
||
DIE KACHELN EINES MODIS (10.09.2026)
|
||
|
||
Wo die anderen fuenf Rollen ihre Kacheln aus assets/js/bereiche.js
|
||
bekommen, kommen sie fuer einen Modi von hier. Der Grund ist derselbe
|
||
wie beim Anzeigenamen und bei der Rollenauswahl: bereiche.js wird an
|
||
JEDEN ausgeliefert, der die Seite oeffnet. Stuende dort auch nur
|
||
`rollen: [... , 'modi']`, waere die Rolle mit einem Blick in den
|
||
Quelltext gefunden -- und damit alles verraten.
|
||
|
||
NICHT NUR EINE LISTE VON ZIELEN, sondern die ganze Kachel. Wer nur
|
||
die Ziele schickte und die Beschriftung aus bereiche.js nehmen liesse,
|
||
bekaeme unter "Dashboard" den Untertitel "Alle Creator auf einen
|
||
Blick" -- fuer jemanden, der keine Creator betreut. Die Worte gehoeren
|
||
zum Empfaenger, nicht zum Ziel.
|
||
|
||
ZEICHEN, TON UND SZENE SIND SCHLUESSEL AUS bereiche.js, keine neuen.
|
||
Sie sagen nichts ueber Modis aus (jede andere Kachel benutzt dieselben)
|
||
und muessen deshalb nicht verborgen werden. Neue zu erfinden waere
|
||
genau falsch herum: Sie muessten in bereiche.js stehen, und dort
|
||
faellt jede unbenutzte Zeile irgendwann jemandem auf. Die Toene sind
|
||
dieselben wie an den entsprechenden Kacheln der anderen Rollen -- ein
|
||
Ton kennzeichnet die Kachel, nicht ihren Platz.
|
||
|
||
WAS NICHT DABEI IST, und warum: Uebersicht, Calls, Content, Reports,
|
||
Scouting, Personen und Automationen. Sie drehen sich um betreute
|
||
Creator, um die Agentur oder um Rechte -- fuer einen Modi waeren sie
|
||
leer oder nicht seine Sache. Der Rollen-Rundgang (pruef-rollen) zeigt,
|
||
dass sie ihn nicht abstuerzen lassen; das ist der Grund, sie
|
||
wegzulassen, und nicht der Grund, sie mitzunehmen.
|
||
|
||
ES IST EINE ROLLENFRAGE, KEINE PERSONENFRAGE: Alle Modis bekommen
|
||
dasselbe. Sollte das je auseinandergehen, gehoert die Auswahl an die
|
||
Person und nicht hierher -- dann aber bewusst und mit einer Stelle,
|
||
an der sie gepflegt wird.
|
||
===================================================================== */
|
||
const MODI_BEREICHE = [
|
||
{ gruppe: "Täglich", gruppeUnter: "Was du sowieso jeden Tag aufmachst",
|
||
name: "Aufgaben", unter: "Offen, in Arbeit, zur Freigabe", zeichen: "aufgaben",
|
||
/* SILBER (23.09.2026). Filipe: „ich will dass diese kachel einen
|
||
richtig geilen silber hat als farbe bitte. mach es richtig geil,
|
||
ein glitzer silber."
|
||
|
||
ALS EIGENES MERKMAL, NICHT ALS TON. Silber ist keine Farbe aus
|
||
den siebzehn -- es ist eine OBERFLAECHE: ein kuehler
|
||
Metallverlauf mit Glanzkante und feinem Funkeln. Als Ton
|
||
eingetragen haette es eine Buntheit nahe null, und
|
||
pruef-kachelfarben verlangt zu Recht mindestens 0,12.
|
||
|
||
`ton: 13` bleibt deshalb stehen und ist der Rueckfall: Wer die
|
||
Kachel ohne die neue Stilvorlage sieht (alter Zwischenspeicher,
|
||
Kontrastmodus), bekommt weiterhin ihr Gruen. Genau so macht es
|
||
die Willkommenskachel mit `regenbogen` seit dem 22.09. */
|
||
ton: 13, silber: true, gross: true, ziel: "aufgaben.html", szene: "garage" },
|
||
{ gruppe: "Täglich", gruppeUnter: "Was du sowieso jeden Tag aufmachst",
|
||
name: "Chat", unter: "Nachrichten mit dem Team", zeichen: "chat",
|
||
ton: 4, ziel: "chat.html", szene: "lounge" },
|
||
{ gruppe: "Täglich", gruppeUnter: "Was du sowieso jeden Tag aufmachst",
|
||
name: "Kalender", unter: "Live-Zeiten und Termine", zeichen: "kalender",
|
||
ton: 8, ziel: "kalender.html", szene: "skyline" },
|
||
{ gruppe: "Täglich", gruppeUnter: "Was du sowieso jeden Tag aufmachst",
|
||
name: "Dateien", unter: "Ablage und Freigaben", zeichen: "dateien",
|
||
ton: 19, ziel: "dateien.html", szene: "arena" },
|
||
|
||
{ gruppe: "Täglich", gruppeUnter: "Was du sowieso jeden Tag aufmachst",
|
||
name: "Ideen-Board", unter: "Gesammelt und nach Dringlichkeit sortiert",
|
||
zeichen: "content", ton: 23, ziel: "bereich.html?b=ideen", szene: "portal" },
|
||
{ gruppe: "Täglich", gruppeUnter: "Was du sowieso jeden Tag aufmachst",
|
||
name: "Angebote", unter: "Vorschläge, über die DogFather entscheidet",
|
||
zeichen: "agentur", ton: 17, ziel: "bereich.html?b=angebote", szene: "halle" },
|
||
|
||
/* RUECKMELDUNG -- IN BEIDE RICHTUNGEN (10.09.2026, Kapitel 5 und 6
|
||
des Pflichtenhefts).
|
||
|
||
Filipe: "Nicht nur ich soll meine Modis bewerten oder ihnen
|
||
Feedback geben koennen. Auch die Modis sollen mir Feedback geben
|
||
koennen." Und: "Es soll vor allem dabei helfen, als Team besser zu
|
||
werden."
|
||
|
||
Deshalb steht die Kachel bei ALLEN im Team an derselben Stelle und
|
||
heisst fuer alle gleich. "Feedback an DogFather" beim einen und
|
||
"Bewertung des Teams" beim anderen waeren zwei Einbahnstrassen --
|
||
und die Richtung stuende im Namen. */
|
||
{ gruppe: "Täglich", gruppeUnter: "Was du sowieso jeden Tag aufmachst",
|
||
name: "Rückmeldung", unter: "Was gut läuft, was hakt – in beide Richtungen",
|
||
zeichen: "reports", ton: 25, ziel: "bereich.html?b=rueckmeldung", szene: "lounge" },
|
||
|
||
{ gruppe: "Rund ums Live", gruppeUnter: "Vor, während und nach der Sendung",
|
||
name: "Live-Ablauf", unter: "Checkliste für vorher, mittendrin und danach",
|
||
zeichen: "live", ton: 3, ziel: "bereich.html?b=live", szene: "portal" },
|
||
{ gruppe: "Rund ums Live", gruppeUnter: "Vor, während und nach der Sendung",
|
||
name: "Community", unter: "Begrüßen, vermitteln, wiedererkennen",
|
||
zeichen: "community", ton: 6, ziel: "bereich.html?b=community", szene: "lounge" },
|
||
{ gruppe: "Rund ums Live", gruppeUnter: "Vor, während und nach der Sendung",
|
||
name: "Technik", unter: "Ton, Bild, Verbindung", zeichen: "technik",
|
||
ton: 14, ziel: "bereich.html?b=technik", szene: "garage" },
|
||
|
||
{ 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" },
|
||
|
||
/* =================================================================
|
||
DER NOTIZBLOCK (26.09.2026)
|
||
|
||
Filipe: „fuer die modis, rechte und linke hand und dogfather
|
||
eine kachel hinzufuegst, sie soll: Notizen, heissen. ich will
|
||
dass du das auch wie ein notizblock erstellst."
|
||
|
||
GENAU DIESE VIER ROLLEN, und dafuer muss hier nichts aufgezaehlt
|
||
werden: MODI_BEREICHE ist die Grundliste, aus der HAND_BEREICHE
|
||
durch Erweitern entsteht und LINKE_MIT_TREFF durch Wegnehmen
|
||
einer einzigen Kachel. Wer hier eintraegt, trifft Modi, rechte
|
||
Hand, linke Hand und DogFather auf der Team-Adresse -- und
|
||
sonst niemanden. Eine eigene Liste waere die, die beim naechsten
|
||
Rollenzuschnitt auseinanderlaeuft.
|
||
|
||
ER GEHOERT ZU „FUER DICH" UND NICHT ZU „TAEGLICH", obwohl man
|
||
ihn taeglich aufmacht. Die Gruppe sagt die wichtigste
|
||
Eigenschaft, bevor man die Kachel anfasst: Diese Notizen sieht
|
||
NIEMAND ausser dem, der sie schreibt -- auch DogFather nicht.
|
||
Stuende die Kachel zwischen Chat und Kalender, waere die erste
|
||
Frage „wer liest das mit?", und die Antwort stuende erst auf der
|
||
Seite.
|
||
|
||
NUR AUF DER TEAM-ADRESSE. Die Seite steht in
|
||
GEHOERT_ZU_ADRESSE (crew-adresse.js) und ist auf der
|
||
Agenturadresse ein 404 -- auch fuer DogFather, der in beiden
|
||
Haeusern arbeitet. Das ist dieselbe Trennung wie bei Chat,
|
||
Kalender und Aufgaben: Auf jeder Adresse sieht er nur deren
|
||
Bestand.
|
||
|
||
TON 46 UND DIE OBERFLAECHE `papier`. Der Ton ist die groesste
|
||
freie Luecke der Palette; warum es kein Bernstein wurde, steht
|
||
mit allen Messungen in tools/kachelton-regeln.mjs. Das
|
||
Papiergefuehl traegt nicht die Farbe, sondern die OBERFLAECHE --
|
||
Linien und ein perforierter Rand, so wie `silber` bei den
|
||
Aufgaben und `regenbogen` bei Willkommen. Faellt sie aus (alter
|
||
Zwischenspeicher, Kontrastmodus), bleibt die Kachel in ihrer
|
||
Farbe und damit unterscheidbar. */
|
||
{ gruppe: "Für dich", gruppeUnter: "Deine Seite im Team",
|
||
name: "Notizen", unter: "Dein Block – nur du siehst ihn",
|
||
zeichen: "notizen", ton: 46, papier: true,
|
||
ziel: "notizen.html", szene: "halle" },
|
||
/* ==== 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. */
|
||
];
|
||
|
||
/**
|
||
* Die Kacheln dieser Person -- oder `null`, wenn sie aus bereiche.js
|
||
* kommen sollen.
|
||
*
|
||
* `null` und nicht eine leere Liste: Die Oberflaeche unterscheidet
|
||
* "nimm deine eigene Liste" von "du bekommst keine Kacheln". Eine leere
|
||
* Liste hiesse das Zweite, und die fuenf bekannten Rollen haetten ab
|
||
* sofort eine leere Startseite.
|
||
*/
|
||
/* Die Eingangs-Kachel steht hier -- VOR ihrer ersten Verwendung.
|
||
|
||
Sie stand zuerst weiter unten beim ZUSATZ_BEREICHE-Block, wo sie
|
||
thematisch hingehoert. Das Modul liess sich damit nicht mehr laden:
|
||
"Cannot access 'EINGANG_KACHEL' before initialization". `const` wird
|
||
erst an seiner Zeile gueltig, und HAND_BEREICHE greift weiter oben
|
||
darauf zu.
|
||
|
||
`node --check` hat das NICHT gemeldet -- es prueft die Schreibweise,
|
||
nicht die Reihenfolge. Gefunden hat es erst der Versuch, das Modul
|
||
wirklich zu laden. Eine Syntaxpruefung ist keine Ladeprobe. */
|
||
const EINGANG_KACHEL = {
|
||
gruppe: "Täglich", gruppeUnter: "Was du sowieso jeden Tag aufmachst",
|
||
/* DER NAME, DRITTE FASSUNG (11.09.2026, Filipe: "ich will dass du den
|
||
namen aenderst von dieser kacheln").
|
||
|
||
Zuerst hiess sie "Team-Lage" -- das klang nach Bericht ueber
|
||
Menschen. Dann "Eingang" -- das beschrieb nur die eine Haelfte
|
||
(die Rueckmeldungen und Angebote, die hereinkommen) und liess die
|
||
andere weg, die den groesseren Teil der Seite ausmacht: WER da
|
||
ist, wer heute kann, wer was offen hat.
|
||
|
||
"Dein Team" sagt beides und stellt niemanden ueber jemanden -- es
|
||
ist die Seite, auf der man sein Team ansieht, nicht die, auf der
|
||
man es beurteilt. Die Unterzeile nennt jetzt beide Haelften. */
|
||
name: "Dein Team", unter: "Wer da ist, wer was offen hat – und was auf dich wartet",
|
||
zeichen: "teamlage", ton: 24, ziel: "teamlage.html", szene: "halle",
|
||
};
|
||
|
||
|
||
/* PERSONEN & ZUGAENGE, ABER FUER DAS TEAM (10.09.2026)
|
||
|
||
Filipe: "die kategorie personen & zugaenge fehlt also muss das
|
||
hinzugefuegt werden und bitte nur basiert auf diese seite."
|
||
|
||
Sie fehlte, weil ich mit den Agenturkacheln auch diese entfernt habe
|
||
-- und ausgerechnet sie ist die, mit der man jemandem eine Rolle gibt.
|
||
Ohne sie war die Team-Adresse eine Seite, auf der man das Team nicht
|
||
verwalten kann.
|
||
|
||
NAME, ZEICHEN UND TON SIND DIESELBEN wie auf der Agenturseite. Es ist
|
||
dieselbe Seite mit demselben Zweck; ein zweiter Name dafuer waere ein
|
||
zweites Ding, das es nicht gibt.
|
||
|
||
"NUR BASIERT AUF DIESE SEITE" steht nicht hier, sondern im Server:
|
||
Auf dieser Adresse liefert die Liste nur Team Dogi, und angelegt
|
||
werden koennen nur Team-Rollen (siehe hausBedingung und
|
||
darfAnlegen). Eine Kachel, die etwas anderes verspricht als die
|
||
Antwort dahinter, waere schlimmer als keine. */
|
||
/* =====================================================================
|
||
ENTWICKLUNG UND TALENTE (11.09.2026)
|
||
|
||
Filipe: "ich will dass ich genau so eine kategorie habe in der
|
||
dogfather und rechten hand rollen, wo wir aufgaben oder bewertungen
|
||
ueber modis eingeben koennen. auch fuer zuschauer die vielleicht
|
||
modis werden koennten waere auch geil."
|
||
|
||
DIE GRUPPE HEISST NICHT "TEAM FUEHREN". Das waere der bequeme Name
|
||
und der falsche: Im Haus gilt, dass DogFather nicht ueber seinem Team
|
||
steht. "Entwicklung & Nachwuchs" sagt, WAS drinsteht, nicht, wer wem
|
||
uebergeordnet ist.
|
||
|
||
ZWEI KACHELN UND NICHT DREI: Aufgaben an das Team gibt es laengst --
|
||
sie stehen im Aufgabenbrett und haengen dort an einer Person, einer
|
||
Frist und einem Status. Eine zweite Stelle dafuer waere ein zweiter
|
||
Ort, an dem man nachsehen muesste, welche Aufgabe wirklich gilt. */
|
||
const FUEHRUNG_GRUPPE = {
|
||
gruppe: "Entwicklung & Nachwuchs",
|
||
gruppeUnter: "Was ihr über die Zeit beobachtet – und wer dazukommen könnte",
|
||
};
|
||
|
||
/* UMBENANNT AM 19.09.2026 -- von "Entwicklung" zu "Eure Aufgaben".
|
||
|
||
Filipe: "diese kategorie ist ja die aufgaben die ich an die modis
|
||
habe, also eher aufgabe und dan perfektionierst du eine neue wo
|
||
diagramme texte und so zu jedem modi gemacht wird und diese
|
||
kategorie kannst du entwicklung nenen."
|
||
|
||
Er hat recht, und der Name war seit dem 11.09. falsch: Damals fuehrte
|
||
die Kachel auf das Notizbrett (dort standen wirklich Beobachtungen),
|
||
seitdem fuehrt sie auf den KATALOG -- eine Liste dessen, was ein Modi
|
||
koennen soll. Das ist eine Aufgabenliste, und sie hiess weiter
|
||
"Entwicklung", weil beim Umhaengen des Ziels niemand den Namen
|
||
angefasst hat.
|
||
|
||
DER NAME "ENTWICKLUNG" WIRD DAMIT FREI fuer das, was Filipe wirklich
|
||
darunter versteht: eine Auswertung je Person, mit Verlauf und Text.
|
||
Die gibt es noch nicht -- sie ist der naechste Schritt und
|
||
ausdruecklich nicht diese Kachel. */
|
||
const ENTWICKLUNG_KACHEL = {
|
||
...FUEHRUNG_GRUPPE,
|
||
name: "Eure Aufgaben", unter: "Was ihr könnt – und was als Nächstes kommt",
|
||
/* SEIT DEM 11.09.2026 FUEHRT DIE KACHEL AUF DEN KATALOG, nicht mehr
|
||
auf das Notizbrett. Filipe: "ich will dass fertige aufgaben da
|
||
stehen und so". Das Brett gibt es weiter -- es ist von der
|
||
Katalogseite aus verlinkt und nimmt auf, was zwischen zwei
|
||
Durchgaengen auffaellt. */
|
||
zeichen: "steckbrief", ton: 26, ziel: "entwicklung.html", szene: "studio",
|
||
};
|
||
|
||
/* DIE KACHEL, FUER DIE DER NAME FREI GEMACHT WURDE (19.09.2026).
|
||
|
||
Filipe: "dan perfektionierst du eine neue wo diagramme texte und so
|
||
zu jedem modi gemacht wird und diese kategorie kannst du entwicklung
|
||
nenen."
|
||
|
||
SIE ZEIGT AUF werdegang.html UND NICHT AUF entwicklung.html. Das ist
|
||
Absicht und sieht beim ersten Hinsehen verdreht aus:
|
||
|
||
entwicklung.html = der KATALOG, Kachel "Eure Aufgaben"
|
||
werdegang.html = die AUSWERTUNG, Kachel "Entwicklung"
|
||
|
||
Die saubere Endform waere, die alte Datei umzubenennen. Das waeren
|
||
25 Fundstellen in 11 Dateien -- und damit zwei Aenderungen in einer:
|
||
Wenn danach etwas klemmt, weiss niemand, welche der beiden es war.
|
||
Deshalb steht es hier als Kommentar und in `pruef-werdegang` als
|
||
Pruefung, die rot wird, wenn die Kachel woanders hinzeigt.
|
||
|
||
EIGENER TON 40: GEMESSEN, NICHT GERATEN -- und der erste Anlauf lag
|
||
falsch. 38 sah frei aus, weil 37 die hoechste Nummer in DIESER Datei
|
||
ist. Gezaehlt in start.css sind es aber 39, alle vergeben: 38 ist
|
||
"Unsere Seiten", 39 "Vertraulich melden" -- beide kamen am 19.09.
|
||
frueh dazu und stehen nicht als `ton:` in workspace.js.
|
||
|
||
Genau davor warnt das Farbwerkzeug in seinem eigenen Kopf ("Zweiter
|
||
Anlauf war Ton 22, weil er frei AUSSAH"). Gezaehlt wird deshalb in
|
||
der Datei, die die Farben wirklich enthaelt.
|
||
|
||
Nach dem Hinzufuegen laeuft `node tools/kachel-farben.mjs --schreiben`
|
||
-- es rechnet alle 40 Toene neu gegeneinander, damit sich keine zwei
|
||
aehneln.
|
||
|
||
EIGENES ZEICHEN "werdegang": Die vorhandenen 22 sind entweder
|
||
vergeben (steckbrief viermal) oder sagen etwas anderes -- "zahlen"
|
||
ist eine Messkurve, "teamlage" sind zwei Koepfe. Hier geht es um
|
||
EINEN Menschen und seinen Weg. */
|
||
const WERDEGANG_KACHEL = {
|
||
...FUEHRUNG_GRUPPE,
|
||
name: "Entwicklung", unter: "Wie sich jemand über die Zeit bewegt – ohne Note",
|
||
zeichen: "werdegang", ton: 40, ziel: "werdegang.html", szene: "wald",
|
||
};
|
||
|
||
const TALENTE_KACHEL = {
|
||
...FUEHRUNG_GRUPPE,
|
||
name: "Talente", unter: "Zuschauer, die ins Team passen könnten",
|
||
zeichen: "scouting", ton: 27, ziel: "talente.html", szene: "portal",
|
||
};
|
||
|
||
const PERSONEN_KACHEL = {
|
||
gruppe: "Für dich", gruppeUnter: "Dein Platz im Team",
|
||
name: "Personen & Zugänge", unter: "Wer dabei ist – und mit welchem Zugang",
|
||
zeichen: "personen", ton: 11, ziel: "personen.html", szene: "zentrale",
|
||
};
|
||
|
||
/* DIE RECHTE HAND SIEHT DIESELBEN KACHELN WIE EIN MODI -- PLUS EINE.
|
||
|
||
Blueprint V3.0, Kapitel 3.1: "Gleicher Ueberblick wie Owner. Kann
|
||
Modis im Alltag koordinieren." Der gleiche Ueberblick heisst hier
|
||
ausdruecklich NICHT eine eigene Kachelwelt: Sie arbeitet an denselben
|
||
Dingen wie das Team, sieht darin aber alles statt nur das Eigene --
|
||
das entscheidet die Sichtbarkeitsregel, nicht die Kachelliste.
|
||
|
||
Dazu kommt der Eingang, denn sie "kann im Alltag vertretend
|
||
Entscheidungen treffen". Genau das passiert dort.
|
||
|
||
ABGELEITET UND NICHT ABGESCHRIEBEN: Wer eine Kachel fuer die Modis
|
||
hinzufuegt, bekommt sie hier automatisch mit. Eine zweite Liste waere
|
||
die Stelle, an der die rechte Hand irgendwann weniger sieht als das
|
||
Team, das sie koordinieren soll. */
|
||
/* PERSONEN & ZUGAENGE GEHOERT DAZU (10.09.2026, zweite Fassung).
|
||
|
||
Erst stand die Kachel nur bei DogFather -- die Seite haengt am
|
||
Server an `nurAdmin`, und ein Knopf, der eine Absage bringt, ist
|
||
schlimmer als kein Knopf.
|
||
|
||
Filipe danach: "ich will dass die rechte hand auch alle sieht."
|
||
Also bekommt sie die Seite -- lesend. Damit stimmt der Knopf wieder,
|
||
und er steht in derselben Liste wie alles andere, was sie und
|
||
DogFather auf dieser Adresse teilen. */
|
||
/** Setzt Kacheln VOR die erste Kachel einer Gruppe.
|
||
*
|
||
* WOZU: Die Startseite gruppiert nach dem ersten Auftreten einer
|
||
* Gruppe im Feld -- die Reihenfolge der Gruppen ist also die
|
||
* Reihenfolge der Kacheln. (gruppeNach gilt nur fuer die
|
||
* Zusatzkacheln der Agenturseite, siehe start.js.)
|
||
*
|
||
* NACH DEM NAMEN UND NICHT NACH EINER ZAHL. "An Position 7 einfuegen"
|
||
* waere beim naechsten Umsortieren still falsch -- und still falsch
|
||
* heisst hier: eine Kategorie rutscht irgendwohin, ohne dass es
|
||
* jemand merkt.
|
||
*
|
||
* FINDET SICH DIE GRUPPE NICHT, wird angehaengt statt weggelassen.
|
||
* Eine Kachel, die wegen einer Reihenfolge verschwindet, waere der
|
||
* schlechtere Tausch -- dieselbe Entscheidung wie bei gruppeNach. */
|
||
function vorGruppe(liste, gruppe, ...neue) {
|
||
const i = liste.findIndex((k) => k?.gruppe === gruppe);
|
||
if (i < 0) return [...liste, ...neue];
|
||
return [...liste.slice(0, i), ...neue, ...liste.slice(i)];
|
||
}
|
||
|
||
/* ENTWICKLUNG & NACHWUCHS STEHT UEBER "RUND UMS LIVE" (Filipe,
|
||
11.09.2026, mit zwei Bildschirmfotos: "mach nur die kategorie auf
|
||
screen1 ueber die kategorie von screen2").
|
||
|
||
NUR AUF DIESER ADRESSE. Auf der Agenturseite bleiben die beiden
|
||
Kacheln ganz unten -- dort steht dieselbe Entscheidung mit dem
|
||
umgekehrten Vorzeichen ("das sind private informationen von meiner
|
||
community", 11.09.2026), und sie kommen dort ueber
|
||
ZUSATZ_BEREICHE.admin mit eigenem gruppeNach. Zwei Adressen, zwei
|
||
Plaetze, beide entschieden. */
|
||
/* WIE GEHT'S DIR? -- die einzige Kachel des Hauses, deren Inhalt
|
||
niemand ausser der Person selbst je sieht (11.09.2026).
|
||
|
||
SIE STEHT BEI "FUER DICH" und nicht bei "Taeglich": Es sind sechs
|
||
Fragen, die man einmal im Monat beantwortet, nicht jeden Tag. Eine
|
||
taegliche Kachel, die man nicht taeglich braucht, wird nach einer
|
||
Woche uebersehen.
|
||
|
||
UND SIE STEHT AUCH BEI DOGFATHER UND DER RECHTEN HAND. Sie sind
|
||
Teil des Teams, nicht darueber -- wenn es jemandem zu viel wird, dann
|
||
ihnen genauso. Eine Kachel, die nur die anderen ausfuellen, waere
|
||
genau die Schieflage, die dieses Haus nicht haben soll. */
|
||
/* WER SIEHT WAS -- auf BEIDEN Adressen (Filipe, 11.09.2026).
|
||
|
||
Sie stand nur bei den Zusatzkacheln der Agenturseite. Auf der
|
||
Team-Dogi-Seite arbeiten DogFather und die rechte Hand aber taeglich;
|
||
dort nicht nachsehen zu koennen, wer was darf, hiesse, die Frage an
|
||
dem Ort zu stellen, an dem man gerade nicht ist.
|
||
|
||
EINMAL BESCHRIEBEN, ZWEIMAL BENUTZT. Zwei Kachelobjekte mit
|
||
demselben Ziel waeren zwei Gelegenheiten, eines davon umzubenennen.
|
||
|
||
EIN EIGENER TON, UND ZWAR EIN NEUER (11.09.2026): Erster Anlauf war
|
||
Ton 26 -- das waere "Entwicklung" ein zweites Mal gewesen. Zweiter
|
||
Anlauf war Ton 22, weil er frei AUSSAH: Ich hatte die Toene ueber
|
||
bereicheFuer() gezaehlt, und das liefert die Zusatzkacheln gar nicht
|
||
mit. Gefunden hat es pruef-start-ansicht, die ueber die ECHTE Ansicht
|
||
zaehlt ("34 Farben auf 35 Kacheln"). Ton 35 ist gemessen, nicht
|
||
geraten. */
|
||
const RECHTE_KACHEL = {
|
||
name: "Wer sieht was", unter: "Jede Rolle, jede Seite – ausgerechnet, nicht notiert",
|
||
zeichen: "schutz", ton: 35, ziel: "rechte.html", szene: "studio",
|
||
};
|
||
|
||
/* WIE GEHT'S DIR? -- EIGENE SEITE SEIT DEM 15.09.2026.
|
||
|
||
Filipe mit dem Bildschirmfoto der Kachel: "ich will dass du diese
|
||
seite perfektionnierst, denn gerade wenn ich drauf druecke geht die
|
||
entwicklungsseite auf."
|
||
|
||
Er hatte recht, und es war schlimmer als ein falscher Verweis: Die
|
||
Kachel verspricht "die Antworten sieht nur du" und oeffnete eine
|
||
Seite mit Namen und Beobachtungen ueber ANDERE Menschen. Wer ihr
|
||
glaubt und draufdrueckt, sieht im ersten Moment das Gegenteil dessen,
|
||
was draufsteht.
|
||
|
||
Jetzt: eine Seite, ein Zweck. entwicklung.html wird dadurch selbst
|
||
einfacher -- sie tut nur noch eine Sache. */
|
||
const BEFINDEN_KACHEL = {
|
||
gruppe: "Für dich", gruppeUnter: "Dein Platz im Team",
|
||
name: "Wie geht's dir?", unter: "Ein paar Fragen – die Antworten siehst nur du",
|
||
zeichen: "steckbrief", ton: 37, ziel: "befinden.html", szene: "lounge",
|
||
};
|
||
|
||
const HAND_BEREICHE = vorGruppe(
|
||
[...MODI_BEREICHE, EINGANG_KACHEL, PERSONEN_KACHEL],
|
||
"Rund ums Live",
|
||
ENTWICKLUNG_KACHEL, WERDEGANG_KACHEL, TALENTE_KACHEL,
|
||
).concat(BEFINDEN_KACHEL, {
|
||
gruppe: "Für dich", gruppeUnter: "Dein Platz im Team",
|
||
...RECHTE_KACHEL,
|
||
});
|
||
|
||
|
||
/* =====================================================================
|
||
DIE SIEBEN BRETTER DES TREFFS (11.09.2026)
|
||
|
||
Filipe: "wieso nur eine kachel fuer die community ich will dass die
|
||
mehrere kacheln haben. mit mehreren optionen."
|
||
|
||
Eine Kachel war zu kurz gedacht: Ein Raum, in dem Ansagen, Fragen,
|
||
Wuensche und Clips durcheinandergehen, ist nach zwei Wochen
|
||
unbenutzbar -- die Ansage von gestern ist weg, die Frage von heute
|
||
geht unter.
|
||
|
||
SORTIERT NACH ZWECK, NICHT NACH THEMA, und in der Reihenfolge, in der
|
||
ein Neuer liest: erst was gilt, dann was laeuft, dann das Gespraech,
|
||
dann das Besondere.
|
||
|
||
Jede der sieben bedient eines der vier Dinge, die darueber
|
||
entscheiden, ob sich jemand zugehoerig fuehlt (Zugehoerigkeit,
|
||
Einfluss, Nutzen, geteilte Erlebnisse). Was keines bedient, ist nicht
|
||
dabei -- deshalb gibt es hier keine Bestenliste und keine Punkte.
|
||
|
||
VON OBEN NACH UNTEN GELESEN SIND SIE EIN WEG: lesen -> mitreden ->
|
||
mitgestalten -> dazugehoeren. Die letzte Kachel uebergibt an die
|
||
Talente-Kategorie, die es schon gibt.
|
||
|
||
DIE KACHELN STEHEN HIER UND NICHT IN assets/js/bereiche.js -- aus
|
||
demselben Grund wie bei den Modis: Diese Datei wird nicht
|
||
ausgeliefert, jene bekommt jeder Besucher. */
|
||
const TREFF_GRUPPE = {
|
||
gruppe: "Community",
|
||
gruppeUnter: "Das Rudel \u2013 der Raum f\u00fcr die Zuschauer",
|
||
};
|
||
|
||
/* DIE BREITE KACHEL STEHT VORNE (17.09.2026).
|
||
|
||
Filipe, mit einem Bildschirmfoto: "das wie es jetzt ist ist es
|
||
einfach total scheisse ... gerade einfach nur dahin geknallt".
|
||
|
||
Er hatte recht, und die Ursache stand genau hier. Das Raster hat
|
||
DREI Spalten, "Das Rudel" ist doppelt breit (`grid-column: span 2`)
|
||
-- stand aber an DRITTER Stelle. Nach zwei normalen Kacheln war noch
|
||
EINE Spalte frei, er passte nicht hinein und rutschte eine Reihe
|
||
tiefer. Zurueck blieb ein Loch mitten im Raster:
|
||
|
||
Reihe 1 [Anschlagbrett] [Was ansteht] ....... LUECKE
|
||
Reihe 2 [ Der Treff (2) ] [Wunschliste]
|
||
Reihe 3 [Highlights] [Regeln] [Mitmachen]
|
||
(die Skizze zeigt den Stand vom 17.09.2026 -- seither
|
||
sind Kacheln dazugekommen, die Regel darunter gilt
|
||
unveraendert)
|
||
Reihe 4 [Meldungen] ...... LUECKE ...... LUECKE
|
||
|
||
DIE REGEL, und sie gilt fuer jedes Raster im Haus: Eine breite
|
||
Kachel gehoert an den ANFANG. Steht sie hinten, faellt die Luecke in
|
||
die MITTE -- und eine Luecke in der Mitte sieht kaputt aus, eine am
|
||
Ende sieht grosszuegig aus.
|
||
|
||
Jetzt fuellen Treff (2) plus Anschlagbrett die erste Reihe, die
|
||
naechsten drei die zweite, der Rest die dritte. Fuer das Team mit
|
||
der achten Kachel geht es genau auf: drei mal drei.
|
||
|
||
UND ES IST AUCH INHALTLICH RICHTIG. Der Treff ist das Herz dieses
|
||
Bereichs -- dass er vorne steht, ist keine Notloesung fuer das
|
||
Raster, sondern die Reihenfolge, die ohnehin stimmt. */
|
||
const TREFF_BEREICHE = [
|
||
/* AUS DEM TREFF-BRETT WIRD DIE WILLKOMMENSSEITE (21.09.2026, Auftrag
|
||
Abschnitt 7). Gemessen davor: Das Brett "treff" hatte NULL Eintraege,
|
||
der Treff-Chat daneben 29 Nachrichten -- geredet wird im Chat, das
|
||
Brett war eine Kachel ohne Inhalt. Sie zeigt jetzt auf die Seite,
|
||
die erklaert, was es hier ueberhaupt gibt. */
|
||
/* ALLE FARBEN DES HAUSES IN EINER KACHEL (22.09.2026).
|
||
|
||
Filipe: „die farbe dieser kachel soll alle farben haben die es auf
|
||
der ganze seite gibt ... nicht gemischt ... soll komplett farbig
|
||
sein und ultra geil aussehen."
|
||
|
||
`ton: 34` bleibt stehen und ist kein Widerspruch: Er faerbt Rand,
|
||
Zeichen und Pfeil und ist der Rueckfall, falls das Stilblatt
|
||
einmal nicht laedt. Die Streifen kommen aus einer eigenen Regel,
|
||
die `tools/kachel-regenbogen.mjs` rechnet -- aus den benutzten
|
||
Toenen, nicht aus einer Liste hier. */
|
||
{ ...TREFF_GRUPPE, name: "Willkommen", unter: "Was es hier gibt, kurz erklaert",
|
||
zeichen: "community", ton: 34, gross: true, regenbogen: true,
|
||
ziel: "willkommen.html", szene: "lounge" },
|
||
/* DER CHAT (19.09.2026). Filipe: "eine geile mischung von punkt 2
|
||
und drei ... nach mitternacht bis morgens 6 uhr keiner schreiben
|
||
kann damit auch die privatsphaere beruecksichtig wird ueber nacht
|
||
... und die sollen nicht anrufen koennen. nur schreiben."
|
||
|
||
GLEICH AN ZWEITER STELLE, direkt hinter dem grossen Treff-Brett.
|
||
Er ist das Lebendigste, was die Community hier hat -- weiter unten
|
||
waere er der Knopf, den man erst findet, wenn einem jemand sagt,
|
||
dass es ihn gibt.
|
||
|
||
ZIEL IST chat.html, dieselbe Seite wie beim Team. Kein zweiter
|
||
Chat: Zwei Oberflaechen fuer dasselbe waeren zwei Stellen, an
|
||
denen ein Fehler behoben werden muss, und die zweite waere die,
|
||
die man vergisst. Was jemand dort SIEHT, entscheidet die
|
||
Raummitgliedschaft -- ein Gast hat genau einen Raum.
|
||
|
||
TON 41: gezaehlt in start.css, nicht in dieser Datei. 40 war die
|
||
hoechste (siehe die Warnung an WERDEGANG_KACHEL, dort ist genau
|
||
dieser Fehler heute schon einmal passiert). */
|
||
/* „TREFF-CHAT“ UND NICHT NUR „CHAT“ (nachgeschärft 19.09.2026).
|
||
|
||
Gefunden beim Durchgehen aus Benutzersicht: Ein Modi und die
|
||
rechte Hand sahen ZWEI Kacheln mit demselben Namen „Chat“, beide
|
||
mit demselben Ziel `chat.html` -- eine unter „Täglich“ (das Team),
|
||
eine hier unter „Community“. Sie taten auch dasselbe: Man landete
|
||
in der Gesprächsliste und musste den richtigen Raum suchen.
|
||
|
||
Zwei gleich benannte Wege zum selben Ort sind schlimmer als ein
|
||
fehlender -- man klickt den falschen und sucht den Unterschied.
|
||
|
||
Jetzt trägt sie ihren eigenen Namen UND ihr eigenes Ziel:
|
||
`?raum=treff` öffnet den Raum der Community direkt. Für die
|
||
Community selbst ändert sich am Namen nichts Wesentliches -- sie
|
||
hat nur diese eine. */
|
||
{ ...TREFF_GRUPPE, name: "Rudel-Chat", unter: "Miteinander reden – nachts schläft er",
|
||
zeichen: "chat", ton: 41, ziel: "chat.html?raum=treff", szene: "lounge" },
|
||
/* HIGHLIGHTS AN DIE SPITZE (22.09.2026).
|
||
|
||
Filipe: „da wo anschlagbrett ist soll highlights sein und dan den
|
||
rest nochmal weiter fuehren."
|
||
|
||
Kein Tausch, sondern ein Aufruecken: Highlights nimmt den Platz,
|
||
alle anderen wandern eine Stelle weiter. Inhaltlich stimmt das
|
||
auch -- was die Leute selbst gemacht haben, steht vor dem, was
|
||
ihnen angesagt wird. */
|
||
{ ...TREFF_GRUPPE, name: "Highlights", unter: "Eure Clips und Bilder",
|
||
zeichen: "live", ton: 29, ziel: "bereich.html?b=highlight", szene: "arena" },
|
||
{ ...TREFF_GRUPPE, name: "Anschlagbrett", unter: "Was gerade gilt",
|
||
zeichen: "reports", ton: 33, ziel: "bereich.html?b=anschlag", szene: "halle" },
|
||
{ ...TREFF_GRUPPE, name: "Was ansteht", unter: "Die n\u00e4chsten Streams und Events",
|
||
zeichen: "kalender", ton: 31, ziel: "bereich.html?b=ansteht", szene: "skyline" },
|
||
{ ...TREFF_GRUPPE, name: "Wunschliste", unter: "Was ihr euch w\u00fcnscht \u2013 und wer mitwill",
|
||
zeichen: "content", ton: 30, ziel: "bereich.html?b=wunsch", szene: "portal" },
|
||
/* HIESS BIS ZUM 21.09.2026 "Unsere Seiten" (Ziel und Datei bleiben
|
||
gleich, nur der Name auf der Kachel aendert sich).
|
||
|
||
Dahinter stehen jetzt drei Dinge statt zwei Adressen: ob gerade
|
||
gestreamt wird, wo man Dogi sonst findet, und die beiden Seiten mit
|
||
dem Rabatt. "Unsere Seiten" haette davon nur noch den letzten
|
||
Drittel benannt -- und wer wegen des Live-Status wiederkommt,
|
||
findet eine Kachel nicht, die nach etwas anderem klingt.
|
||
|
||
DER NAME STEHT AB JETZT NUR EINMAL IM HAUS: pruef-wege-nach-draussen
|
||
liest die Ueberschrift der Seite und verlangt, dass die Kachel
|
||
genauso heisst. Zwei Namen fuer dieselbe Sache waren schon einmal
|
||
ein echter Fehler (die zwei Kacheln "Chat" am 19.09.) -- man klickt
|
||
den falschen und sucht den Unterschied. */
|
||
/* MATERIAL (22.09.2026).
|
||
|
||
Filipe: "ich brauch auch noch eine neue kachel im bereich
|
||
community, wo wir als team ... bilder reinschicken koennen, mit
|
||
text und wo die leute sei es community oder modis sich die bilder
|
||
nehmen koennen zum posten ... sobald ein bild oder ein video ...
|
||
runtergeladen wurde, soll direkt blockiert werden."
|
||
|
||
SIE STEHT VOR "DRAUSSEN": Wer hier etwas holt, geht danach
|
||
hinaus und postet es. Die Reihenfolge ist der Weg.
|
||
|
||
Ton 42 ist GERECHNET, nicht gegriffen -- tools/kachel-farbe-
|
||
einzeln.mjs, Abstand 0,0823 zum naechsten Nachbarn (1,3-fach
|
||
besser als der erste Vorschlag), Kontrast 6,08:1. */
|
||
{ ...TREFF_GRUPPE, name: "Dogi-Media", unter: "Bilder und Videos zum Posten – jedes nur einmal",
|
||
zeichen: "galerie", ton: 42, ziel: "material.html", szene: "studio" },
|
||
/* HINTER DOGI-MEDIA (28.09.2026, Filipe: „die kachel von der
|
||
kategorie, reaktion, soll nach der kachel, dogi-move,
|
||
erscheinen").
|
||
|
||
Eine Kachel „Dogi-Move" gibt es im ganzen Haus nicht -- das Wort
|
||
kommt nirgends vor. Die einzige, die passt, ist Dogi-Media.
|
||
Sollte etwas anderes gemeint gewesen sein, ist es eine Zeile
|
||
zurueck. */
|
||
/* ==== DIE REACTION STEHT GANZ VORNE ============================
|
||
(28.09.2026, nach Filipes Kurznotiz.)
|
||
|
||
Sie ist die einzige Kachel im Haus, die einen ZUSTAND hat --
|
||
zu, gleich, jetzt. Was gerade laeuft, gehoert an die erste
|
||
Stelle; was immer da ist, kann warten. Steht sie geschlossen
|
||
da, sagt sie das und nimmt trotzdem den Platz: Eine Kachel,
|
||
die je nach Lage woanders steht, ist keine Kachel, die man
|
||
wiederfindet.
|
||
|
||
KEINE TON-NUMMER. Der Farbraum ist bei 46 Toenen voll (siehe
|
||
tools/kachelton-regeln.mjs), und eine 47. Farbe wuerde den
|
||
Mindestabstand reissen. Sie braucht auch keine: Ihre Farbe IST
|
||
ihr Zustand -- grau geschlossen, bernstein in Vorbereitung,
|
||
rot auf Sendung. `live: true` sagt der Wand, dass diese Kachel
|
||
ihre Flaeche selbst mitbringt, genau wie `papier` beim
|
||
Notizblock und `silber` bei den Aufgaben. */
|
||
{ ...TREFF_GRUPPE, name: "Reaction", unter: "Zusammen schauen \u2013 live mit Kamera und Chat",
|
||
zeichen: "reaktion", live: true, ziel: "reaktion.html", szene: "arena" },
|
||
/* DIE FARBE, DIE "REGELN & HILFE" HATTE (22.09.2026).
|
||
Filipes screen5: "die farbe dieser kachel soll die farbe von der
|
||
kachel: regeln & hilfe werden". Regeln & Hilfe bekommt dafuer
|
||
eine eigene -- zwei Kacheln mit derselben Farbe in derselben
|
||
Ansicht waeren genau das, was screen4 abschafft.
|
||
Ton 39 ist GESETZT. */
|
||
{ ...TREFF_GRUPPE, name: "Draußen", unter: "Ist er live? Kanäle, Shop & dein Rabatt",
|
||
zeichen: "hinaus", ton: 39, ziel: "unsere-seiten.html", szene: "portal" },
|
||
/* UNTERSTÜTZEN (24.09.2026).
|
||
|
||
Filipe: "ich brauch auch noch eine kachel im community bereich. wo
|
||
mein paypal und meine amazon liste sein wird. wo die leute alle
|
||
supporten können auf andere art anstatt nur tiktok."
|
||
|
||
SIE STEHT VOR "MITMACHEN" (getauscht am 24.09.2026 auf Ansage:
|
||
"links soll unterstützen sein, in der mitte dan mittmachen und
|
||
recht regeln & hilfe").
|
||
|
||
Mein erster Entwurf hatte sie dahinter, mit der Begruendung "erst
|
||
dazugehoeren, dann etwas beitragen". Die Ueberlegung war nicht
|
||
falsch -- sie war nur meine. Filipe sieht die drei Kacheln jeden
|
||
Tag nebeneinander und entscheidet, was links steht; eine
|
||
Herleitung ersetzt das nicht.
|
||
|
||
GANZ VORNE STEHT SIE TROTZDEM NICHT: Die vorletzte Reihe ist weit
|
||
genug vom Eingang weg, dass niemand mit einer hohlen Hand begruesst
|
||
wird.
|
||
|
||
WARUM SIE KEIN BRETT IST: Die Bretter (bereich.html?b=...) sind
|
||
Listen, auf die jemand schreibt. Hier schreibt niemand — hier
|
||
stehen Wege, und genau einer darf sie aendern. Deshalb eine eigene
|
||
Seite, wie bei "Draussen" und "Regeln & Hilfe".
|
||
|
||
TON 45 ist GERECHNET, nicht gegriffen: tools/kachel-farbe-
|
||
einzeln.mjs gegen alle 44 vorhandenen Toene. Die Zahl steht
|
||
darunter im Kommentar der CSS-Zeile.
|
||
|
||
RASTERRECHNUNG (drei Spalten, "Willkommen" belegt zwei): Mit
|
||
dieser Kachel sind es fuer die Community 11 + 1 = 12 Plaetze,
|
||
also genau vier volle Reihen. Vorher waren es elf und damit eine
|
||
Luecke am Ende. */
|
||
{ ...TREFF_GRUPPE, name: "Unterst\u00fctzen", unter: "Dogi etwas zur\u00fcckgeben \u2013 auch ohne Geld",
|
||
zeichen: "unterstuetzen", ton: 45, ziel: "unterstuetzen.html", szene: "lounge" },
|
||
{ ...TREFF_GRUPPE, name: "Mitmachen", unter: "Wie du dazugeh\u00f6ren kannst",
|
||
zeichen: "personen", ton: 32, ziel: "bereich.html?b=mitmachen", szene: "garage" },
|
||
/* UNSERE SEITEN (19.09.2026) -- die einzige Kachel, die HINAUSFUEHRT.
|
||
|
||
Filipe: "ich brauch im community bereich noch eine kachel, die wenn
|
||
man drauf dr\u00fcckt dan kacheln hat die auf die website von dogfather
|
||
universe geht ... und die shop site von vanvan."
|
||
|
||
SIE IST KEIN BRETT, sondern eine Seite -- deshalb ein Dateiname als
|
||
Ziel und kein "bereich.html?b=...". Die Ableitung von TREFF_BRETTER
|
||
weiter unten nimmt sie dadurch von selbst nicht mit, genau wie die
|
||
Moderationskachel. Eine zweite Liste, die man pflegen muesste,
|
||
entsteht nicht.
|
||
|
||
UND SIE STEHT AM ENDE. Die sieben davor fuehren nach innen: lesen,
|
||
schreiben, mitmachen. Diese eine verlaesst den Treff -- das ist der
|
||
letzte Schritt, nicht der erste.
|
||
|
||
RASTERRECHNUNG (drei Spalten, "Das Rudel" belegt zwei): Mit dieser
|
||
Kachel sind es fuer die Community 7 + 1 Kacheln = 9 Plaetze, also
|
||
genau drei volle Reihen. Vorher waren es acht Plaetze und damit
|
||
eine Luecke am Ende. Sie passt also nicht nur inhaltlich. */
|
||
{ ...TREFF_GRUPPE, name: "Regeln & Hilfe", unter: "Wie es hier l\u00e4uft",
|
||
/* ZEIGT JETZT AUF DIE SEITE, DIE DIE ANTWORT HAT (19.09.2026).
|
||
|
||
Sie fuehrte auf `bereich.html?b=regeln` -- ein Brett, auf das die
|
||
Leitung Regeln eintragen KANN und das am ersten Tag leer ist
|
||
("Hier stehen die Regeln des Treffs -- sobald sie eingetragen
|
||
sind.").
|
||
|
||
Die Regeln, die es WIRKLICH gibt, stehen auf `treff-regeln.html`:
|
||
Begruessung, die fuenf Regeln, die drei Stufen, das Mindestalter,
|
||
was bei Aerger zu tun ist. Diese Seite war von genau EINER Stelle
|
||
aus erreichbar -- einem kleinen Textlink unter der Unterzeile
|
||
einer Brettseite. Keine der elf Kacheln fuehrte hin.
|
||
|
||
Damit war die einzige "So laeuft das hier"-Seite des Hauses fuer
|
||
einen neuen Zuschauer praktisch unsichtbar, waehrend die Kachel,
|
||
die ihren Namen trug, auf eine leere Liste zeigte.
|
||
|
||
Das Brett gibt es weiter; treff-regeln.html verlinkt darauf. Ein
|
||
Ort fuer die Frage, nicht zwei. */
|
||
/* EIGENER TON (22.09.2026). "Draussen" traegt jetzt die Farbe,
|
||
die hier stand -- ausdruecklicher Wunsch. Zwei Kacheln mit
|
||
derselben Farbe in derselben Ansicht waeren genau das, was
|
||
screen4 abschafft. */
|
||
zeichen: "schutz", ton: 28, ziel: "treff-regeln.html", szene: "studio" },
|
||
];
|
||
|
||
/* MELDUNGEN & MASSNAHMEN -- FUER DAS TEAM, NICHT FUER DIE COMMUNITY
|
||
(11.09.2026).
|
||
|
||
Diese Kachel steht ausdruecklich NICHT in TREFF_BEREICHE. Die Liste
|
||
dort ist das, was ein Mitglied sieht; sie wird dem Team nur
|
||
angehaengt. Eine achte Kachel darin waere auch bei der Community
|
||
gelandet -- und zwar lautlos, denn sie saehe dort aus wie die
|
||
anderen sieben. Erst beim Klicken haette der Server 404 gesagt, und
|
||
das sieht aus wie ein kaputtes Programm.
|
||
|
||
SIE GEHOERT AUCH NICHT IN TREFF_BRETTER: Sie ist kein Brett mit
|
||
Eintraegen, sondern eine Seite. Das Ziel ist deshalb ein
|
||
Dateiname und kein "bereich.html?b=...", und die Ableitung weiter
|
||
unten nimmt sie von selbst nicht mit. */
|
||
/* EIGENE GRUPPE, NICHT DIE DER COMMUNITY (19.09.2026).
|
||
|
||
Filipe: "ich will das oben die sachen fuer dogfather sind, dan die
|
||
sachen fuer modis und dan community bereich."
|
||
|
||
Bis heute trug diese Kachel `...TREFF_GRUPPE` -- sie landete damit
|
||
IN der Gruppe \u201eCommunity" und dort, weil sie zuletzt angehaengt
|
||
wird, hinter allen sieben Brettern. Wer moderiert, musste also an
|
||
der ganzen Community vorbeiscrollen, um an seine Arbeit zu kommen.
|
||
|
||
Getrennt ist es auch inhaltlich richtig: Die sieben Bretter sind der
|
||
Raum der Zuschauer. Diese Kachel ist Arbeit AN diesem Raum -- und
|
||
die sieht nur, wer moderiert. */
|
||
const MODERATION_GRUPPE = {
|
||
gruppe: "Moderation",
|
||
gruppeUnter: "Die Arbeit am Rudel \u2013 sieht nur, wer moderiert",
|
||
};
|
||
|
||
const TREFF_MODERATION_KACHEL = {
|
||
...MODERATION_GRUPPE,
|
||
name: "Meldungen & Ma\u00dfnahmen", unter: "Was gemeldet wurde \u2013 und was daraus wurde",
|
||
/* EIGENES ZEICHEN, NICHT DASSELBE WIE "REGELN & HILFE" (17.09.2026).
|
||
|
||
Hier stand zweimal `schutz` -- zwei Kacheln nebeneinander mit
|
||
demselben Schild. Auf dem Bildschirmfoto sind sie nicht zu
|
||
unterscheiden, und genau daran erkennt man eine Sammlung, die
|
||
niemand als Ganzes angesehen hat.
|
||
|
||
`startcheck` ist eine abgehakte Liste und sagt den Unterschied:
|
||
Bei "Regeln & Hilfe" steht, was GILT. Hier steht, was daraus
|
||
WURDE. */
|
||
zeichen: "startcheck", ton: 36, ziel: "treff-moderation.html", szene: "studio",
|
||
};
|
||
|
||
/* Welche Bretter zum Treff gehoeren -- ABGELEITET aus den Kacheln, nicht
|
||
danebengeschrieben. Eine zweite Liste waere die, die beim naechsten
|
||
neuen Brett vergessen wird. */
|
||
export const TREFF_BRETTER = [...new Set([
|
||
...TREFF_BEREICHE
|
||
.map((k) => new URL("http://x/" + k.ziel).searchParams.get("b"))
|
||
.filter(Boolean),
|
||
/* EIN BRETT OHNE KACHEL (berichtigt 20.09.2026).
|
||
----------------------------------------------------------------
|
||
Die Liste wird aus den Kachelzielen abgeleitet, und das ist
|
||
richtig: Was niemand anklicken kann, muss auch nicht offenstehen.
|
||
|
||
Am 19.09.2026 bekam die Kachel „Regeln & Hilfe" ein neues Ziel --
|
||
`treff-regeln.html` statt `bereich.html?b=regeln`, weil die
|
||
Kachel vorher auf ein leeres Brett zeigte. Damit fiel `regeln`
|
||
aus dieser Ableitung heraus, und das BRETT war ab da fuer die
|
||
Community gesperrt (404).
|
||
|
||
Nur verweist `treff-regeln.html` weiterhin darauf: „Haeufige
|
||
Fragen und Antworten stehen auf dem Brett Regeln & Hilfe." Ein
|
||
Verweis, der seit einem Tag ins Leere fuehrt -- und zwar auf der
|
||
einen Seite, die einem neuen Mitglied alles erklaeren soll.
|
||
|
||
Gefunden hat es `pruef-treff` (4 Fehler, seit dem 19.09.), nicht
|
||
ein Mensch. Deshalb steht es hier ausdruecklich und mit Grund:
|
||
Ein Brett kann erreichbar sein muessen, ohne eine eigene Kachel
|
||
zu haben. Wer die Ableitung liest und diese Zeile nicht sieht,
|
||
haelt die Liste fuer vollstaendig. */
|
||
"regeln",
|
||
|
||
/* UND DASSELBE EIN DRITTES MAL (21.09.2026) -- deshalb steht unten
|
||
jetzt eine Pruefung, die es laut macht.
|
||
----------------------------------------------------------------
|
||
Am 21.09. wurde die Kachel "Das Rudel" zur Willkommensseite
|
||
(Auftrag Abschnitt 7). Damit fiel `treff` aus dieser Ableitung.
|
||
|
||
WAS DAS ANRICHTETE, ist schlimmer als beim letzten Mal: Die
|
||
Schranke in workspace-bereiche.js faengt Aussenrollen und
|
||
Team-Dogi-Rollen ab, laesst eine AGENTURROLLE aber unbesehen
|
||
durch ("if (!TEAM_DOGI_ROLLEN.has(rolle)) return next()"). Solange
|
||
`treff` hier stand, fing der Zweig darueber sie ab -- danach nicht
|
||
mehr. Eine Creatorin konnte das Brett des Treffs lesen.
|
||
|
||
Gefunden hat es wieder pruef-treff, nicht das Nachdenken. Und
|
||
wieder war es kein neuer Fehler, sondern derselbe: Ein Brett
|
||
gehoert zum Treff, WEIL es dazugehoert -- nicht, weil gerade
|
||
jemand eine Kachel darauf zeigen laesst. */
|
||
"treff",
|
||
])];
|
||
|
||
/* Wer den Treff ueberhaupt sehen darf: die Community selbst und das
|
||
Team von Dogi. Die Agentur ausdruecklich nicht -- Creator, Scouts und
|
||
Spicy Media haben mit den Zuschauern dieses Streams nichts zu tun. */
|
||
export const TREFF_ROLLEN = new Set(["gast", "modi", "hand", "linke", "admin"]);
|
||
|
||
/* DER VERTRAULICHE MELDEWEG (19.09.2026).
|
||
|
||
Eigene Gruppe, nicht bei den Brettern: Auf den sieben Brettern
|
||
schreibt man oeffentlich. Hier schreibt man an genau zwei Menschen.
|
||
Zwischen den anderen Kacheln wuerde das verschwimmen -- und wer ein
|
||
Problem hat, sucht nicht lange.
|
||
|
||
SIE STEHT VOR "FUER DICH": Ein Problem ist dringender als der eigene
|
||
Steckbrief. Die Reihenfolge innerhalb der Gruppe "Fuer dich" ergibt
|
||
sich aus der Liste, die Sortierung in start.js fasst beide ans Ende. */
|
||
/* DER SUPPORT (24.09.2026).
|
||
|
||
Filipe: „da fehlt auch eine support kachel. die jeder sieht. jeder
|
||
benutzen kann. jeder kann da probleme von der seite melden und
|
||
hinweisen. fotos mit schicken mit einem text."
|
||
|
||
SIE STEHT NICHT IN EINER ROLLENLISTE, sondern wird in
|
||
`bereicheFuer()` an JEDE angehaengt -- siehe dort. Sie in fuenf
|
||
Listen einzutragen waere fuenf Gelegenheiten, sie bei der sechsten
|
||
Rolle zu vergessen; und genau so ist heute der
|
||
Benachrichtigungs-Knopf auf 14 Seiten verschwunden.
|
||
|
||
TON 44: gezaehlt in start.css, wo die Farben wirklich stehen (bis
|
||
43 vergeben). Danach laeuft `node tools/kachel-farben.mjs
|
||
--schreiben`, das alle Toene neu gegeneinander rechnet.
|
||
|
||
ZEICHEN "schutz" wie beim vertraulichen Meldeweg -- beides sind
|
||
Wege, auf denen man sich meldet, wenn etwas nicht stimmt. Ein
|
||
eigenes Zeichen zu erfinden hiesse, zwei Dinge zu unterscheiden,
|
||
die sich fuer den Benutzer gleich anfuehlen. */
|
||
const SUPPORT_KACHEL = {
|
||
/* SIE GEHOERT ZU "FUER DICH" (nachgetragen 24.09.2026).
|
||
|
||
Filipe, mit zwei Bildschirmfotos: "die kachel auf screen 2 soll
|
||
zwischen den kacheln auf screen3 sein bitte" -- also zwischen
|
||
"Wer sieht was" und "Vertraulich melden".
|
||
|
||
Dabei fiel ein zweiter Fehler auf, den er gar nicht gemeldet hat:
|
||
Die Kachel hatte GAR KEINE Gruppe. Sie stand deshalb in einem
|
||
eigenen Abschnitt ohne Ueberschrift, allein, mit einem
|
||
Aufklapp-Kopf, auf dem nur "1" stand -- bei JEDER Rolle. Gebaut
|
||
habe ich das heute Morgen, und im Bildschirmfoto war es zu sehen;
|
||
mir ist es nicht aufgefallen, weil ich auf die Kachel geschaut
|
||
habe und nicht auf das, was um sie herum steht.
|
||
|
||
"Deine eigene Seite" ist derselbe Untertitel wie beim
|
||
vertraulichen Meldeweg -- und das ist kein Zufall: Bei der
|
||
Community steht der Support VOR ihm und waere sonst die erste
|
||
Kachel der Gruppe, deren Untertitel dann fehlte. */
|
||
gruppe: "Für dich", gruppeUnter: "Deine eigene Seite",
|
||
name: "Support", unter: "Etwas kaputt? Schreib es mit Bild",
|
||
zeichen: "schutz", ton: 44, ziel: "support.html", szene: "studio",
|
||
};
|
||
|
||
const HILFE_KACHEL = {
|
||
gruppe: "Für dich", gruppeUnter: "Deine eigene Seite",
|
||
name: "Vertraulich melden", unter: "Nur DogFather und die rechte Hand",
|
||
/* KNALLROT -- AUSDRUECKLICH DIESE KACHEL (berichtigt 22.09.2026).
|
||
==================================================================
|
||
Filipe: "nicht die kachel draussen soll rot sein sondern die
|
||
kachel vertraulich melden nur die soll knall rot sein!!!"
|
||
|
||
Ich hatte screen5 und screen6 vertauscht und das Rot auf
|
||
"Draussen" gelegt. Hier sitzt es richtig: Ein vertraulicher
|
||
Meldeweg ist das, was jemand im schlimmsten Moment sucht -- und
|
||
dann zaehlt, dass man ihn auf der Wand sofort findet, ohne zu
|
||
lesen. Rot ist hier die Aussage, nicht die Dekoration.
|
||
|
||
#ff1a1a, voll gesaettigt, Kontrast 4,84:1 gegen den Grund.
|
||
Ton 38 ist GESETZT: pruef-kachelfarben wird rot, wenn ein
|
||
Farblauf ihn anfasst. */
|
||
zeichen: "schutz", ton: 38, ziel: "hilfe.html", szene: "studio",
|
||
};
|
||
|
||
/* DAS TEAM SIEHT DEN TREFF MIT -- als eigene Gruppe unter seinen
|
||
uebrigen Kacheln.
|
||
|
||
Filipe: "die modis rechte hand und ich also dogfather sehen die aber
|
||
auch. die community aber nur die dann."
|
||
|
||
Zusammengesetzt und nicht in MODI_BEREICHE hineingeschrieben, weil
|
||
diese Liste weiter oben steht als die Kacheln des Treffs. Eine Kopie
|
||
dort waere die Stelle, an der beim naechsten neuen Brett eines fehlt. */
|
||
/* =====================================================================
|
||
WAS DER MODI UEBER SICH SELBST SIEHT (19.09.2026)
|
||
|
||
GEFUNDEN BEIM DURCHGEHEN DER SEITE AUS BENUTZERSICHT, und es war
|
||
mein eigener Fehler vom Vortag: `entwicklung.html` und
|
||
`werdegang.html` haben BEIDE eine ausgearbeitete Eigensicht --
|
||
entwicklung.js zeigt die eigene Karte, werdegang.js die eigene
|
||
Bewegung. Die Rechtetafel laesst den Modi auf beide Seiten
|
||
(rechte.js).
|
||
|
||
Nur: In seiner Kachelliste stand keine von beiden. Der Weg dorthin
|
||
existierte fuer ihn nicht -- er haette die Adresse von Hand tippen
|
||
muessen. Die ganze Eigensicht war damit toter Code, ausgerechnet
|
||
fuer den Menschen, fuer den sie geschrieben wurde.
|
||
|
||
ZWEI KACHELN UND NICHT DIE DER LEITUNG. Dieselben Ziele, aber die
|
||
Leitungstexte passen nicht: "Wie sich jemand ueber die Zeit bewegt"
|
||
liest sich, als ginge es um andere Leute. Aus seiner Sicht geht es
|
||
um ihn.
|
||
|
||
UND SIE LOESEN EINE NAMENSKOLLISION. Bis heute setzten BEIDE Seiten
|
||
fuer ihn die Ueberschrift "Deine Entwicklung" -- wer aus der Glocke
|
||
kam, konnte nicht sagen, auf welcher er stand. Jetzt heisst der
|
||
Katalog "Deine Aufgaben" (passend zur Kachel "Eure Aufgaben") und
|
||
die Auswertung "Deine Entwicklung". Ein Name, eine Sache.
|
||
|
||
ABGELEITET, NICHT ABGESCHRIEBEN: Ziel, Zeichen und Ton kommen aus
|
||
den Kacheln der Leitung. Wer dort das Ziel aendert, aendert es hier
|
||
mit -- zwei Kachelobjekte mit demselben Ziel waeren sonst zwei
|
||
Gelegenheiten, eines davon zu vergessen. */
|
||
const FUER_DICH = {
|
||
gruppe: "Für dich", gruppeUnter: "Deine Seite im Team",
|
||
};
|
||
const MEINE_AUFGABEN_KACHEL = {
|
||
...FUER_DICH,
|
||
zeichen: ENTWICKLUNG_KACHEL.zeichen, ton: ENTWICKLUNG_KACHEL.ton,
|
||
ziel: ENTWICKLUNG_KACHEL.ziel, szene: ENTWICKLUNG_KACHEL.szene,
|
||
name: "Deine Aufgaben", unter: "Was du kannst – und was als Nächstes kommt",
|
||
};
|
||
const MEINE_ENTWICKLUNG_KACHEL = {
|
||
...FUER_DICH,
|
||
zeichen: WERDEGANG_KACHEL.zeichen, ton: WERDEGANG_KACHEL.ton,
|
||
ziel: WERDEGANG_KACHEL.ziel, szene: WERDEGANG_KACHEL.szene,
|
||
name: "Deine Entwicklung", unter: "Was sich bei dir bewegt hat – ohne Note",
|
||
};
|
||
|
||
/* DER MODI BEKOMMT DENSELBEN BEREICH WIE DIE LEITUNG (22.09.2026).
|
||
|
||
Filipe zum Bildschirmfoto der Gruppe "Entwicklung & Nachwuchs":
|
||
"die modis sollen diesen bereich und diese zwei kacheln auch sehen,
|
||
die sollen aber nur ihre aufgaben und daten sehen."
|
||
|
||
DIESELBEN ZWEI KACHELN, NICHT EIGENE MIT ANDEREM NAMEN. Bis heute
|
||
standen hier MEINE_AUFGABEN_KACHEL und MEINE_ENTWICKLUNG_KACHEL --
|
||
zwei Kacheln mit denselben Zielen und anderen Namen. Zwei Namen fuer
|
||
dieselbe Sache ist genau der Fehler, der am 19.09. schon einmal zwei
|
||
Kacheln "Chat" ergeben hat: Man klickt den falschen und sucht den
|
||
Unterschied.
|
||
|
||
"TALENTE" BLEIBT DRAUSSEN. Dort stehen Notizen ueber Zuschauer, die
|
||
nichts davon wissen -- das ist etwas anderes als die eigene
|
||
Entwicklung. Der Riegel dafuer sitzt ohnehin in rechte.js; diese
|
||
Zeile sorgt nur dafuer, dass gar nicht erst eine Kachel dorthin
|
||
zeigt.
|
||
|
||
DASS ER DORT NUR SICH SELBST SIEHT, entscheiden die Seiten: In
|
||
workspace-werdegang.js steht `leitung ? alle : []`, in
|
||
workspace-entwicklungs-punkte.js dasselbe. Die Kachel oeffnet die
|
||
Seite -- was darauf steht, entscheidet der Server. */
|
||
/* =====================================================================
|
||
CALLS & PROTOKOLLE GEHOEREN NEBEN JEDEN KALENDER (22.09.2026)
|
||
=====================================================================
|
||
Filipe: „ich will diese kachel vom workspace auch im team dogi seite.
|
||
ich will dass jeder immer nur seine eigenen sachen von seinem
|
||
kalender sieht mehr nicht. perfektioniere das bitte. jeder der einen
|
||
kalender hat soll auch sowas haben danke."
|
||
|
||
GEMESSEN, BEVOR GEBAUT WURDE:
|
||
|
||
Rolle Kacheln Kalender Calls
|
||
admin 31 ja NEIN
|
||
hand 31 ja NEIN
|
||
linke 30 ja NEIN
|
||
modi 26 ja NEIN
|
||
gast 12 nein nein
|
||
|
||
Vier Listen mit Kalender, keine mit Calls -- und die Community hat
|
||
keinen Kalender und braucht deshalb auch keine Calls. Sein Satz
|
||
„jeder der einen kalender hat" beschreibt die Lage also genau.
|
||
|
||
DESHALB IST ES EINE REGEL UND KEINE VIERFACHE EINTRAGUNG. Vier
|
||
Stellen von Hand zu pflegen waere die Stelle, an der es beim
|
||
naechsten neuen Rollenzuschnitt auseinanderlaeuft: Einer bekommt
|
||
einen Kalender, und die Calls vergisst man.
|
||
|
||
DIE GRUPPE WIRD MITGENOMMEN, nicht danebengeschrieben: Die neue
|
||
Kachel erbt `gruppe` und `gruppeUnter` vom Kalender, neben dem sie
|
||
steht. Zieht der Kalender eines Tages in eine andere Gruppe, zieht
|
||
sie mit.
|
||
|
||
DER TON IST GERECHNET, NICHT UEBERNOMMEN -- und der erste Versuch
|
||
war falsch.
|
||
|
||
Naheliegend war Ton 15, derselbe wie im anderen Haus: dieselbe
|
||
Sache, dieselbe Farbe. Als Zahl war er frei. Auf dem BILDSCHIRM
|
||
nicht: pruef-kachelfarben misst den Abstand zwischen je zwei
|
||
Kacheln an echten Bildpunkten, und 15 lag bei 0,0139 zu „Was
|
||
ansteht" -- unter der Grenze von 0,02. Zwei Kacheln, die gleich
|
||
aussehen, sind eine Kachel zu viel.
|
||
|
||
Also ausgerechnet, welcher freie Ton am weitesten von ALLEN
|
||
benutzten wegliegt (OKLab-Abstand):
|
||
|
||
Ton 22 #fee0b2 0,1135 aber Buntheit nur 0,068 -> zu blass
|
||
Ton 7 #098f71 0,1067 Buntheit 0,112 -> zu blass
|
||
Ton 16 #19b2c2 0,0854 Abstand unter 0,090 -> zu nah
|
||
Ton 15 #01d7e3 0,0279 (der erste Versuch)
|
||
|
||
KEINER DER FREIEN TOENE ERFUELLTE ALLE HAUSREGELN. Die Palette
|
||
kannte 42 Toene, 31 davon im Einsatz -- und unter den elf uebrigen
|
||
war keiner, der gleichzeitig weit genug weg (>= 0,090) und bunt
|
||
genug (>= 0,12) ist. Eine 32. Kachel braucht einen 32. Ton.
|
||
|
||
Also einen NEUEN gerechnet statt einen vorhandenen umzufaerben:
|
||
Ton 16 gehoert im anderen Haus zu „Start-Check" -- ihn zu aendern
|
||
haette dort eine Kachel umgefaerbt, nach der niemand gefragt hat.
|
||
`tools/kachel-farbe-einzeln.mjs` haelt alle vorhandenen fest und
|
||
sucht fuer EINEN die Farbe, die am weitesten von allen wegliegt:
|
||
|
||
Ton 43 #027afb Abstand 0,0925 Buntheit 0,212 Kontrast 4,63:1
|
||
|
||
Alle drei Regeln erfuellt, und es bleibt ein Blau -- die Kachel ist
|
||
damit wiedererkennbar als dieselbe wie im anderen Haus.
|
||
===================================================================== */
|
||
const CALLS_KACHEL = {
|
||
name: "Calls & Protokolle",
|
||
unter: "Gespräche mit To-dos",
|
||
zeichen: "calls",
|
||
ton: 43,
|
||
ziel: "calls.html",
|
||
szene: "lounge",
|
||
};
|
||
|
||
/** Hinter jeden Kalender einer Liste eine Calls-Kachel setzen. */
|
||
function mitCalls(liste) {
|
||
const raus = [];
|
||
for (const k of liste) {
|
||
raus.push(k);
|
||
/* Am ZIEL erkannt, nicht am Namen: Der Name einer Kachel ist im
|
||
Haus schon mehrfach gewandert, das Ziel nicht. */
|
||
if (String(k.ziel || "").startsWith("kalender.html")) {
|
||
raus.push({ gruppe: k.gruppe, gruppeUnter: k.gruppeUnter, ...CALLS_KACHEL });
|
||
}
|
||
}
|
||
return raus;
|
||
}
|
||
|
||
const MODI_MIT_TREFF = mitCalls([...MODI_BEREICHE, BEFINDEN_KACHEL,
|
||
ENTWICKLUNG_KACHEL, WERDEGANG_KACHEL, ...TREFF_BEREICHE,
|
||
TREFF_MODERATION_KACHEL]);
|
||
const HAND_MIT_TREFF = mitCalls([...HAND_BEREICHE, ...TREFF_BEREICHE,
|
||
TREFF_MODERATION_KACHEL, HILFE_KACHEL]);
|
||
|
||
/* DIE LINKE HAND SIEHT "DEIN TEAM" NICHT (22.09.2026).
|
||
|
||
Filipe: "die rolle linke hand soll die kachel dein team auch nicht
|
||
sehen bitte danke."
|
||
|
||
ABGELEITET, NICHT NACHGEBAUT: Ihre Liste ist die der rechten Hand
|
||
MINUS dieser einen Kachel. Eine zweite, eigene Liste waere die, die
|
||
beim naechsten neuen Bereich auseinanderlaeuft -- dann haette sie
|
||
ihn, und die linke Hand nicht, und niemand wuesste warum.
|
||
|
||
Erkannt wird die Kachel an ihrem ZIEL, nicht an ihrem Namen: Der
|
||
Name ist am 17.09. schon einmal gewandert ("Eingang" -> "Dein
|
||
Team"), das Ziel nicht. */
|
||
const LINKE_MIT_TREFF = HAND_MIT_TREFF.filter((k) => k.ziel !== EINGANG_KACHEL.ziel);
|
||
|
||
/* AUSSEN_ROLLEN kommt aus crew-adresse.js und wird hier nur
|
||
weitergereicht -- die Adressregel braucht sie zuerst, und diese Datei
|
||
importiert von dort. Eine zweite Menge waere die, die auseinanderlaeuft. */
|
||
export { AUSSEN_ROLLEN };
|
||
|
||
/** Darf diese Person telefonieren?
|
||
*
|
||
* HIER UND NICHT AUS workspace-treffchat.js IMPORTIERT: Jene Datei
|
||
* importiert aus dieser. Ein Import zurueck waere ein Ring, und Ringe
|
||
* halten in JavaScript genau so lange, bis jemand eine Konstante auf
|
||
* oberster Ebene benutzt. `workspace-treffchat.js` reicht diese
|
||
* Funktion deshalb nur WEITER -- gerechnet wird sie an einer Stelle,
|
||
* naemlich hier. */
|
||
export function darfTelefonieren(person) {
|
||
return !AUSSEN_ROLLEN.has(person?.rolle);
|
||
}
|
||
|
||
/* =====================================================================
|
||
KACHEL UND TUER GEHOEREN ZUSAMMEN (11.09.2026)
|
||
=====================================================================
|
||
|
||
Seit DogFather die Rechtetafel von Hand umstellen kann, kann eine
|
||
Seite zugehen, ohne dass ihre Kachel verschwindet. Gefunden hat das
|
||
pruef-rechte-umstellen mit einer einzigen Zeile: Er nahm einem Modi
|
||
chat.html weg -- die Schranke griff sofort, und die Kachel stand
|
||
weiterhin da. Ein Knopf, der auf die Startseite zurueckwirft.
|
||
|
||
Der Satz "Kachel und Tuer gehoeren zusammen" steht seit dem
|
||
06.09.2026 in assets/js/bereiche.js. Bis heute war er eine Bitte an
|
||
den, der beides pflegt. Jetzt ist er eine Rechnung: Die Kachelliste
|
||
wird gegen dieselbe Tafel gefiltert, aus der die Schranke ihre
|
||
Entscheidung holt.
|
||
|
||
NACH DEM PFAD UND NICHT NACH DEM GANZEN ZIEL: "bereich.html?b=live"
|
||
ist die Seite bereich.html. Wer die Frage am Fragezeichen nicht
|
||
abschneidet, filtert jede Brett-Kachel weg -- lautlos, denn eine
|
||
fehlende Kachel sieht aus wie eine Rolle, die sie eben nicht hat.
|
||
|
||
WAS KEIN ZIEL HAT, BLEIBT. Eine Kachel ohne "ziel" ist kein Weg zu
|
||
einer Seite, und "ich kenne die Seite nicht" darf nicht "also weg
|
||
damit" heissen. */
|
||
function nurOffeneKacheln(liste, person) {
|
||
if (!Array.isArray(liste) || !person) return liste;
|
||
return liste.filter((k) => {
|
||
/* EINE KACHEL, DIE HINAUSFUEHRT, HAT HIER KEINE SEITE (21.09.2026).
|
||
|
||
Unten wird aus dem Ziel ein Pfad gebaut ("/workspace/" + ziel)
|
||
und die Rechtetafel gefragt. Bei einer vollstaendigen Adresse --
|
||
der Tuer zu Team Dogi -- kaeme dabei
|
||
"/workspace/https://crew..." heraus, das steht in keiner Tafel,
|
||
und die Kachel waere lautlos verschwunden. Lautlos ist das
|
||
Schlimme daran: Man sucht den Fehler dann bei der Rolle. */
|
||
if (k?.aussen) return true;
|
||
const ziel = String(k?.ziel || "");
|
||
if (!ziel) return true;
|
||
const pfad = "/workspace/" + ziel.split("?")[0];
|
||
return darfSeite(person, pfad);
|
||
});
|
||
}
|
||
|
||
/* JEDER HAT EINEN STECKBRIEF -- AUCH DIE COMMUNITY (19.09.2026).
|
||
|
||
Filipe: "jeder soll ein steckbrief haben, jeder der ein account hat
|
||
... jeder soll genau wie ich foto und so hinzufuegen koennen."
|
||
|
||
Das Team hatte die Kachel laengst (sie steht in MODI_BEREICHE unter
|
||
„Für dich"), die Community nicht -- und damit die groesste Gruppe
|
||
ueberhaupt. Die Seite selbst konnte es die ganze Zeit: `/mein` fragt
|
||
nach der eigenen Nummer und kennt gar keine Rolle.
|
||
|
||
EIGENE FASSUNG DES UNTERTEXTS: „Deine Seite im Team" waere fuer ein
|
||
Mitglied falsch -- es ist nicht im Team. Der Steckbrief ist fuer sie
|
||
das, was man von sich zeigt. */
|
||
const STECKBRIEF_KACHEL_TREFF = {
|
||
gruppe: "Für dich", gruppeUnter: "Deine eigene Seite",
|
||
name: "Mein Steckbrief", unter: "Dein Bild und deine Kanäle",
|
||
zeichen: "steckbrief", ton: 10, ziel: "steckbrief.html", szene: "halle",
|
||
};
|
||
|
||
/* Die Community: der Treff und ihre eigene Seite. Sonst nichts. */
|
||
const TREFF_MIT_EIGENEM = [...TREFF_BEREICHE, HILFE_KACHEL, STECKBRIEF_KACHEL_TREFF];
|
||
|
||
/* DIE SUPPORT-KACHEL BEKOMMT JEDER -- an genau einer Stelle
|
||
angehaengt (24.09.2026).
|
||
|
||
`bereicheFuer` gibt je nach Rolle eine von fuenf Listen zurueck,
|
||
dazu `null` ("nimm die Liste aus dem Browser") und `[]` ("nichts").
|
||
Die Kachel in jede Liste einzutragen waeren fuenf Aenderungen und
|
||
fuenf kuenftige Gelegenheiten, eine zu vergessen. Stattdessen
|
||
haengt diese Huelle sie an das an, was herauskommt.
|
||
|
||
`null` UND `[]` BLEIBEN UNANGETASTET, und das ist Absicht:
|
||
`null` heisst "der Browser hat die Liste" -- dort steht sie in
|
||
assets/js/bereiche.js und wird dort ergaenzt. `[]` heisst "diese
|
||
Rolle bekommt gar nichts"; ihr eine einzelne Kachel zu geben waere
|
||
die Ausnahme, die spaeter niemand mehr erklaeren kann. */
|
||
export function bereicheFuer(person) {
|
||
const liste = bereicheRoh(person);
|
||
if (liste === null || !Array.isArray(liste) || !liste.length) return liste;
|
||
if (liste.some((k) => k?.ziel === "support.html")) return liste;
|
||
|
||
/* VOR DEN VERTRAULICHEN MELDEWEG, NICHT ANS ENDE (24.09.2026).
|
||
|
||
Filipe: "die kachel auf screen 2 soll zwischen den kacheln auf
|
||
screen3 sein bitte" -- zwischen "Wer sieht was" und "Vertraulich
|
||
melden".
|
||
|
||
GESUCHT WIRD DER NACHBAR, NICHT EINE STELLE. Eine feste Zahl
|
||
("an Position 12 einschieben") waere beim naechsten Umbau still
|
||
falsch -- dieselbe Ueberlegung wie bei vorGruppe() weiter oben.
|
||
Gefunden wird ueber das ZIEL der Nachbarkachel; das aendert sich
|
||
nicht, wenn jemand ihren Namen umschreibt.
|
||
|
||
FINDET SICH DER NACHBAR NICHT, wird angehaengt. Ein Modi hat den
|
||
vertraulichen Meldeweg nicht -- bei ihm steht der Support dann am
|
||
Ende von "Fuer dich", und das ist richtig: Er ist dort die
|
||
letzte der drei eigenen Seiten. Eine Kachel wegzulassen, weil
|
||
eine Reihenfolge nicht aufgeht, waere der schlechtere Tausch. */
|
||
const nachbar = liste.findIndex((k) => k?.ziel === HILFE_KACHEL.ziel);
|
||
return nachbar < 0
|
||
? [...liste, SUPPORT_KACHEL]
|
||
: [...liste.slice(0, nachbar), SUPPORT_KACHEL, ...liste.slice(nachbar)];
|
||
}
|
||
|
||
function bereicheRoh(person) {
|
||
if (person?.rolle === "modi") return MODI_MIT_TREFF;
|
||
/* Die linke Hand vor der allgemeinen Hand-Regel -- sonst faenge
|
||
`istHand()` sie ab, und der Filter darunter liefe nie. */
|
||
if (person?.rolle === "linke") return LINKE_MIT_TREFF;
|
||
if (istHand(person)) return HAND_MIT_TREFF;
|
||
/* DIE COMMUNITY SIEHT DEN TREFF UND SONST NICHTS (11.09.2026) --
|
||
seit dem 19.09. plus den eigenen Steckbrief. */
|
||
if (person?.rolle === "gast") return TREFF_MIT_EIGENEM;
|
||
/* 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. */
|
||
/* Auf der Team-Adresse sehen DogFather und die rechte Hand dieselben
|
||
Kacheln. Eine eigene Liste fuer ihn waere die, die beim naechsten
|
||
Umbau auseinanderlaeuft -- und sie stuende gegen die Hausregel,
|
||
ihn nicht ueber sein Team zu stellen. */
|
||
if (person?.haus === "crew") return HAND_MIT_TREFF;
|
||
/* `null` HEISST "NIMM DIE LISTE AUS DEM BROWSER" -- und die enthaelt
|
||
fuenfundzwanzig Kacheln des Agenturhauses. Bis zum 11.09.2026 stand
|
||
hier nur `return null`, also bekam JEDE Rolle ohne eigenen Zweig
|
||
diese Liste angeboten. Gefiltert wurde sie danach zwar nach
|
||
`rollen:` an jeder Kachel, aber das ist eine zweite Schranke an
|
||
einer anderen Stelle -- und sie lebt in einer Datei, die jeder
|
||
herunterlaedt.
|
||
|
||
Jetzt bekommen nur die fuenf Rollen `null`, fuer die diese Liste
|
||
geschrieben wurde. Alles andere bekommt eine leere Liste: keine
|
||
Kachel, kein Weg, nichts zu filtern. */
|
||
if (AGENTUR_LISTE.has(person?.rolle)) return null;
|
||
return [];
|
||
}
|
||
|
||
/* Die fuenf, deren Kacheln in assets/js/bereiche.js stehen. Nicht zu
|
||
verwechseln mit AGENTUR_ROLLEN (dort fehlt `admin`, weil es dabei um
|
||
die Sichtbarkeit von Zeilen geht und nicht um Kacheln). */
|
||
const AGENTUR_LISTE = new Set(["spicy", "admin", "manager", "scout", "creator"]);
|
||
|
||
/* WELCHE BEREICHSSEITEN EIN MODI OEFFNEN DARF -- abgeleitet aus seinen
|
||
eigenen Kacheln und nicht danebengeschrieben.
|
||
|
||
Eine zweite Liste waere die Stelle, an der es auseinanderlaeuft: Wer
|
||
eine Kachel hinzufuegt und den Eintrag vergisst, baut einen Knopf,
|
||
der auf die Startseite zurueckwirft. Wer eine Kachel entfernt und den
|
||
Eintrag vergisst, laesst eine Tuer offen. So kann beides nicht
|
||
passieren.
|
||
|
||
Gebraucht wird sie, weil `bereich.html` seit heute fuer ihn offen ist
|
||
-- ohne diese Einschraenkung kaeme er ueber die Adresszeile auch in
|
||
die Agentur-Ablage, die ihn nichts angeht. */
|
||
/* EINE KACHEL IST NICHT MEHR DER EINZIGE WEG ZU EINEM BRETT
|
||
(11.09.2026, gefunden von pruef-entwicklung).
|
||
|
||
Die Ableitung oben liest die Bretter aus den Kachelzielen. Das war
|
||
richtig, solange jede Kachel auf ein Brett zeigte. Seit „Entwicklung"
|
||
und „Talente" auf ihre KATALOGSEITEN zeigen, fielen genau diese
|
||
beiden Bretter aus der Liste -- und die rechte Hand bekam auf dem
|
||
Notizbrett, das die Katalogseite selbst verlinkt, ein 404.
|
||
|
||
Kein Fehler im Riegel, sondern in seiner QUELLE: Die Frage "welche
|
||
Bretter darf diese Rolle oeffnen" laesst sich nicht mehr allein aus
|
||
den Kacheln beantworten, weil es jetzt Bretter gibt, die man ueber
|
||
eine Seite erreicht statt ueber eine Kachel.
|
||
|
||
Beides wird deshalb zusammengezogen -- und die zweite Liste ist
|
||
ausdruecklich KURZ und begruendet, nicht offen: Hier stehen nur die
|
||
Bretter, zu denen eine erlaubte SEITE verlinkt. Wer ein drittes
|
||
dazunimmt, ohne es einzutragen, bekommt dasselbe 404 und findet
|
||
diesen Absatz. */
|
||
/* Bretter, die kein eigenes Kachelziel haben, sondern von einer SEITE
|
||
aus verlinkt sind -- das Notizbrett neben dem Katalog.
|
||
|
||
AUS EINER LISTE WURDE EINE ZUORDNUNG (19.09.2026), und das war ein
|
||
echtes Loch, kein Schoenheitsfehler:
|
||
|
||
Der Satz darueber lautete "Wer die Seite oeffnen darf, muss auch
|
||
dorthin kommen." Der Gedanke ist richtig -- nur stand er als feste
|
||
Liste da und fragte nie, WER gerade fragt. Ergebnis: Ein Modi kam
|
||
ueber `bereich.html?b=talente` an die Notizen zu Kandidaten, obwohl
|
||
`talente.html` fuer ihn ausdruecklich gesperrt ist. Die Begruendung
|
||
der Sperre steht in rechte.js und gilt hier genauso: "Hier stehen
|
||
Namen von Menschen, die nichts davon wissen."
|
||
|
||
Schlimmer noch: In workspace-bereiche.js stand ein Kommentar, der
|
||
das Gegenteil des Codes behauptete ("entwicklung, talente bleiben
|
||
404"). Ein falscher Kommentar an einer Rechtestelle ist schlimmer
|
||
als gar keiner -- der Naechste liest ihn und prueft nicht nach.
|
||
|
||
Jetzt steht neben jedem Brett die Seite, von der es abhaengt, und
|
||
gefragt wird `darfSeite()` -- dieselbe Tafel, die auch die Seite
|
||
selbst bewacht. Eine zweite Regel kann damit nicht auseinanderlaufen. */
|
||
const BRETTER_UEBER_SEITEN = {
|
||
entwicklung: "/workspace/entwicklung.html",
|
||
talente: "/workspace/talente.html",
|
||
};
|
||
|
||
/** Bretter mit eigener Kachel. Diese darf jeder aus dem Team, der die
|
||
* Kachel sieht -- sie sind daraus abgeleitet, nicht abgeschrieben. */
|
||
export const MODI_BEREICHE_ERLAUBT = new Set([
|
||
...HAND_MIT_TREFF
|
||
.map((k) => (/bereich\.html\?b=([a-z]+)/.exec(k.ziel) || [])[1])
|
||
.filter(Boolean),
|
||
/* DIESELBE LUECKE WIE BEI TREFF_BRETTER (20.09.2026), und aus
|
||
demselben Grund: `regeln` hat seit dem 19.09. keine Kachel mehr,
|
||
faellt damit aus jeder Ableitung ueber Kachelziele -- und ist
|
||
trotzdem ein Brett, das `treff-regeln.html` verlinkt.
|
||
|
||
Zwei Ableitungen, dieselbe blinde Stelle. Das ist der Grund,
|
||
warum hier eine Zeile steht statt einer zweiten klugen Regel:
|
||
Wer eine Kachel umleitet, nimmt einem Brett damit stillschweigend
|
||
die Erreichbarkeit. Das muss sichtbar sein, nicht gerechnet. */
|
||
"regeln",
|
||
/* Und `treff` aus demselben Grund (21.09.2026): Seine Kachel zeigt
|
||
seit heute auf die Willkommensseite. Ohne diese Zeile sieht ein
|
||
Modi sechs von sieben Treff-Brettern -- und das siebte ist
|
||
ausgerechnet das, das dem Bereich den Namen gibt. */
|
||
"treff",
|
||
]);
|
||
|
||
/** Und die Bretter hinter einer Seite -- nur fuer den, der die Seite darf.
|
||
*
|
||
* DER DRITTE AUSGANG FEHLT HIER ABSICHTLICH: Ein Brett, das nicht in
|
||
* der Zuordnung steht, ist keine offene Frage, sondern schlicht nicht
|
||
* gemeint. `false` ist die richtige Antwort. */
|
||
export function brettHinterSeiteOffen(person, brett) {
|
||
const seite = BRETTER_UEBER_SEITEN[brett];
|
||
return !!seite && darfSeite(person, seite);
|
||
}
|
||
|
||
/* Der Satz unter der Begruessung. Die fuenf bekannten Rollen haben ihn
|
||
in assets/js/start.js stehen ("Creator · dein eigener Bereich"); fuer
|
||
einen Modi darf er dort nicht stehen und kommt deshalb von hier.
|
||
|
||
OHNE IHN STAND DA NUR DAS WORT "Modi" -- neben fuenf Rollen, die einen
|
||
ganzen Satz bekommen. Das sieht nicht verborgen aus, sondern
|
||
unfertig, und Filipe haette zu Recht gefragt, warum seine Seite
|
||
halbfertig wirkt. Gefunden hat das die Browserpruefung, nicht das
|
||
Nachdenken.
|
||
|
||
AUF AUGENHOEHE FORMULIERT: "dein Bereich im Team", nicht "dein
|
||
Bereich unter DogFather". Ein Modi ist Teil des Teams, kein
|
||
Untergebener -- das gilt fuer jeden Text im Haus. */
|
||
const MODI_ROLLENTEXT = "Modi · dein Bereich im Team";
|
||
|
||
/* DIE ZIERZEILE UEBER "DIE ZENTRALE".
|
||
|
||
Sie nennt den BETRIEB, unter dem gearbeitet wird -- seit dem
|
||
08.09.2026 auf Filipes Wunsch "Spicy Media" statt "Dogfather
|
||
Universe". Das steht fest in start.html, und fuer die fuenf bekannten
|
||
Rollen stimmt es auch.
|
||
|
||
FUER EINEN MODI STIMMT ES NICHT. Ein Modi gehoert nicht zur Agentur,
|
||
er moderiert die Lives -- "Team Dogi" heisst diese Runde auch auf der
|
||
oeffentlichen Seite (team-modis.html). Bis hierher stand ueber seiner
|
||
Startseite die Marke eines Betriebs, mit dem er nichts zu tun hat.
|
||
Aufgefallen ist das nicht beim Nachdenken, sondern auf dem
|
||
Bildschirmfoto der Browserpruefung.
|
||
|
||
Wie ueberall kommt der Text vom Server: Ein zweiter Markenname in
|
||
start.html waere zwar kein Verrat (er sagt nichts ueber Modis), aber
|
||
eine Zeile, die fuer fast jeden Leser tot dasteht -- und tote Zeilen
|
||
werden irgendwann falsch erklaert. */
|
||
const MODI_MARKE = "Team Dogi";
|
||
|
||
/* =====================================================================
|
||
DER GROSSE TITEL AUF DER STARTSEITE (24.09.2026)
|
||
|
||
Filipe: "anstatt irrenanstalt soll da auch Zentrale stehen bitte.
|
||
auch getrennt von der team dogi seite da steht was anderes und soll
|
||
auch so bleiben."
|
||
|
||
NACHGEMESSEN, BEVOR ETWAS GEAENDERT WURDE: Es stand NICHT etwas
|
||
anderes. `crewWeiche` biegt sechs Dinge um (Manifest, App-Symbole,
|
||
Zugangswand, Buehnen, Marke, Haus-CSS) -- `start.html` ist nicht
|
||
dabei, und kein Skript setzt `#ztitel`. Auf BEIDEN Adressen stand
|
||
derselbe fest eingebaute Titel. Verschieden war nur die Zierzeile
|
||
darueber ("Spicy Media" gegen "Team Dogi"), und die ist es
|
||
vermutlich, die den Eindruck gemacht hat.
|
||
|
||
WAS DARAUS FOLGT: Der Agenturtitel heisst ab jetzt "Zentrale" und
|
||
steht als Vorgabe in der Seite. Das Wort des Teamhauses kommt von
|
||
hier -- damit bleibt dort woertlich stehen, was vorher dastand, und
|
||
es aendert sich fuer das andere Haus nichts. Ob Filipe dort etwas
|
||
anderes will, entscheidet er; geraten wird es nicht.
|
||
|
||
NACH DER ADRESSE UND NICHT NACH DER ROLLE, anders als bei der Marke:
|
||
Ein Titel sagt, WO man ist, keine Marke sagt, ZU WEM man gehoert.
|
||
Die Rolle spielt dafuer keine Rolle -- und praktisch kann ein Modi
|
||
ohnehin nur auf crew. herein (sitzungPasstZurAdresse).
|
||
|
||
UND ER STEHT HIER STATT IN DER SEITE, aus demselben Grund wie
|
||
MODI_MARKE zwei Zeilen darueber: Was nur das Teamhaus angeht, gehoert
|
||
nicht in eine Datei, die jeder Creator herunterlaedt. */
|
||
const MODI_TITEL = "Die IrrenAnstalt";
|
||
|
||
/* =====================================================================
|
||
DIE KATEGORIEN EINER MODI-AUFGABE (10.09.2026)
|
||
|
||
Es sind die des Anforderungsdokuments -- alle dreizehn, die der
|
||
Aufgabenkatalog in Teil 2 tatsaechlich benutzt, dazu "Sonstiges" aus
|
||
Kapitel 6.1.
|
||
|
||
BEIM ERSTEN ANLAUF WAREN ES ACHT, mit der Begruendung, dreizehn
|
||
Knoepfe seien auf einem Handy unbedienbar. Das war eine plausible
|
||
Erklaerung fuer etwas Falsches: Gebaut ist ein AUSWAHLFELD, keine
|
||
Knopfleiste -- vierzehn Eintraege darin sind voellig unauffaellig.
|
||
Die Zusammenfassung haette eine Uebersetzungstabelle noetig gemacht
|
||
("Branding gehoert zu Planung"), die spaeter niemand nachvollziehen
|
||
kann. Jetzt steht an jeder Aufgabe die Kategorie, die im Dokument
|
||
danebensteht.
|
||
|
||
DIE REIHENFOLGE IST DIE DER HAEUFIGKEIT, nicht die des Dokuments:
|
||
Was taeglich vorkommt, steht oben. Wer eine Kategorie sucht, soll
|
||
nicht scrollen muessen.
|
||
|
||
SIE STEHEN AUF DEM SERVER, nicht in assets/js/aufgaben.js. Nicht weil
|
||
die Woerter etwas verraten wuerden -- "Clipping" sagt nichts --,
|
||
sondern weil eine Liste, die im Browser jedes Menschen liegt und nur
|
||
bei einem einzigen benutzt wird, die Frage aufwirft, fuer wen sie da
|
||
ist. Genau diese Frage soll niemand stellen. */
|
||
/* DIE VIERZEHN KATEGORIEN -- jetzt mit einem Satz dazu (16.09.2026).
|
||
*
|
||
* Filipe: "perfektionniere die kategorien wo ich aufgaben an die modis
|
||
* verteile."
|
||
*
|
||
* Bis heute standen hier vierzehn nackte Namen. "Organisation" und
|
||
* "Planung" nebeneinander, ohne einen Satz, der sagt, was worin
|
||
* gehoert -- und dann landet dieselbe Aufgabe beim einen unter
|
||
* Planung, beim anderen unter Organisation, und die Auswertung
|
||
* darueber ist wertlos.
|
||
*
|
||
* Der Satz steht deshalb an der Kategorie und nicht in einer
|
||
* Anleitung: Er wird dort gebraucht, wo man sich entscheidet. */
|
||
export const MODI_KATEGORIEN = [
|
||
{ wert: "chat", name: "Chat-Moderation",
|
||
text: "Was im Livechat passiert, während gesendet wird. Löschen, "
|
||
+ "stummschalten, begrüßen, deeskalieren." },
|
||
{ wert: "community", name: "Community",
|
||
text: "Die Menschen außerhalb der Sendung: wer wiederkommt, wer neu "
|
||
+ "ist, wer lange nicht da war." },
|
||
{ wert: "events", name: "Events",
|
||
text: "Alles mit einem Termin: Matches, Aktionen, Geburtstage, "
|
||
+ "besondere Sendungen." },
|
||
{ wert: "clipping", name: "Clipping",
|
||
text: "Aus einer Sendung viele Videos machen — schneiden, "
|
||
+ "beschriften, hochladen." },
|
||
{ wert: "social", name: "Social Media",
|
||
text: "Alles außerhalb des Livestreams: Ankündigungen, Beiträge, "
|
||
+ "Antworten unter Videos." },
|
||
{ wert: "technik", name: "Technik",
|
||
text: "Ton, Bild, Verbindung, Geräte. Meistens unsichtbar — und "
|
||
+ "sofort sichtbar, wenn es fehlt." },
|
||
{ wert: "planung", name: "Planung",
|
||
text: "Was NOCH NICHT ist: Sendeplan, Themen, wer wann kann." },
|
||
{ wert: "organisation", name: "Organisation",
|
||
text: "Was BEREITS ist, in Ordnung halten: Ablage, Listen, Zugänge, "
|
||
+ "Übersichten." },
|
||
{ wert: "team", name: "Team",
|
||
text: "Die Zusammenarbeit selbst: Übergaben, Einarbeitung, "
|
||
+ "Absprachen, Vertretung." },
|
||
{ wert: "kommunikation", name: "Kommunikation",
|
||
text: "Was nach außen geht und beantwortet werden muss: "
|
||
+ "Nachrichten, Anfragen, Kooperationen." },
|
||
{ wert: "analyse", name: "Analyse",
|
||
text: "Nachsehen statt schätzen: Zahlen, Verläufe, was sich "
|
||
+ "verändert hat." },
|
||
{ wert: "wachstum", name: "Wachstum",
|
||
text: "Gezielt mehr werden: neue Zuschauer, neue Kanäle, neue "
|
||
+ "Zeiten ausprobieren." },
|
||
{ wert: "branding", name: "Branding",
|
||
text: "Wie es aussieht und klingt: Farben, Zeichen, Sprache, "
|
||
+ "Wiedererkennung." },
|
||
{ wert: "sonstiges", name: "Sonstiges",
|
||
text: "Was in keine der dreizehn passt. Bleibt absichtlich klein — "
|
||
+ "wird es groß, fehlt eine Kategorie." },
|
||
];
|
||
|
||
/** Die drei Stufen einer Modi-Aufgabe.
|
||
*
|
||
* DIESELBE IDEE WIE BEI DEN CREATOR-VORLAGEN, andere Worte: Dort heisst
|
||
* es Anfaenger/Fortgeschritten/Profi, und das passt fuer jemanden, der
|
||
* etwas LERNT. Hier geht es nicht um Koennen, sondern darum, wie lange
|
||
* jemand dabei ist und wie viel Vertrauen die Aufgabe braucht.
|
||
*
|
||
* Eine Aufgabe der Stufe "erfahren" an einen Neuen zu geben ist kein
|
||
* Kompliment, sondern ein Ueberfallen. */
|
||
export const MODI_STUFEN = [
|
||
{ wert: "neu", name: "Neu dabei",
|
||
text: "Die ersten Wochen. Klar umrissen, ohne Folgen, wenn etwas "
|
||
+ "schiefgeht — und jemand ist erreichbar." },
|
||
{ wert: "dabei", name: "Eingearbeitet",
|
||
text: "Kennt den Laden. Kann allein entscheiden, ohne vorher zu "
|
||
+ "fragen, und weiß, wann man doch fragt." },
|
||
{ wert: "erfahren", name: "Erfahren",
|
||
text: "Trägt Verantwortung für andere: arbeitet ein, vertritt, "
|
||
+ "entscheidet im Zweifel." },
|
||
];
|
||
/**
|
||
* Darf diese Person mit Kategorien arbeiten?
|
||
*
|
||
* Ein Modi (es sind seine Aufgaben) und die DogFather-Rolle (sie sieht
|
||
* seine Aufgaben und legt ihm welche an). Sonst niemand -- und "sonst
|
||
* niemand" heisst hier: Die Liste kommt gar nicht erst mit, statt
|
||
* mitzukommen und ausgeblendet zu werden.
|
||
*/
|
||
/* =====================================================================
|
||
DIE KANAELE (17.09.2026)
|
||
|
||
Filipe: "ich hab ja auch 2 neben account, hasidog und dogfather
|
||
clips." Bis heute wusste der Arbeitsplatz davon nichts -- es gab
|
||
genau eine Welt, und die hiess nirgends.
|
||
|
||
WARUM DAS EIN ECHTER MANGEL WAR, nicht nur eine fehlende Spalte:
|
||
"Schneide drei Ausschnitte" ist bei drei Accounts keine Aufgabe,
|
||
sondern eine Frage. Wer sie bekommt, muss nachfragen -- oder raet.
|
||
Raet er falsch, ist die Arbeit nicht halb getan, sondern am
|
||
falschen Ort gelandet, und das faellt erst auf, wenn jemand es
|
||
sieht. Genau diese Sorte Mehrdeutigkeit kostet doppelt.
|
||
|
||
WARUM EINE LISTE IM QUELLTEXT UND KEINE TABELLE.
|
||
Sonst gilt hier: Eine abgeschriebene Liste altert. Diese hier ist
|
||
keine Abschrift -- sie laesst sich aus nichts ableiten, weil sie
|
||
eine Tatsache ueber Filipes Betrieb ist und nirgends sonst steht.
|
||
Sie darf deshalb hier stehen, und einen Kanal dazuzunehmen ist eine
|
||
Zeile. Sobald sich das oefter aendert als ein paarmal im Jahr,
|
||
gehoert sie in die Verwaltung -- vorher waere das eine Oberflaeche
|
||
fuer drei Zeilen.
|
||
|
||
KEIN KANAL IST EIN GUELTIGER ZUSTAND. Vieles gilt fuer alles (eine
|
||
Absprache im Team, ein Zugang, eine Auswertung). Ein Pflichtfeld
|
||
haette dafuer einen falschen Kanal erzwungen -- und ein falscher
|
||
Eintrag ist schlechter als ein leerer.
|
||
===================================================================== */
|
||
/* ZWEI SAETZE JE KANAL, WEIL ES ZWEI LESERSCHAFTEN GIBT (21.09.2026).
|
||
|
||
`text` erklaert dem TEAM, wofuer der Kanal da ist -- es steht an
|
||
einer Aufgabe und beantwortet "wo soll das hin". `fuerDraussen`
|
||
steht auf der Community-Seite und beantwortet etwas anderes: "was
|
||
finde ich da". "Hier wird geschnitten, beschriftet und hochgeladen"
|
||
ist fuer die Zuschauerin keine Auskunft, sondern ein Blick in eine
|
||
fremde Werkstatt.
|
||
|
||
ES BLEIBT EINE LISTE. Der zweite Satz haengt am Kanal, nicht in
|
||
einer zweiten Aufzaehlung woanders -- sonst haette ein vierter
|
||
Account wieder zwei Orte, und einer davon wird vergessen. Genau so
|
||
ist am selben Tag "@hasidog00804" entstanden. */
|
||
export const KANAELE = [
|
||
{ wert: "dogfather", name: "DogFather", handle: "@dogfather0804",
|
||
text: "Der Hauptkanal. Die Livestreams und alles, was unmittelbar "
|
||
+ "daran hängt.",
|
||
fuerDraussen: "Der Hauptkanal — hier laufen die Livestreams." },
|
||
{ wert: "hasidog", name: "HasiDog", handle: "@hasidog0804",
|
||
text: "Der zweite Account — eigene Zuschauer, eigener Ton. Was hier "
|
||
+ "läuft, ist nicht einfach dasselbe noch einmal.",
|
||
fuerDraussen: "Der zweite Account. Eigener Ton, eigene Leute." },
|
||
{ wert: "clips", name: "DogFather Clips", handle: "@dogis.modi.gang",
|
||
text: "Nur Ausschnitte. Hier wird geschnitten, beschriftet und "
|
||
+ "hochgeladen, nicht gesendet.",
|
||
fuerDraussen: "Die besten Ausschnitte zum Nachschauen." },
|
||
];
|
||
|
||
/** Zu welchem Kanal gehoert dieser TikTok-Name?
|
||
*
|
||
* DIE HANDLES STEHEN AM KANAL UND NICHT IN EINER ZWEITEN LISTE
|
||
* (17.09.2026). Der Video-Plan nennt genau das als die eine Stelle,
|
||
* an der etwas auseinanderlaufen kann: Sie stehen ohnehin schon in
|
||
* der Analyse-App (`TIKTOK_ACCOUNTS_LIST`). Wer einen vierten Account
|
||
* aufmacht und nur einen der beiden Orte pflegt, bekommt Videos, die
|
||
* zu keinem Kanal gehoeren.
|
||
*
|
||
* Hier gibt es deshalb genau EINE Liste, und sie haengt an dem
|
||
* Begriff, zu dem sie gehoert.
|
||
*
|
||
* `null` heisst: gehoert uns nicht. Das ist KEIN Sonderfall, sondern
|
||
* der wichtigste Ausgang -- ein fremdes Video hat in Filipes
|
||
* Highlights nichts zu suchen. */
|
||
export function kanalVonHandle(handle) {
|
||
const h = String(handle || "").trim().toLowerCase().replace(/^@?/, "@");
|
||
return KANAELE.find((k) => k.handle.toLowerCase() === h)?.wert ?? null;
|
||
}
|
||
|
||
/** Wer darf mit Kanaelen arbeiten? Dieselbe Runde wie bei den
|
||
* Kategorien -- das Team und DogFather. Fuer alle anderen kommt die
|
||
* Liste gar nicht erst mit, statt mitzukommen und ausgeblendet zu
|
||
* werden. */
|
||
export function kanaeleFuer(person) {
|
||
return istTeamDogi(person) ? KANAELE : [];
|
||
}
|
||
|
||
/* =====================================================================
|
||
DIE ALTEN ZURUECKGENOMMENEN NACHRICHTEN GEHEN GANZ (22.09.2026)
|
||
=====================================================================
|
||
|
||
Filipe: "Geloeschte Nachrichten verschwinden vollstaendig -- keine
|
||
Spur, kein ‚wurde geloescht'-Hinweis, bei niemandem, auch nicht bei
|
||
DogFather."
|
||
|
||
Seit dem 22.09.2026 loescht der Chat wirklich (DELETE). Was davor
|
||
zurueckgenommen wurde, steht aber noch als Zeile da: Text leer,
|
||
`weg_am` gesetzt. Die Anzeige zeigt sie nicht mehr -- in der
|
||
Datenbank waere sie trotzdem. "Keine Spur" gaelte dann fuer alles,
|
||
was ab heute passiert, und nicht fuer das, was ihn zu dem Satz
|
||
gebracht hat.
|
||
|
||
ES GEHT NUR UM ZEILEN OHNE INHALT. `text = ''` ist die Bedingung,
|
||
nicht bloss `weg_am IS NOT NULL`: Der Text wurde beim Zuruecknehmen
|
||
geleert, also ist hier nichts zu verlieren. Stuende irgendwo doch
|
||
noch Text in einer zurueckgenommenen Zeile, bliebe sie liegen --
|
||
lieber ein Rest, den jemand ansehen kann, als ein stiller Verlust.
|
||
|
||
WIEDERHOLBAR: Beim zweiten Start gibt es nichts mehr zu tun, und es
|
||
passiert nichts. GESICHERT WIRD VORHER -- aber nur, wenn es wirklich
|
||
etwas zu tun gibt. Eine Sicherung bei jedem Start waere eine Datei
|
||
je Neustart und damit kein Schutz, sondern Muell.
|
||
|
||
---------------------------------------------------------------------
|
||
ALS EIGENE, EXPORTIERTE FUNKTION -- und das hat einen Grund, der an
|
||
genau dieser Stelle entstanden ist.
|
||
|
||
In der ersten Fassung stand `d.prepare("DELETE ...").changes` statt
|
||
`.run().changes`: die Eigenschaft der vorbereiteten ANWEISUNG, nicht
|
||
die des Laufs. Die Anweisung wurde vorbereitet und nie ausgefuehrt.
|
||
Nichts stuerzte ab, nichts war rot. Im Protokoll stand "undefined
|
||
zurueckgenommene Nachrichten endgueltig entfernt" -- ein Zaehler,
|
||
der `undefined` sagt, hat nicht gezaehlt, und was nicht zaehlt, hat
|
||
meist auch nicht gearbeitet. Die fuenf Zeilen blieben liegen, und
|
||
weil die Sicherung VOR dem Loeschen geschrieben wird, waere bei
|
||
jedem Neustart eine weitere Sicherungsdatei entstanden.
|
||
|
||
Eine Umstellung, die tief im Hochfahren steckt, kann keine Pruefung
|
||
aufrufen. Eine Funktion schon: pruef-loeschen.mjs gibt ihr eine
|
||
vorbereitete Datenbank und sieht nach, was sie WIRKLICH getan hat.
|
||
|
||
@returns {{entfernt:number, gesichert:string|null}}
|
||
===================================================================== */
|
||
export function loeschspurenAufraeumen(d, dbPfad = DB_PFAD) {
|
||
try {
|
||
const spalten = d.prepare("PRAGMA table_info(chat_nachrichten)").all().map((x) => x.name);
|
||
if (!spalten.includes("weg_am")) return { entfernt: 0, gesichert: null };
|
||
const offen = d.prepare(
|
||
"SELECT COUNT(*) AS n FROM chat_nachrichten WHERE weg_am IS NOT NULL AND text = ''").get().n;
|
||
if (offen === 0) return { entfernt: 0, gesichert: null };
|
||
|
||
let sicherung = null;
|
||
if (dbPfad) {
|
||
const stempel = new Date().toISOString().replace(/[-:T]/g, "").slice(0, 14);
|
||
sicherung = `${dbPfad}.vor-loeschspuren-${stempel}`;
|
||
d.exec(`VACUUM INTO '${sicherung.replace(/'/g, "''")}'`);
|
||
console.log("[workspace] Sicherung vor dem Aufraeumen:", sicherung);
|
||
}
|
||
/* Reaktionen und Erwaehnungen haengen mit ON DELETE CASCADE daran
|
||
und gehen von selbst mit -- `PRAGMA foreign_keys = ON` steht
|
||
beim Oeffnen. */
|
||
const entfernt = d.prepare(
|
||
"DELETE FROM chat_nachrichten WHERE weg_am IS NOT NULL AND text = ''").run().changes;
|
||
/* Und die Raeume rechnen ihren letzten Zeitpunkt neu -- sonst
|
||
stuende einer oben in der Liste, zu dessen Zeitpunkt es nichts
|
||
mehr gibt. */
|
||
d.exec(`UPDATE chat_raeume SET letzte_am =
|
||
(SELECT MAX(erstellt) FROM chat_nachrichten WHERE raum_id = chat_raeume.id)`);
|
||
console.log(`[workspace] ${entfernt} zurueckgenommene Nachrichten endgueltig entfernt.`);
|
||
return { entfernt, gesichert: sicherung };
|
||
} catch (fehler) {
|
||
console.error("[workspace] Aufraeumen der Loeschspuren:", fehler?.message);
|
||
return { entfernt: 0, gesichert: null };
|
||
}
|
||
}
|
||
|
||
export function kategorienFuer(person) {
|
||
return istTeamDogi(person) ? MODI_KATEGORIEN : [];
|
||
}
|
||
|
||
/**
|
||
* FUER WESSEN AUFGABEN gilt die Kategorie? (10.09.2026)
|
||
*
|
||
* null -> fuer alle eigenen; die Oberflaeche zeigt das Feld immer
|
||
* [Nummern] -> nur wenn eine dieser Personen gewaehlt ist
|
||
* [] -> nie
|
||
*
|
||
* WARUM DAS DER SERVER BEANTWORTET UND NICHT DER BROWSER: Die
|
||
* Oberflaeche muesste sonst `p.rolle === 'modi'` vergleichen -- und
|
||
* assets/js/aufgaben.js bekommt JEDER, der die Seite oeffnet. Genau so
|
||
* stand es beim ersten Anlauf da, an sechs Stellen. Mit Nummern statt
|
||
* einem Rollennamen steht im Browser nichts, woraus sich etwas
|
||
* schliessen laesst: Wer keine bekommt, sieht eine leere Liste, und
|
||
* eine leere Liste sagt nichts.
|
||
*/
|
||
export function kategoriePersonen(person) {
|
||
if (!person) return [];
|
||
/* Ein Modi arbeitet ausschliesslich an eigenen Aufgaben -- dort gilt
|
||
sie immer, unabhaengig davon, wen er eintraegt. */
|
||
if (person.rolle === "modi") return null;
|
||
if (person.rolle !== "admin" && !istHand(person)) return [];
|
||
try {
|
||
/* Auch die rechte Hand selbst: Sie traegt Aufgaben ein -- fuer
|
||
andere UND fuer sich. Waere ihre eigene Nummer nicht dabei,
|
||
verschwaende das Kategoriefeld genau dann, wenn sie sich selbst
|
||
etwas notiert. */
|
||
const liste = [...TEAM_DOGI_ROLLEN].map((r) => `'${r}'`).join(", ");
|
||
return db().prepare(
|
||
`SELECT id FROM personen WHERE rolle IN (${liste}) AND aktiv = 1`).all().map((z) => z.id);
|
||
} catch {
|
||
/* Ohne Datenbank lieber kein Feld als ein falsches. */
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/* Der Satz der rechten Hand sagt, was ihre Rolle ist, ohne sie ueber
|
||
die anderen zu stellen: Sie haelt den Ueberblick, sie steht nicht
|
||
darueber. */
|
||
const HAND_ROLLENTEXT = "Rechte Hand · du hast alles im Blick und entscheidest mit";
|
||
const LINKE_ROLLENTEXT = "Linke Hand · du fuehrst das Team durch den Tag";
|
||
|
||
export function rollentextFuer(person) {
|
||
if (person?.rolle === "modi") return MODI_ROLLENTEXT;
|
||
if (person?.rolle === "hand") return HAND_ROLLENTEXT;
|
||
if (person?.rolle === "linke") return LINKE_ROLLENTEXT;
|
||
/* DIE COMMUNITY HATTE NUR EIN WORT (nachgetragen 19.09.2026).
|
||
|
||
Bei jeder anderen Rolle steht unter dem Titel ein Satz. Beim
|
||
Community-Mitglied stand dort das nackte Wort "Community" -- und
|
||
das ist woertlich derselbe Mangel, den der Kommentar bei
|
||
MODI_ROLLENTEXT beschreibt und fuer den Modi behebt: "das sieht
|
||
nicht verborgen aus, sondern unfertig."
|
||
|
||
Fuer den Menschen, der von aussen kommt und niemanden hier kennt,
|
||
ist der erste Satz nach dem Anmelden der wichtigste im ganzen
|
||
Haus. */
|
||
if (person?.rolle === "gast") return "Community · dein Platz im Rudel";
|
||
return null;
|
||
}
|
||
|
||
/* DIE MARKE HAENGT AN ZWEI DINGEN, NICHT AN EINEM (10.09.2026).
|
||
*
|
||
* An der ROLLE: Wer zu Team Dogi gehoert, sieht ueberall den Namen
|
||
* seines Teams -- auch dann, wenn er die Seite ueber eine andere
|
||
* Adresse aufmacht.
|
||
*
|
||
* Und an der ADRESSE: Auf crew.dogfather-universe.com gibt es Spicy
|
||
* Media nicht. Dort stand ueber der Zentrale trotzdem "SPICY MEDIA" --
|
||
* gross, mitten im Bild, weil diese Funktion nur die Rolle kannte und
|
||
* DogFather nun einmal zur Agentur gehoert. Filipe: "ich will doch
|
||
* dass der eingang hier getrennt ist von der workspace seite."
|
||
*
|
||
* Zwei Fragen, EINE Antwortstelle. Wer die Adressbedingung stattdessen
|
||
* an der Aufrufstelle prueft, hat sie beim naechsten Aufruf nicht --
|
||
* und diese Funktion wird ueber kurz oder lang ein zweites Mal
|
||
* gebraucht. */
|
||
export function markeFuer(person, hostKopfzeile) {
|
||
if (istCrewAdresse(hostKopfzeile)) return MODI_MARKE;
|
||
return TEAM_DOGI_ROLLEN.has(person?.rolle) ? MODI_MARKE : null;
|
||
}
|
||
|
||
/** Der grosse Titel auf der Startseite -- oder `null` fuer die Vorgabe
|
||
* aus der Seite ("Zentrale"). Begruendung oben bei MODI_TITEL. */
|
||
export function titelFuer(hostKopfzeile) {
|
||
return istCrewAdresse(hostKopfzeile) ? MODI_TITEL : null;
|
||
}
|
||
|
||
/* =====================================================================
|
||
KACHELN, DIE ZU EINER VORHANDENEN LISTE DAZUKOMMEN (10.09.2026)
|
||
|
||
bereicheFuer() ersetzt die Liste im Browser ganz -- das passt fuer
|
||
eine Rolle, die dort gar nicht vorkommt. Fuer die DogFather-Rolle
|
||
passt es nicht: Sie hat ihre 23 Kacheln, und es soll EINE dazu.
|
||
|
||
Sie steht hier und nicht in assets/js/bereiche.js, weil dort jeder
|
||
mitliest. "Team-Lage" allein verraet zwar nichts -- aber eine
|
||
Kachel, die nur bei einer einzigen Rolle erscheint, wirft die Frage
|
||
auf, fuer wen sie ist. Genau die soll niemand stellen.
|
||
===================================================================== */
|
||
/* ZWEIMAL "TEAM-LAGE" -- KORRIGIERT AM 10.09.2026.
|
||
|
||
Hier stand: Gruppe "Rund um das Team", Name "Team-Lage", Ton 22.
|
||
In bereiche.js gibt es aber SCHON eine Kachel "Team-Lage" mit Ton 22
|
||
und demselben Zeichen, die nach team.html fuehrt. DogFather hatte
|
||
damit zwei Kacheln, die gleich heissen, gleich aussehen, gleich
|
||
gefaerbt sind, nebeneinander in derselben Gruppe stehen -- und zu
|
||
verschiedenen Seiten fuehren.
|
||
|
||
Gefunden hat das nicht das Auge, sondern pruef-start-ansicht mit dem
|
||
Satz "jede Kachel hat ihre eigene Farbe (23 Farben auf 24 Kacheln)".
|
||
Ohne diese Zeile waere es stehen geblieben; zwei gleiche Kacheln
|
||
fallen niemandem auf, der weiss, welche er meint.
|
||
|
||
Jetzt drei Unterschiede statt keinem: eigener Name, eigene Gruppe,
|
||
eigene Farbe. Das Zeichen bleibt geteilt -- nachgemessen sind alle
|
||
22 Zeichen des Hauses bei DogFather bereits in Gebrauch, und ein
|
||
geteiltes Zeichen bei verschiedenem Namen, Ort und Ton ist keine
|
||
Verwechslungsgefahr (profil steht ohnehin schon zweimal).
|
||
|
||
"Eingang" und nicht "Rueckmeldungen": Die Kachel steht bei den
|
||
taeglichen Dingen, weil dort etwas WARTET -- Vorschlaege, ueber die
|
||
entschieden werden will. Das ist eine Aufgabe fuer heute, keine
|
||
Uebersicht fuer zwischendurch. */
|
||
/* =====================================================================
|
||
DIE EIGENE GRUPPE FUER DAS TEAM (10.09.2026)
|
||
|
||
Filipe zum Bildschirmfoto seiner Startseite: "ich will dass die
|
||
kacheln von den modis bei der rolle dogfather eine eigene kategorie
|
||
haben. damit ich nicht zwischen den kacheln suchen muss."
|
||
|
||
Er hatte recht, und es war schlimmer als unuebersichtlich: Der
|
||
Eingang stand unter "Taeglich" zwischen Chat und Kalender, das
|
||
Ideen-Board ebenfalls -- zwei Kacheln derselben Welt, verteilt auf
|
||
eine Reihe mit sieben anderen. Wer sie sucht, liest jedes Mal alle.
|
||
|
||
`gruppeNach` setzt die Gruppe direkt hinter "Taeglich" statt ans
|
||
Ende. Ganz unten waere zwar auch eine eigene Kategorie -- und immer
|
||
noch Scrollen.
|
||
|
||
DAS IDEEN-BOARD IST DABEI VON assets/js/bereiche.js HIERHER GEZOGEN.
|
||
Zwei Gruende, und der zweite ist der wichtigere:
|
||
1. Es gehoert in dieselbe Gruppe wie der Eingang, und die entsteht
|
||
hier.
|
||
2. Es stand mit `rollen: ['admin']` in einer Datei, die JEDER
|
||
herunterlaedt. Eine Kachel, die nur eine Rolle sieht, wirft dort
|
||
die Frage auf, wofuer sie ist -- genau die Frage, die niemand
|
||
stellen soll. Jetzt kommt sie vom Server und nur an die, die sie
|
||
bekommt.
|
||
|
||
Der Gruppenname "Team Dogi" ist unverfaenglich: Er steht so auf der
|
||
oeffentlichen Seite. */
|
||
/* GANZ NACH UNTEN (11.09.2026).
|
||
|
||
Filipe: "diese zwei kategorien sollen auf dieser seite (workspace)
|
||
ganz unten sein. die letzten kategorien bei der rolle dogfather ...
|
||
goldene regel das sind private informationen von meiner community."
|
||
|
||
Vorher stand hier `gruppeNach: "Täglich"` -- damit sassen die
|
||
privatesten Kacheln des Hauses zwischen den taeglichen Werkzeugen,
|
||
also genau dort, wo der Blick zuerst hinfaellt und wo jemand
|
||
mitliest, der einem ueber die Schulter schaut.
|
||
|
||
"Team & System" ist die letzte Gruppe der Workspace-Liste
|
||
(bereiche.js). Sie steht hier als NAME und nicht als Position: Eine
|
||
Zahl ("an vierter Stelle") waere beim naechsten Umsortieren still
|
||
falsch -- und still falsch heisst hier: privates Material rutscht
|
||
wieder nach oben. Verschiebt jemand die Gruppen, wandert Team Dogi
|
||
mit, solange "Team & System" die letzte bleibt.
|
||
|
||
pruef-start-ansicht misst die Reihenfolge nach, damit aus "solange"
|
||
keine Hoffnung wird. */
|
||
const TEAM_GRUPPE = {
|
||
gruppe: "Team Dogi",
|
||
gruppeUnter: "Was dein Team macht – und was auf dich wartet",
|
||
gruppeNach: "Team & System",
|
||
};
|
||
|
||
/* =====================================================================
|
||
KEINE MISCHUNG MEHR (21.09.2026)
|
||
|
||
Filipe: "ich will dass du im workspace alles von team dogi weg
|
||
nimmst. nur was ich im kalender eintrage soll ich im workspace sehen
|
||
und im team dogi. aber die kacheln von team dogi soll ich nur bei
|
||
team dogi sehen und die von workspace nur bei workspace bitte. ich
|
||
will keine mischung mehr von kacheln. wie gesagt nur die der kalender
|
||
soll verbunden sein von dogfather sonst nichts."
|
||
|
||
---------------------------------------------------------------------
|
||
WAS HIER BIS HEUTE STAND
|
||
|
||
Eine Liste ZUSATZ_BEREICHE mit siebzehn Kacheln, die auf der
|
||
AGENTURADRESSE an DogFathers Seite gehaengt wurden: Dein Team,
|
||
Ideen-Board, Rueckmeldung, Wer sieht was, Entwicklung, Werdegang,
|
||
Talente -- und der ganze Treff samt Moderation. Nachgemessen am
|
||
21.09.2026 bekam er dort 0 eigene Kacheln und 17 fremde; auf der
|
||
Team-Adresse 30, in sechs Gruppen, aus beiden Haeusern.
|
||
|
||
Der Gedanke dahinter war gut gemeint: Er soll ueberall an alles
|
||
herankommen. Die Wirkung war eine Seite, auf der zwei Betriebe
|
||
uebereinanderliegen -- und zwar genau der Zustand, den dieselbe
|
||
Datei am 10.09. fuer die andere Richtung schon als Fehler erkannt
|
||
hatte ("Fuenfundzwanzig Kacheln, von denen zwei Drittel Creator,
|
||
Scouts und Agentur betreffen, waeren dort Fenster in ein Haus, in
|
||
dem er gerade nicht ist").
|
||
|
||
---------------------------------------------------------------------
|
||
DIE EINE AUSNAHME IST DER KALENDER -- UND SIE BRAUCHT NICHTS
|
||
|
||
Nachgemessen: Die Terminabfragen in workspace-kalender.js filtern
|
||
NICHT nach Haus. Was er eintraegt, steht ohnehin auf beiden
|
||
Adressen. Die Verbindung, die er will, existiert also bereits; sie
|
||
musste nicht gebaut, sondern nur nicht zerschnitten werden.
|
||
|
||
---------------------------------------------------------------------
|
||
EINE TUER IST KEINE MISCHUNG
|
||
|
||
Ohne Ersatz waere von der Agenturadresse aus kein Weg mehr zu Team
|
||
Dogi sichtbar -- er muesste die Adresse tippen. Das ist die Sorte
|
||
Sackgasse, die dieses Haus nicht baut. Statt siebzehn Brettern steht
|
||
dort jetzt EINE Kachel, die hinuebergeht.
|
||
|
||
SIE VERRAET NICHTS: Sie kommt vom Server und nur fuer die, die zu
|
||
Team Dogi gehoeren. In keiner ausgelieferten Datei steht sie, und
|
||
ein Manager bekommt sie nie zu sehen -- genau wie bei den Kacheln,
|
||
die sie ersetzt.
|
||
===================================================================== */
|
||
export function zusatzBereicheFuer(person) {
|
||
/* NUR DOGFATHER UND VANVAN -- also `admin`.
|
||
|
||
Der erste Entwurf gab die Tuer auch an TEAM_DOGI_ROLLEN (die
|
||
rechte Hand und die Moderation). Das war falsch, und pruef-modi
|
||
-checkliste hat es sofort gemeldet: "Modi bekommt 1
|
||
Zusatzkachel(n), erwartet keine".
|
||
|
||
WARUM ES FALSCH IST: Wer NUR zu Team Dogi gehoert, hat auf der
|
||
Agenturadresse gar nichts zu suchen -- die Tuer waere dort seine
|
||
einzige Kachel, also eine Seite, die aus nichts als einem Ausgang
|
||
besteht. Und sie wuerde eine Adresse nennen, die auf dieser Seite
|
||
niemand nennen soll.
|
||
|
||
`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: nachCrew ? "Team Dogi" : "Creator Workspace",
|
||
gruppeUnter: "Das andere Haus \u2014 eigene Seite, eigene Daten",
|
||
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",
|
||
/* KEIN TON (25.09.2026). Hier stand 40 -- dieselbe Nummer wie
|
||
„Entwicklung", und DogFather sieht beide nebeneinander. Zwei
|
||
Kacheln in einer Farbe sind auf einer Wand, die ueber Farbe
|
||
funktioniert, keine Kleinigkeit: Die Farbe ist der einzige
|
||
Hinweis, den man ohne Lesen bekommt.
|
||
|
||
Eine 46. Bereichsfarbe waere die naheliegende Antwort und die
|
||
falsche: Gerechnet bliebe sie unter der Hausgrenze fuer den
|
||
Abstand (0,0862 statt 0,09) -- der Farbraum ist voll. Diese
|
||
Kachel gehoert aber auch gar keinem Bereich an, sie fuehrt
|
||
hinaus. Ohne `ton` faellt sie auf `--akzent` zurueck und ist
|
||
damit von allen 45 Bereichskacheln unterscheidbar. Die
|
||
Begruendung steht ausfuehrlich in start.css bei Ton 45. */
|
||
szene: "portal",
|
||
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
|
||
Rechtetafel. Bei einer vollstaendigen Adresse kommt dabei
|
||
Unsinn heraus, und die Kachel verschwaende lautlos. */
|
||
aussen: true,
|
||
}];
|
||
}
|
||
|
||
/* scrypt-Parameter. N=2^15 braucht auf diesem Server rund 150 ms — spürbar
|
||
genug, um Rateversuche auszubremsen, und unauffällig für einen echten
|
||
Anmeldevorgang. Die Werte werden je Datensatz mitgespeichert, damit sie
|
||
später erhöht werden können, ohne alte Codes ungültig zu machen. */
|
||
const SCRYPT = { N: 32768, r: 8, p: 1, keylen: 64 };
|
||
|
||
let _db = null;
|
||
let _dbFehler = null;
|
||
|
||
/* =====================================================================
|
||
Umstellungen an bestehenden Datenbanken.
|
||
|
||
CREATE TABLE IF NOT EXISTS legt eine Tabelle nur an, wenn sie fehlt --
|
||
eine vorhandene wird NICHT angefasst. Die CHECK-Regel fuer die Rolle
|
||
stand also weiterhin auf den alten drei Werten, und ein Manager waere
|
||
an der Datenbank gescheitert, obwohl im Code alles stimmt.
|
||
|
||
SQLite kann eine CHECK-Regel nicht aendern. Der einzige saubere Weg
|
||
ist: neue Tabelle, Daten hinueber, alte weg, neue umbenennen. Das ist
|
||
der Moment, in dem eine Datenbank kaputtgehen kann -- deshalb wird
|
||
vorher eine vollstaendige Sicherung geschrieben (VACUUM INTO, von
|
||
SQLite selbst und in sich konsistent, anders als ein Dateikopie
|
||
waehrend laufender Schreibvorgaenge).
|
||
===================================================================== */
|
||
/* =====================================================================
|
||
DIE ROLLENLISTE DER DATENBANK ERWEITERN (09.09.2026)
|
||
|
||
Welche Rollen es geben darf, steht als CHECK-Regel in der Tabelle
|
||
`personen`. SQLite kann eine CHECK-Regel nicht aendern -- der einzige
|
||
saubere Weg ist: neue Tabelle, Daten hinueber, alte weg, neue
|
||
umbenennen. Das ist der Moment, in dem eine Datenbank kaputtgehen
|
||
kann. Deshalb steht der Ablauf ab jetzt EINMAL hier.
|
||
|
||
WARUM ALS FUNKTION: Er stand vorher zweimal da -- einmal fuer
|
||
'manager', einmal fuer 'spicy'. Fuer 'modi' waere es die dritte
|
||
Abschrift geworden, und jede Abschrift ist eine Gelegenheit, eine der
|
||
vier Absicherungen zu vergessen: die Sicherung VORHER, die Zaehlung
|
||
INNERHALB der Transaktion, die Spaltenliste aus der Tabelle statt aus
|
||
dem Gedaechtnis, die Pruefung auf verwaiste Verweise DANACH. Der
|
||
manager-Block darueber bleibt unangetastet: Er baut die Tabelle mit
|
||
einer fest hingeschriebenen Spaltenliste auf und ist damit ein
|
||
anderer Fall -- ihn mit einzufangen waere ein zweiter Umbau an einer
|
||
Stelle, an der ein Fehler Daten kostet.
|
||
|
||
GEPRUEFT WIRD DIE REGEL, NICHT DER TEXT (07.09.2026, gefunden von
|
||
pruef-spicy): SQLite hebt den CREATE-Text woertlich auf, mitsamt
|
||
Kommentaren. Ein erklaerender Satz mit dem Wort 'spicy' genuegte, und
|
||
die Umstellung hielt die Tabelle fuer schon umgestellt, obwohl die
|
||
CHECK-Regel noch die alte war. Deshalb wird die Regel herausgeschnitten
|
||
und NUR darin gesucht.
|
||
|
||
@param marker Die Rolle, an der erkannt wird, ob schon umgestellt ist.
|
||
@param rollen Die vollstaendige neue Liste erlaubter Rollen.
|
||
===================================================================== */
|
||
/* AUSGEGEBEN SEIT DEM 24.09.2026. Die Checkliste brauchte denselben
|
||
Griff fuer `punkt_stand.stufe`, und ich hatte ihn dort schon ein
|
||
zweites Mal hingeschrieben -- mit der Zeilenzaehlung und der
|
||
Spaltenliste aus PRAGMA, aber OHNE die Sicherung davor, ohne die
|
||
Indizes und ohne die Pruefung auf verwaiste Verweise danach. Drei
|
||
von fuenf Absicherungen fehlten, und keine davon haette gefehlt,
|
||
wenn ich die vorhandene Funktion benutzt haette.
|
||
Genau davor warnt der Absatz "WARUM ALS FUNKTION" oben. */
|
||
export function checkListeErweitern(d, tabelle, spalte, marker, werte, jetztStempel) {
|
||
const rollenPlan = d.prepare(
|
||
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get(tabelle)?.sql || "";
|
||
/* DOPPELTE BACKSLASHES, und das ist kein Schoenheitsfehler:
|
||
In einem Template-Literal verschluckt JavaScript den einfachen
|
||
Backslash -- aus `\s` wird schlicht `s`, das Muster hiesse dann
|
||
"bereichs+INs*(...)" und traefe nie etwas. Die Umstellung haette
|
||
daraufhin STILLSCHWEIGEND nichts getan: keine Regel gefunden,
|
||
also nichts umzustellen, kein Fehler, kein Hinweis. Gemessen
|
||
statt ueberlegt -- der Unterschied war im Quelltext nicht zu
|
||
sehen. */
|
||
const muster = new RegExp(`${spalte}\\s+IN\\s*\\(([^)]*)\\)`, "i");
|
||
const regel = rollenPlan.match(muster)?.[1] || "";
|
||
/* Keine Regel gefunden heisst: Es gibt nichts umzustellen. Nicht
|
||
etwa "dann bauen wir sie neu" -- eine Tabelle ohne CHECK ist ein
|
||
Zustand, den jemand ansehen sollte, kein Fall fuer Automatik. */
|
||
if (!rollenPlan || !regel || regel.includes(`'${marker}'`)) return;
|
||
|
||
const sicherung = `${DB_PFAD}.vor-${marker}-${jetztStempel}`;
|
||
try {
|
||
d.exec(`VACUUM INTO '${sicherung.replace(/'/g, "''")}'`);
|
||
console.log("[workspace] Sicherung vor der Umstellung:", sicherung);
|
||
} catch (fehler) {
|
||
/* Ohne Sicherung wird NICHT umgestellt. Lieber laeuft die neue Rolle
|
||
noch nicht, als dass Daten ohne Netz angefasst werden. */
|
||
console.error("[workspace] Sicherung fehlgeschlagen, Umstellung abgebrochen:", fehler?.message);
|
||
return;
|
||
}
|
||
|
||
/* Nur die Rollenliste ersetzen -- und pruefen, dass es geklappt hat.
|
||
|
||
`IF NOT EXISTS` MUSS MIT INS MUSTER (07.09.2026, gefunden von
|
||
pruef-spicy). Die Tabelle wurde mit `CREATE TABLE IF NOT EXISTS
|
||
personen` angelegt, also steht das auch in sqlite_master. Ohne diese
|
||
drei Woerter im Muster griff die Ersetzung nicht, der Bauplan blieb
|
||
unveraendert, die Schutzabfrage darunter schlug an -- und die
|
||
Umstellung waere auf dem echten Server NIE gelaufen. Sie haette es
|
||
gesagt (das ist der Wert der Abfrage), aber sie waere nie gelaufen. */
|
||
/* KEIN MUSTER FUER DEN TABELLENKOPF -- alles vor der ersten Klammer
|
||
IST der Kopf ("CREATE TABLE IF NOT EXISTS eintraege "). Hier stand
|
||
eine Regel mit Anfuehrungszeichen, Backticks und Backslashes in
|
||
einem Template-Literal, und genau daran ist sie gescheitert:
|
||
JavaScript verschluckt darin den einfachen Backslash, aus dem
|
||
Muster wurde "CREATE TABLEs+..." und es traf nie etwas. Die
|
||
Umstellung haette daraufhin STILLSCHWEIGEND nichts getan.
|
||
|
||
Ein Schnitt an der ersten Klammer braucht keine Maskierung, ist in
|
||
einer Zeile zu lesen und kann nicht falsch escaped sein. */
|
||
const klammer = rollenPlan.indexOf('(');
|
||
if (klammer < 0) {
|
||
console.error('[workspace] Umstellung abgebrochen: Bauplan ohne Klammer.');
|
||
return;
|
||
}
|
||
const neuerPlan = (`CREATE TABLE ${tabelle}_neu ` + rollenPlan.slice(klammer))
|
||
.replace(new RegExp(`${spalte}\\s+IN\\s*\\([^)]*\\)`, "i"),
|
||
`${spalte} IN (${werte.map((r) => `'${r}'`).join(",")})`);
|
||
if (!neuerPlan.includes(`'${marker}'`) || !neuerPlan.includes(`${tabelle}_neu`)) {
|
||
console.error("[workspace] Umstellung abgebrochen: Der Bauplan liess sich nicht "
|
||
+ "umschreiben. Steht die CHECK-Regel noch so da wie erwartet?");
|
||
return;
|
||
}
|
||
|
||
/* Die Spaltenliste kommt aus der Tabelle, nicht aus dem Gedaechtnis. */
|
||
const spalten = d.prepare(`PRAGMA table_info(${tabelle})`).all().map((z) => z.name);
|
||
if (!spalten.length) {
|
||
console.error("[workspace] Umstellung abgebrochen: keine Spalten gefunden.");
|
||
return;
|
||
}
|
||
const liste = spalten.map((n) => `"${n}"`).join(", ");
|
||
|
||
/* DIE INDIZES GEHEN MIT -- SONST GEHEN SIE VERLOREN (10.09.2026).
|
||
|
||
`DROP TABLE` nimmt jeden Index der Tabelle mit. Der Bauplan aus
|
||
sqlite_master beschreibt nur die TABELLE, nicht ihre Indizes; die
|
||
neue stand danach blank da.
|
||
|
||
Aufgefallen ist das nicht im Betrieb, sondern beim Nachdenken
|
||
darueber, was diese Auslieferung auf dem echten Server tut: Dort
|
||
wird `chat_raeume` umgebaut, und `idx_chat_raeume_letzte` waere
|
||
danach weg gewesen. Gemerkt haette es niemand -- die Abfrage laeuft
|
||
weiter, sie liest nur die ganze Tabelle. Beim naechsten Neustart
|
||
stuende der Index wieder da (er steht oben im Bauplan), aber "bis
|
||
zum naechsten Neustart falsch" ist kein Zustand, den man einbaut.
|
||
|
||
Bei einem EINDEUTIGEN Index waere es keine Frage der
|
||
Geschwindigkeit mehr, sondern der Richtigkeit: Die Zusage "einen
|
||
Kanal je Zustaendigkeit" haette bis zum Neustart still ausgesetzt.
|
||
|
||
`sql IS NULL` filtert die Indizes weg, die SQLite sich selbst zu
|
||
einer UNIQUE-Spalte baut: Die entstehen mit der neuen Tabelle von
|
||
allein, und ihr Name (`sqlite_autoindex_...`) ist gar nicht
|
||
anlegbar. */
|
||
const indizes = d.prepare(
|
||
"SELECT sql FROM sqlite_master WHERE type = 'index' AND tbl_name = ? AND sql IS NOT NULL")
|
||
.all(tabelle).map((z) => z.sql);
|
||
|
||
/* Fremdschluessel muessen aus sein, weil andere Tabellen auf
|
||
personen(id) zeigen -- und das laesst sich nicht innerhalb einer
|
||
Transaktion umschalten. */
|
||
d.exec("PRAGMA foreign_keys = OFF");
|
||
try {
|
||
const vorher = d.prepare(`SELECT COUNT(*) AS n FROM ${tabelle}`).get().n;
|
||
d.exec("BEGIN");
|
||
d.exec(neuerPlan);
|
||
d.exec(`INSERT INTO ${tabelle}_neu (${liste}) SELECT ${liste} FROM ${tabelle};`);
|
||
const nachher = d.prepare(`SELECT COUNT(*) AS n FROM ${tabelle}_neu`).get().n;
|
||
/* Die Zaehlung steht INNERHALB der Transaktion -- stimmt sie nicht,
|
||
wird zurueckgerollt und die alte Tabelle bleibt unberuehrt. */
|
||
if (nachher !== vorher) {
|
||
d.exec("ROLLBACK");
|
||
console.error(`[workspace] Umstellung abgebrochen: ${vorher} Zeilen vorher, `
|
||
+ `${nachher} nachher. Sicherung: ${sicherung}`);
|
||
} else {
|
||
d.exec(`DROP TABLE ${tabelle};`);
|
||
d.exec(`ALTER TABLE ${tabelle}_neu RENAME TO ${tabelle};`);
|
||
d.exec("COMMIT");
|
||
/* ERST NACH DEM COMMIT. Waere zurueckgerollt worden, stuenden die
|
||
alten Indizes noch -- ein zweites Anlegen waere dann ein Fehler
|
||
ohne Anlass. */
|
||
let indizesZurueck = 0;
|
||
for (const bau of indizes) {
|
||
try { d.exec(bau); indizesZurueck++; }
|
||
catch (f) { console.error("[workspace] Index nach der Umstellung:", f?.message); }
|
||
}
|
||
if (indizesZurueck !== indizes.length) {
|
||
console.error(`[workspace] ACHTUNG: nur ${indizesZurueck} von ${indizes.length} `
|
||
+ `Indizes auf ${tabelle} wiederhergestellt. Sicherung: ${sicherung}`);
|
||
}
|
||
const kaputt = d.prepare("PRAGMA foreign_key_check").all();
|
||
if (kaputt.length) {
|
||
console.error("[workspace] ACHTUNG: nach der Umstellung", kaputt.length,
|
||
"verwaiste Verweise. Sicherung liegt unter", sicherung);
|
||
} else {
|
||
console.log(`[workspace] '${marker}' in ${tabelle}.${spalte} freigeschaltet, `
|
||
+ `${nachher} Zeilen, ${spalten.length} Spalten, ${indizesZurueck} von `
|
||
+ `${indizes.length} Indizes uebernommen, Verweise geprueft.`);
|
||
}
|
||
}
|
||
} catch (fehler) {
|
||
try { d.exec("ROLLBACK"); } catch { /* schon zurueckgerollt */ }
|
||
console.error("[workspace] Umstellung fehlgeschlagen:", fehler?.message,
|
||
"-- Sicherung:", sicherung);
|
||
} finally {
|
||
d.exec("PRAGMA foreign_keys = ON");
|
||
}
|
||
}
|
||
|
||
/* =====================================================================
|
||
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"],
|
||
/* DIE TEILNEHMER DER SERIE ZAEHLEN MIT (nachgetragen 25.09.2026).
|
||
|
||
Hier standen nur `creator_id`, `teilnehmer_id` und
|
||
`erstellt_von` -- bei den TERMINEN eine Zeile hoeher wird
|
||
dagegen auch `termin_teilnehmer` gefragt. Dieselbe Frage, zwei
|
||
verschiedene Antworten: genau die Bauart, vor der in dieser
|
||
Datei an fuenfzehn Stellen gewarnt wird, und ich habe sie beim
|
||
Nachruesten der Serien selbst gebaut.
|
||
|
||
GEMESSEN AM ECHTEN STAND nach dem Ausliefern: Zwei Serien
|
||
blieben ohne Haus -- „Community-Call" und „Schulung-Agentur".
|
||
An beiden haengt nur DogFather; ihre Teilnehmer sind fuenf
|
||
Creator und Scouts, also eindeutig die Agentur. Der Beleg war
|
||
da, er wurde nur nicht gelesen.
|
||
|
||
WAS DAS GEKOSTET HAETTE: Die 53 bereits erzeugten Termine sind
|
||
richtig zugeordnet (sie haben ja Teilnehmer). Aber jeder KUENFTIG
|
||
erzeugte haette `haus = null` geerbt -- und ein `null` ist in
|
||
BEIDEN Haeusern sichtbar. Die Trennung waere Woche fuer Woche
|
||
ein Stueck weiter aufgegangen, ohne dass etwas rot wird. */
|
||
["termin_serien", (r) => [
|
||
...d.prepare("SELECT person_id AS p FROM serie_teilnehmer WHERE serie_id = ?")
|
||
.all(r.id).map((z) => z.p),
|
||
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).
|
||
|
||
`VACUUM INTO` weigert sich, eine vorhandene Datei zu ueberschreiben
|
||
("output file already exists") -- zu Recht, eine Sicherung darf
|
||
nichts wegwerfen. Der Stempel hatte aber nur Minutenaufloesung:
|
||
Zwei Umstellungen innerhalb derselben Minute wollten in dieselbe
|
||
Datei, die zweite scheiterte, und weil ohne Sicherung nicht
|
||
umgestellt wird, blieb sie einfach aus.
|
||
|
||
Das ist genau die unangenehme Sorte Fehler: Es sieht nach "die
|
||
Umstellung lief" aus (die Datei liegt ja da) und war doch keine.
|
||
Gefunden von pruef-spicy, das den Server zweimal in derselben
|
||
Minute startet -- im Betrieb genuegt dafuer ein Neustart im
|
||
falschen Moment. */
|
||
const jetztStempel = new Date().toISOString().slice(0, 19).replace(/[-:T]/g, "");
|
||
|
||
/* ---- Woher ein Eintrag kommt: das Video (17.09.2026) ----
|
||
|
||
Stufe A des Video-Plans. `quelle_url` ist die Adresse bei TikTok,
|
||
`quelle_kanal` einer der drei Kanaele, `quelle_geholt_am` der
|
||
Zeitpunkt des Einlesens.
|
||
|
||
DAS COVERBILD STEHT HIER NICHT. Es liegt als Datei in unserer
|
||
eigenen Ablage (`dateien.eintrag_id`), und das ist der ganze
|
||
Punkt: TikToks Bildadresse traegt `x-expires` und haelt
|
||
GEMESSEN 48 Stunden. Wer sie speichert, hat uebermorgen schwarze
|
||
Kacheln -- und merkt es beim Bauen nicht und beim Testen nicht. */
|
||
try {
|
||
const spalten = d.prepare("PRAGMA table_info(eintraege)").all().map((s) => s.name);
|
||
for (const [name, typ] of [["quelle_url", "TEXT"], ["quelle_kanal", "TEXT"],
|
||
["quelle_geholt_am", "TEXT"],
|
||
/* ---- Stufe 5: fuehrt der Knopf noch irgendwohin? (18.09.2026) ----
|
||
|
||
Ein Video kann bei TikTok geloescht oder auf privat gestellt
|
||
werden. Bei uns bleibt der Eintrag stehen -- mit Cover, Text
|
||
und einem Knopf, der ins Leere fuehrt. Das faellt niemandem
|
||
auf, der die Seite baut; es faellt dem auf, der klickt.
|
||
|
||
`quelle_fehlversuche` ist der Grund, warum es drei Spalten
|
||
sind und nicht eine: Ein einzelner misslungener Abruf heisst
|
||
NICHT, dass das Video weg ist -- er heisst vielleicht, dass
|
||
TikTok gerade Schluckauf hat. Wer daraus sofort „geloescht"
|
||
macht, leert bei der naechsten Stoerung die halbe Galerie. */
|
||
["quelle_geprueft_am", "TEXT"], ["quelle_fehlt_seit", "TEXT"],
|
||
["quelle_fehlversuche", "INTEGER"]]) {
|
||
if (spalten.length && !spalten.includes(name)) {
|
||
d.exec(`ALTER TABLE eintraege ADD COLUMN ${name} ${typ}`);
|
||
console.log(`[workspace] Spalte '${name}' an den Eintraegen ergaenzt.`);
|
||
}
|
||
}
|
||
} catch (fehler) {
|
||
console.error("[workspace] Video-Spalten:", fehler?.message);
|
||
}
|
||
|
||
/* ---- Antworten an einem Beitrag (17.09.2026), Stufe 6 ----
|
||
|
||
"Das Rudel -- Hallo sagen, fragen, loben" war eine Liste aus
|
||
Karten. Der einzige Bereich, in dem Menschen MITEINANDER reden
|
||
sollen, zwang sie, nebeneinander zu reden: Jeder schreibt eine
|
||
eigene Karte, niemand antwortet jemandem.
|
||
|
||
EINE EIGENE TABELLE UND KEIN EINTRAG MIT "antwort_auf": Eine
|
||
Antwort hat keine Art, keinen Zustand, keine Frist und gehoert in
|
||
keinen Filter. Sie als halben Eintrag zu fuehren hiesse, ihr
|
||
zwanzig Spalten mitzugeben, die immer leer sind -- und sie taeuchte
|
||
in jeder Zaehlung auf, die Beitraege zaehlt.
|
||
|
||
ON DELETE CASCADE: Verschwindet der Beitrag, verschwinden die
|
||
Antworten. Eine Antwort ohne ihre Frage ist nicht halb so viel
|
||
wert, sondern gar nichts. */
|
||
try {
|
||
d.exec(`
|
||
CREATE TABLE IF NOT EXISTS eintrag_antworten (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
eintrag_id INTEGER NOT NULL REFERENCES eintraege(id) ON DELETE CASCADE,
|
||
person_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
text TEXT NOT NULL,
|
||
erstellt TEXT NOT NULL
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_antworten_eintrag
|
||
ON eintrag_antworten (eintrag_id, id);`);
|
||
} catch (fehler) {
|
||
console.error("[workspace] Antworten-Tabelle:", fehler?.message);
|
||
}
|
||
|
||
/* ---- Der Kreislauf: ein Eintrag kommt aus einem anderen (17.09.2026)
|
||
|
||
Aus dem Plan im Vault, Stufe 8 -- und die Stelle, an der der Plan
|
||
zum dritten Mal danebenlag: Dort stand "`aus_eintrag_id` gibt es in
|
||
der Tabelle bereits". Gibt es -- an den AUFGABEN. Die Eintraege
|
||
hatten nichts dergleichen.
|
||
|
||
WOZU: Heute sind die acht Bretter acht Silos. Ein Wunsch wird
|
||
angenommen, irgendwann passiert etwas im Stream, irgendwann liegt
|
||
ein Clip bei den Highlights -- und nichts davon weiss voneinander.
|
||
Wer den Wunsch geschrieben hat, erfaehrt nie, dass er erfuellt
|
||
wurde.
|
||
|
||
Mit dieser einen Spalte wird daraus eine Kette:
|
||
|
||
Wunsch -> Termin bei "Was ansteht" -> Clip bei "Highlights"
|
||
|
||
ON DELETE SET NULL und nicht CASCADE: Wird der Wunsch geloescht,
|
||
bleibt der Clip. Er ist ja trotzdem passiert. */
|
||
try {
|
||
const spalten = d.prepare("PRAGMA table_info(eintraege)").all().map((s) => s.name);
|
||
if (spalten.length && !spalten.includes("aus_eintrag_id")) {
|
||
d.exec("ALTER TABLE eintraege ADD COLUMN aus_eintrag_id INTEGER REFERENCES eintraege(id) ON DELETE SET NULL");
|
||
d.exec("CREATE INDEX IF NOT EXISTS idx_eintraege_aus ON eintraege (aus_eintrag_id)");
|
||
console.log("[workspace] Spalte 'aus_eintrag_id' an den Eintraegen ergaenzt.");
|
||
}
|
||
} catch (fehler) {
|
||
console.error("[workspace] Spalte 'aus_eintrag_id':", fehler?.message);
|
||
}
|
||
|
||
/* ---- Ein Bild an einem Community-Beitrag (17.09.2026) ----
|
||
|
||
"Highlights -- eure Clips und Bilder" konnte bis heute gar keine
|
||
Bilder tragen: `dateien` hing an `aufgabe_id`, nicht an einem
|
||
Eintrag. Der Plan im Vault sagte "Anhaenge sind schon da" -- sie
|
||
hingen nur am falschen Ding. */
|
||
try {
|
||
const spalten = d.prepare("PRAGMA table_info(dateien)").all().map((s) => s.name);
|
||
if (spalten.length && !spalten.includes("eintrag_id")) {
|
||
d.exec("ALTER TABLE dateien ADD COLUMN eintrag_id INTEGER REFERENCES eintraege(id) ON DELETE CASCADE");
|
||
d.exec("CREATE INDEX IF NOT EXISTS idx_dateien_eintrag ON dateien (eintrag_id)");
|
||
console.log("[workspace] Spalte 'eintrag_id' an den Dateien ergaenzt.");
|
||
}
|
||
} catch (fehler) {
|
||
console.error("[workspace] Spalte 'eintrag_id':", fehler?.message);
|
||
}
|
||
|
||
/* ---- Die Uhrzeit an einem Termin (17.09.2026) ----
|
||
|
||
"Was ansteht" bekommt einen Countdown: "Nächster Stream in 3 Std
|
||
12 Min". Beim Bauen stellte sich heraus, dass die Daten das gar
|
||
nicht hergeben -- `eintraege.datum` ist ein TAG
|
||
(`^\d{4}-\d{2}-\d{2}$`, die Pruefung lehnt eine Uhrzeit ab).
|
||
|
||
Das ist genau die Sorte Luecke, die man beim Planen uebersieht und
|
||
beim Bauen findet: Der Plan versprach eine Stundenangabe, und die
|
||
Datenbank kannte nur Tage.
|
||
|
||
Optional, nicht Pflicht. Viele Termine haben keine Uhrzeit ("diese
|
||
Woche", "im Oktober"), und ein Pflichtfeld haette dafuer eine
|
||
erfundene erzwungen. Ohne Uhrzeit zaehlt der Countdown in Tagen. */
|
||
try {
|
||
const spalten = d.prepare("PRAGMA table_info(eintraege)").all().map((s) => s.name);
|
||
if (spalten.length && !spalten.includes("uhrzeit")) {
|
||
d.exec("ALTER TABLE eintraege ADD COLUMN uhrzeit TEXT");
|
||
console.log("[workspace] Spalte 'uhrzeit' an den Eintraegen ergaenzt.");
|
||
}
|
||
} catch (fehler) {
|
||
console.error("[workspace] Spalte 'uhrzeit':", fehler?.message);
|
||
}
|
||
|
||
/* ---- Der Kanal an der Aufgabe (17.09.2026) ----
|
||
Drei Accounts, eine Aufgabenliste: Ohne diese Spalte ist
|
||
"schneide drei Ausschnitte" bei HasiDog und Clips mehrdeutig.
|
||
ADD COLUMN und kein Tabellenneubau -- es aendert sich keine
|
||
CHECK-Regel, alte Zeilen bleiben leer, und leer heisst hier
|
||
ausdruecklich "gilt fuer alles" und nicht "fehlt". */
|
||
try {
|
||
const spalten = d.prepare("PRAGMA table_info(aufgaben)").all().map((s) => s.name);
|
||
if (spalten.length && !spalten.includes("kanal")) {
|
||
d.exec("ALTER TABLE aufgaben ADD COLUMN kanal TEXT");
|
||
console.log("[workspace] Spalte 'kanal' an den Aufgaben ergaenzt.");
|
||
}
|
||
} catch (fehler) {
|
||
console.error("[workspace] Spalte 'kanal':", fehler?.message);
|
||
}
|
||
|
||
/* ---- Zeitpunkt des Hochladens in der Bibliothek ----
|
||
Eine neue Spalte laesst SQLite anstandslos anhaengen -- anders als
|
||
eine CHECK-Regel. Alte Eintraege bleiben leer und fallen auf das
|
||
Veroeffentlichungsdatum zurueck. */
|
||
try {
|
||
const spalten = d.prepare("PRAGMA table_info(wissen)").all().map((s) => s.name);
|
||
if (spalten.length && !spalten.includes("hochgeladen")) {
|
||
d.exec("ALTER TABLE wissen ADD COLUMN hochgeladen TEXT");
|
||
console.log("[workspace] Spalte 'hochgeladen' in der Bibliothek ergaenzt.");
|
||
}
|
||
} catch (fehler) {
|
||
console.error("[workspace] Spalte 'hochgeladen':", fehler?.message);
|
||
}
|
||
|
||
/* ---- Content-Planung: aus der Liste wird eine Strecke ----------------
|
||
|
||
Die Content-Planung war eine flache Liste mit drei Arten. Die
|
||
Recherche vom 31.08.2026 ist an dieser Stelle eindeutig: *"A content
|
||
calendar for creators is a pipeline, not a datebook."* Und der
|
||
wichtigste Einzelwert eines Kurzvideos ist der Aufhaenger -- die
|
||
ersten ein bis drei Sekunden entscheiden ueber die Verbreitung. Der
|
||
gehoert in ein eigenes Feld und nicht irgendwo in den Fliesstext.
|
||
|
||
Vier neue Spalten. ALTER TABLE ADD COLUMN laesst SQLite anstandslos
|
||
zu; alte Eintraege bleiben leer und stoeren nicht. */
|
||
/* ---- Der eigene Steckbrief (01.09.2026) ------------------------------
|
||
|
||
Jede Person -- Creator, Scout, Manager, DogFather -- bekommt ein
|
||
eigenes Profil: ein Bild, ein Satz ueber sich, und die Kanaele, auf
|
||
denen sie unterwegs ist.
|
||
|
||
Warum der TikTok-Name als eigene Spalte und nicht als freier Text:
|
||
Er wird zu einem Verweis, taucht in der Personenliste auf und soll
|
||
eindeutig sein. Gespeichert wird NUR der oeffentliche Name (das
|
||
Handle ohne @) -- kein Zugriffstoken, kein Passwort. Eine echte
|
||
Anmeldung bei TikTok braucht ein Entwicklerkonto mit Freischaltung
|
||
und laufender Pruefung durch TikTok; das waere eine eigene Baustelle
|
||
mit Kosten und Abhaengigkeit. Der Verweis leistet das, was hier
|
||
gebraucht wird: Man kommt mit einem Klick auf den Kanal, und in der
|
||
Uebersicht steht, wer wo unterwegs ist.
|
||
|
||
Das Bild liegt als Datei auf der Platte, in der Datenbank steht nur
|
||
der Dateiname. Bilder in eine SQLite-Datei zu legen macht jede
|
||
Sicherung um ein Vielfaches groesser -- und die Sicherung laeuft
|
||
jede Nacht. */
|
||
for (const [tabelle, spalte, typ] of [
|
||
["eintraege", "hook", "TEXT"], // die ersten Sekunden
|
||
["eintraege", "format", "TEXT"], // Talking Head, Tutorial, Story …
|
||
["eintraege", "saeule_id", "INTEGER"],// zu welcher Content-Saeule
|
||
["eintraege", "geplant", "TEXT"], // wann es raus soll
|
||
|
||
/* ---- WAS EIN SCOUT WIRKLICH AUFSCHREIBEN MUSS (11.09.2026) ----
|
||
|
||
Filipe, zur Pipeline: "perfektionnier diese kategorien ... und was
|
||
man da noch alles immer in jeder kategorie eintragen kann. weil
|
||
man kan da nichts machen."
|
||
|
||
Ein Lead hatte bis heute Name, Plattform, Handle, Priorität,
|
||
Aktivität, Potenzial, Notizen und zwei Daten. Das genügt, um
|
||
jemanden zu MERKEN, aber nicht, um ihn zu BEURTEILEN. Die sieben
|
||
Felder hier sind genau die Fragen, die ein Scout sonst im Kopf
|
||
behält -- und die der Nächste nicht mehr findet, wenn der Scout
|
||
im Urlaub ist.
|
||
|
||
`netzwerk` ist das wichtigste davon und kein Schmuck: Wer schon
|
||
bei einem anderen Creator-Netzwerk unter Vertrag steht, kann
|
||
nicht übernommen werden. Diese Frage ganz am Anfang zu stellen
|
||
spart die ganze übrige Arbeit -- und sie hier zu haben heisst,
|
||
dass niemand zweimal fragt.
|
||
|
||
`land` steht da, weil die Betreuung in DE, LU, AT und CH läuft
|
||
und die Schweiz rechtlich anders liegt als die drei EU-Länder
|
||
(siehe die TikTok-Recherche vom 10.09.). Auch dafür ist es
|
||
besser, es beim ersten Kontakt zu wissen als beim Vertrag. */
|
||
["leads", "follower", "INTEGER"], // Reichweite beim Fund
|
||
["leads", "land", "TEXT"], // DE / AT / CH / LU / andere
|
||
["leads", "woher", "TEXT"], // wie gefunden: LIVE, Empfehlung, Kommentar …
|
||
["leads", "kontaktweg", "TEXT"], // wo angeschrieben: TikTok-DM, Instagram …
|
||
["leads", "live_zeiten", "TEXT"], // wann die Person üblicherweise live ist
|
||
["leads", "netzwerk", "TEXT"], // schon bei einer Agentur? nein/ja/unbekannt
|
||
["leads", "absage_grund", "TEXT"], // warum es nicht gepasst hat
|
||
|
||
["personen", "bild", "TEXT"], // Dateiname des Profilbildes
|
||
["personen", "ueber_mich", "TEXT"], // ein Satz zur Person
|
||
["personen", "tiktok", "TEXT"], // öffentlicher Name, ohne @
|
||
["personen", "instagram", "TEXT"],
|
||
["personen", "youtube", "TEXT"],
|
||
["personen", "twitch", "TEXT"],
|
||
/* Die eigene Kachelfarbe im Chat (20.09.2026). Ein Schluessel aus
|
||
CHAT_KACHELN, nicht die Farbe selbst: Sonst stuende in der
|
||
Datenbank ein CSS-Wert, und eine Aenderung am Farbton hiesse,
|
||
jede Zeile anzufassen. NULL heisst "Standard" und ist der
|
||
Normalfall -- ein Pflichtwert waere eine Entscheidung, die
|
||
niemand treffen wollte. */
|
||
["personen", "chat_kachel", "TEXT"],
|
||
|
||
/* ---- Wiederkehrende Termine (02.09.2026) ----
|
||
Drei Spalten an `termine`, damit eine Ausprägung weiss, woher sie
|
||
stammt:
|
||
serie_id die Regel, aus der sie entstanden ist
|
||
serie_tag der geplante Tag -- daran erkennt der Nachfüller,
|
||
dass es diese Ausprägung schon gibt, und legt sie
|
||
kein zweites Mal an
|
||
serie_beruehrt jemand hat sie von Hand angefasst (verschoben,
|
||
umbenannt, abgehakt). Solche Termine bleiben beim
|
||
Abstellen der Serie stehen -- was jemand
|
||
bearbeitet hat, wird ihm nicht weggeräumt. */
|
||
["termine", "serie_id", "INTEGER REFERENCES termin_serien(id) ON DELETE SET NULL"],
|
||
["termine", "serie_tag", "TEXT"],
|
||
["termine", "serie_beruehrt", "INTEGER NOT NULL DEFAULT 0"],
|
||
|
||
/* ---- Frei eingetragene Namen (02.09.2026) ----
|
||
Wunsch: "ich will da auch sachen selber noch eintragen können,
|
||
also aussuchen und selbst eintragen."
|
||
|
||
Bis hierher konnte an einem Eintrag nur stehen, wer auch ein Konto
|
||
im Workspace hat. Eine Agentur, eine Marke, ein Gast liess sich
|
||
nicht eintragen -- man haette dafuer ein Konto anlegen muessen,
|
||
das nie jemand benutzt.
|
||
|
||
Eine EIGENE Spalte je Feld, kein Missbrauch der id-Spalte: Ein
|
||
Fremdschluessel zeigt auf eine Person oder auf nichts. Einen
|
||
Namen dort hineinzuschreiben haette jede Verknuepfung zerstoert.
|
||
|
||
Es gilt entweder/oder: Steht eine Person drin, ist das Feld leer,
|
||
und umgekehrt. Durchgesetzt wird das beim Speichern (siehe
|
||
externPruefen), nicht durch eine CHECK-Regel -- die liesse sich
|
||
an einer bestehenden Tabelle nicht mehr nachtragen.
|
||
|
||
WICHTIG, damit nichts still verschwindet: Ueberall, wo bisher der
|
||
Name der verknuepften Person gelesen wurde, faellt die Abfrage
|
||
jetzt auf diesen Text zurueck (siehe externSql). Ein frei
|
||
eingetragener Name steht dadurch in jeder Liste, jeder Suche und
|
||
jeder Uebersicht mit da -- gekennzeichnet als "(extern)", damit
|
||
niemand ihn fuer ein Konto haelt. */
|
||
["termine", "teilnehmer_extern", "TEXT"],
|
||
["termin_serien", "teilnehmer_extern", "TEXT"],
|
||
["aufgaben", "creator_extern", "TEXT"],
|
||
["aufgaben", "verantwortlich_extern", "TEXT"],
|
||
["eintraege", "creator_extern", "TEXT"],
|
||
["dateien", "creator_extern", "TEXT"],
|
||
|
||
/* ABBRECHEN (05.09.2026). Wunsch: *"ich will dass man in jedem
|
||
status die aufgaben auch abbrechen kann."*
|
||
|
||
Drei Spalten, weil "abgebrochen" allein zu wenig sagt:
|
||
abbruch_grund WARUM. Pflichtfeld in der Oberflaeche -- ohne
|
||
Grund weiss in vier Wochen niemand mehr, warum
|
||
etwas wegfiel, und es sieht aus wie Loeschen.
|
||
abgebrochen_am WANN.
|
||
abbruch_von WER. Nur Leitung und Scouts duerfen es, und
|
||
wer es war, gehoert dazu.
|
||
status_vorher IN WELCHEM ZUSTAND. Diese Spalte ist der Grund,
|
||
warum "abgebrochen" ein eigener Status wurde
|
||
und nicht bloss ein Haken: Ohne sie ginge beim
|
||
Wechsel verloren, ob die Arbeit schon lief.
|
||
"Im Review abgebrochen" ist eine ganz andere
|
||
Aussage als "nie angefangen". */
|
||
/* WIE EINE AUFGABE VERTEILT WURDE (21.09.2026).
|
||
|
||
Filipe, Abschnitt 3: *"Dogfather und die rechte Hand muessen
|
||
eine Aufgabe einer einzelnen Person zuweisen koennen. Sie
|
||
muessen eine Aufgabe auch mehreren bestimmten Personen
|
||
gleichzeitig zuweisen koennen. Zusaetzlich soll es eine Option
|
||
geben, bei der eine Aufgabe fuer mehrere Modis sichtbar ist und
|
||
die erste Modi, die gerade Zeit hat, die Aufgabe uebernehmen
|
||
kann."*
|
||
|
||
DREI WERTE, UND DER UNTERSCHIED ZWISCHEN DEN LETZTEN BEIDEN IST
|
||
DER GANZE PUNKT:
|
||
|
||
einzeln eine Person, sie macht es
|
||
mehrere mehrere Personen, JEDE macht ihren Teil
|
||
pool mehrere sehen es, EINE nimmt es -- danach ist es
|
||
fuer die anderen erledigt
|
||
|
||
Ohne diese Spalte waeren "mehrere" und "pool" in der Datenbank
|
||
nicht zu unterscheiden: Beide sind n Zeilen in
|
||
aufgaben_zuteilung. Die Oberflaeche muesste dann raten, ob eine
|
||
uebernommene Aufgabe fuer die anderen noch offen ist -- und
|
||
genau daraus entsteht die doppelte Bearbeitung, die der Auftrag
|
||
ausdruecklich verhindern will.
|
||
|
||
KEIN CHECK an dieser Spalte, mit Absicht: Sie wird nachgetragen,
|
||
und ein CHECK auf einer nachgetragenen Spalte hiesse die Tabelle
|
||
neu zu bauen -- der Weg, der in diesem Haus am 11.09. und am
|
||
21.09. schon Spalten gekostet hat. Geprueft wird beim
|
||
Schreiben, in der Route. */
|
||
["aufgaben", "verteilart", "TEXT"],
|
||
|
||
["aufgaben", "abbruch_grund", "TEXT"],
|
||
["aufgaben", "abgebrochen_am", "TEXT"],
|
||
["aufgaben", "abbruch_von", "INTEGER REFERENCES personen(id) ON DELETE SET NULL"],
|
||
["aufgaben", "status_vorher", "TEXT"],
|
||
|
||
/* AGENTUR-EVENTS (07.09.2026).
|
||
|
||
Ein Event ist kein Notizzettel mit Ueberschrift und Text. Wer
|
||
mitmachen soll, muss vier Dinge finden, ohne zu fragen: bis wann
|
||
es laeuft, was zu tun ist, was es zu gewinnen gibt und was
|
||
verboten ist. Genau daran scheitern Aktionen -- nicht am Aufruf,
|
||
sondern an den Rueckfragen danach.
|
||
|
||
Der BEGINN ist `datum` (gibt es schon, ist NOT NULL). Neu ist
|
||
das Ende und die beiden Textfelder. `event_`-Vorsatz, weil es im
|
||
Workspace bereits eine Tabelle `aufgaben` gibt -- eine Spalte
|
||
`aufgaben` auf `eintraege` waere beim Lesen von Code jedes Mal
|
||
eine Verwechslung.
|
||
|
||
ADD COLUMN und kein Tabellenneubau: Es aendert sich kein CHECK,
|
||
also darf die Tabelle stehen bleiben. Der Neubau vom 06.09. war
|
||
noetig, weil dort der erlaubte Wertebereich von `bereich` wuchs
|
||
-- und er ist der Weg, bei dem man Inhalte verlieren kann. */
|
||
["eintraege", "event_ende", "TEXT"],
|
||
["eintraege", "event_aufgaben", "TEXT"],
|
||
["eintraege", "event_regeln", "TEXT"],
|
||
|
||
/* DER TAGESRUF BRAUCHT MEHR ALS AN/AUS (08.09.2026, screen10).
|
||
|
||
Filipe: "ich will das neben diesem kreis auch ein kleiner button
|
||
ist fuer den wecker von den aufgaben, oder quasi eine taetige
|
||
meldung einmal am tag zu aktivieren wenn noch aufgaben auf sind."
|
||
|
||
Eine Uhrzeit ist kein Schalter. Statt einer eigenen Tabelle fuer
|
||
eine einzige Zahl bekommt `push_einstellungen` eine allgemeine
|
||
Spalte `wert`: Wo eine Art mehr braucht als an/aus, steht es
|
||
dort. Die vorhandenen Arten lassen sie leer und merken nichts
|
||
davon.
|
||
|
||
KEIN `NOT NULL`, KEINE VORGABE IN DER DATENBANK: Die Vorgabe
|
||
gehoert nach workspace-push.js zu den Arten, sonst stuende sie an
|
||
zwei Stellen -- und die in der Datenbank wuerde bei einer
|
||
Aenderung vergessen. Genau diese Sorte Doppelung hat mich heute
|
||
schon dreimal aufgehalten. */
|
||
["push_einstellungen", "wert", "TEXT"],
|
||
|
||
/* ANTWORTEN MIT ZITAT (09.09.2026, Punkt 15).
|
||
|
||
Filipe: "ich will dass du diese seite viel krasser und
|
||
detaillierter machst ... alles reinsetzt was wir noch gebrauchen
|
||
koennten."
|
||
|
||
In einem Gespraech zu zweit weiss man meistens, worauf sich
|
||
etwas bezieht. In einer Gruppe nicht -- dort laufen drei Faeden
|
||
parallel, und "ja, mach das" kann alles heissen. Die Spalte
|
||
zeigt auf die Nachricht, auf die geantwortet wird.
|
||
|
||
KEIN FREMDSCHLUESSEL AUF chat_nachrichten: Wird die zitierte
|
||
Nachricht zurueckgenommen, bleibt ihre Zeile ohnehin stehen
|
||
(nur der Text wird geleert) -- und wird der ganze Raum
|
||
geloescht, geht die Antwort per CASCADE ueber `raum_id`
|
||
ohnehin mit. Ein zweiter Schluessel brauchte nur eine zweite
|
||
Gelegenheit fuer eine Sperre. */
|
||
["chat_nachrichten", "antwort_auf", "INTEGER"],
|
||
|
||
/* WURDE HIER DAS GANZE RUDEL GERUFEN? (23.09.2026)
|
||
|
||
Filipe: „dan will ich auch dass nur die modis, rechte hand,
|
||
linke hand und dogfather, alle auch auf einmal markieren koennen
|
||
im chat mit einem, @rudel ,dann sollen alle eine benarichtigung
|
||
bekommen."
|
||
|
||
WARUM DAS GESPEICHERT WIRD UND NICHT BEIM LESEN NACHGERECHNET:
|
||
Beim Lesen muesste der Browser wissen, ob der ABSENDER es damals
|
||
durfte. Er weiss nur dessen heutige Rolle. Wechselt jemand die
|
||
Rolle, aenderte sich rueckwirkend, ob ein alter Satz das Rudel
|
||
gerufen hat -- die Hervorhebung verschwaende aus einer Nachricht
|
||
von vorletzter Woche, in der sie damals richtig war.
|
||
|
||
Und es waere die zweite Rechnung ueber dieselbe Frage: Der
|
||
Server entscheidet beim Schreiben, wer gerufen wird. Rechnete
|
||
der Browser beim Lesen noch einmal, haetten wir genau die Lage,
|
||
vor der chat-erwaehnung.js im ersten Absatz warnt -- die Seite
|
||
hebt jemanden hervor, den niemand benachrichtigt hat.
|
||
|
||
VORGABE 0. Alle Nachrichten von vor heute haben kein Rudel
|
||
gerufen, und das ist keine Annahme, sondern eine Tatsache: Es
|
||
gab das Wort noch nicht. */
|
||
["chat_nachrichten", "rudel", "INTEGER NOT NULL DEFAULT 0"],
|
||
|
||
/* GESPRAECH LOESCHEN -- NUR BEI MIR (09.09.2026).
|
||
|
||
Filipe: "man muss die chats auch geloescht bekommen!!!"
|
||
Auf Nachfrage entschieden: nur bei mir, nicht bei beiden. Das
|
||
Gespraech verschwindet aus MEINER Liste, das Gegenueber behaelt
|
||
seinen Verlauf. Sonst koennte jeder jedem die Unterhaltung
|
||
vernichten -- und in einem Gespraech, in dem Absprachen stehen,
|
||
waere das ein Loch, das niemand mehr fuellen kann.
|
||
|
||
ZWEI SPALTEN, NICHT EINE, und der Grund ist ein Randfall:
|
||
`geloescht_bis` haelt fest, bis zu welcher Nachricht ich es nicht
|
||
mehr sehen will. Bei einem Gespraech OHNE jede Nachricht waere
|
||
das 0 -- und 0 heisst auch "nie geloescht". `geloescht_am`
|
||
unterscheidet die beiden Faelle. Kommt spaeter etwas Neues
|
||
(id > geloescht_bis), taucht das Gespraech von selbst wieder auf;
|
||
das Alte bleibt weg. */
|
||
["chat_teilnehmer", "geloescht_bis", "INTEGER NOT NULL DEFAULT 0"],
|
||
["chat_teilnehmer", "geloescht_am", "TEXT"],
|
||
|
||
/* GESPRAECHE OBEN ANHEFTEN (25.09.2026).
|
||
|
||
Filipe: „ich will dass man auch individuel jeder fuer sich auch
|
||
in der liste chats fixieren kann. auch mehrere nicht nur eins."
|
||
|
||
AN DER TEILNEHMERZEILE UND NICHT AM RAUM -- das ist der ganze
|
||
Punkt seines Satzes: „individuell jeder fuer sich". Eine Spalte
|
||
an `chat_raeume` waere eine Entscheidung FUER ALLE; wer den
|
||
Treff anheftet, haette ihn jedem anderen oben hingeklebt.
|
||
|
||
EIN ZEITSTEMPEL UND KEIN JA/NEIN. Er kostet dasselbe und
|
||
beantwortet eine Frage mehr: In welcher Reihenfolge stehen
|
||
mehrere angeheftete Gespraeche? Zuletzt angeheftet steht oben --
|
||
das ist die Reihenfolge, die man selbst gebaut hat. Mit einer
|
||
0/1-Spalte muesste man sie erfinden. */
|
||
["chat_teilnehmer", "fixiert_am", "TEXT"],
|
||
|
||
/* ANHAENGE: FOTOS UND PDF (09.09.2026).
|
||
|
||
Filipe: "am besten waere es auch wenn man da auch im chat pdfs
|
||
schicken koennte. pdfs und fotos."
|
||
|
||
Je Nachricht hoechstens ein Anhang -- deshalb Spalten und keine
|
||
eigene Tabelle. Eine Tabelle waere die richtige Wahl fuer
|
||
"mehrere je Nachricht"; die gibt es hier nicht, und eine leere
|
||
Verbundabfrage bei jedem Verlauf waere Aufwand fuer einen Fall,
|
||
den es nicht gibt.
|
||
|
||
`anhang_datei` ist der ERZEUGTE Name auf der Platte, niemals der
|
||
eingeschickte. `anhang_art` ist "bild" oder "pdf" und wird aus
|
||
den ersten Bytes der Datei bestimmt, nicht aus dem Namen und
|
||
nicht aus dem Content-Type -- beide kommen vom Absender.
|
||
Breite und Hoehe stehen dabei, damit der Verlauf beim Laden
|
||
eines Bildes nicht springt. */
|
||
["chat_nachrichten", "anhang_datei", "TEXT"],
|
||
["chat_nachrichten", "anhang_name", "TEXT"],
|
||
["chat_nachrichten", "anhang_art", "TEXT"],
|
||
["chat_nachrichten", "anhang_typ", "TEXT"],
|
||
["chat_nachrichten", "anhang_groesse", "INTEGER"],
|
||
["chat_nachrichten", "anhang_breite", "INTEGER"],
|
||
["chat_nachrichten", "anhang_hoehe", "INTEGER"],
|
||
|
||
/* WIE LANG IST DIE SPRACHNACHRICHT? (23.09.2026)
|
||
|
||
Filipe: „dan will ich auch dass man im chat auch sprachnachrichten
|
||
und gifts reinschicken kann."
|
||
|
||
In Millisekunden, und sie ist AUSDRUECKLICH nur fuer die Anzeige,
|
||
bevor die Datei geladen ist: In der Gespraechsliste steht
|
||
„Sprachnachricht (0:14)", und in der Blase steht die Laenge schon
|
||
da, waehrend der Ton noch laedt. Ohne sie springt die Zeile in
|
||
dem Moment, in dem die Zahl nachkommt.
|
||
|
||
SIE KOMMT VOM ABSENDER und ist damit eine Behauptung, keine
|
||
Messung -- das gehoert gesagt. Sie zu ueberpruefen hiesse, vier
|
||
Behaelterformate zu zerlegen (WebM schreibt die Dauer beim
|
||
Aufnehmen oft gar nicht erst hinein). Der Wert wird deshalb
|
||
begrenzt, und die WAHRE Laenge sagt ohnehin das Abspielgeraet,
|
||
sobald es die Datei kennt. Was wirklich schuetzt, ist die
|
||
Byte-Grenze, nicht diese Zahl. */
|
||
["chat_nachrichten", "anhang_dauer", "INTEGER"],
|
||
|
||
/* WELCHE VORLAGE DAHINTERSTECKT (09.09.2026).
|
||
|
||
Filipe: "ich will nur dass du dich informierst und schaust was
|
||
man vielleicht noch besser machen koennte von der benutzung ...
|
||
ich red von den aufgaben."
|
||
|
||
Beim Durchgehen der Bedienung ist aufgefallen: Man konnte
|
||
dieselbe Vorlage zweimal uebernehmen und bekam die Aufgabe
|
||
zweimal auf das Brett -- ohne jeden Hinweis. Im Vorlagenbrett
|
||
sah man nicht, was man schon geholt hatte.
|
||
|
||
Die Kennung der Vorlage steht jetzt an der Aufgabe. Damit kann
|
||
das Brett zeigen, was bereits uebernommen ist, und es braucht
|
||
dafuer KEINE zweite Abfrage: Die Aufgabenliste hat der Browser
|
||
ohnehin schon. Ueber den TITEL zu vergleichen waere die
|
||
naheliegende Abkuerzung gewesen -- und faellt in dem Moment um,
|
||
in dem jemand den Titel einer uebernommenen Aufgabe aendert. */
|
||
["aufgaben", "vorlage", "TEXT"],
|
||
|
||
/* DER SUCHSCHLUESSEL ZUM CODE (09.09.2026).
|
||
|
||
Bisher gab es nur den scrypt-Hash, und der ist mit Absicht
|
||
langsam (rund 150 ms). Wer eine Person AM CODE finden will, muss
|
||
deshalb alle Kandidaten der Reihe nach durchrechnen -- die
|
||
Anmeldung tut genau das, und im Code steht seit langem der
|
||
Hinweis, dass das ab etwa 50 Codes je Rolle spuerbar wird.
|
||
|
||
Diese Spalte ist die schnelle Antwort auf "zu welcher Person
|
||
gehoert dieser Code?": ein HMAC ueber den Code, in Mikrosekunden
|
||
berechnet und mit einem Index hinterlegt.
|
||
|
||
WARUM DAS UNBEDENKLICH IST -- nachgemessen, nicht angenommen: Ein
|
||
Code besteht aus 16 Zeichen eines 32er-Alphabets, das sind 80 Bit
|
||
Zufall. Selbst wenn jemand die ganze Datenbank haette, waere
|
||
Durchprobieren aussichtslos, egal wie schnell die Rechnung ist.
|
||
Der Schluessel steckt zusaetzlich nicht im Code, sondern in den
|
||
Einstellungen (siehe kennungSchluessel).
|
||
|
||
DER SCRYPT-HASH BLEIBT DIE ENTSCHEIDUNG. Der Suchschluessel sagt
|
||
nur, WEN man pruefen soll; ob der Code stimmt, sagt weiterhin
|
||
allein scrypt. Ein Treffer hier allein laesst niemanden herein. */
|
||
["personen", "code_kennung", "TEXT"],
|
||
|
||
/* DIE STUFE IM TEAM (10.09.2026).
|
||
|
||
Blueprint V3.0, Kapitel 4.1: Probe, Standard, Senior. Ein
|
||
Probe-Modi ist in der Einarbeitung, ein Senior kann die rechte
|
||
Hand bei Abwesenheit vertreten.
|
||
|
||
WARUM OHNE CHECK-REGEL, anders als bei `rolle`: Eine CHECK-Liste
|
||
laesst sich in SQLite nur ueber einen Tabellenneubau erweitern
|
||
(siehe checkListeErweitern) -- und die Stufen sind eine
|
||
ARBEITSEINTEILUNG, keine Rechtegrenze. Kaeme morgen eine vierte
|
||
dazu, waere ein Tabellenneubau auf einer Live-Datenbank ein
|
||
hoher Preis fuer eine Beschriftung. Geprueft wird deshalb im
|
||
Code, an genau einer Stelle (STUFEN), und was dort nicht
|
||
draufsteht, wird abgewiesen.
|
||
|
||
NULL HEISST "Probe" und nicht "unbekannt". Ein dritter Zustand
|
||
waere eine Frage, die niemand beantworten kann ("was ist er
|
||
denn nun?"); und ein neuer Mensch faengt ohnehin in der
|
||
Einarbeitung an. Beim Lesen wird NULL deshalb auf 'probe'
|
||
abgebildet -- an einer Stelle, nicht in jeder Abfrage.
|
||
|
||
Die Spalte heisst wie `punkt_stand.stufe`, meint aber etwas
|
||
anderes (dort: gut/verbessern). Zwei Tabellen, kein Konflikt --
|
||
aber wer beides im selben Kopf hat, sollte es wissen. */
|
||
["personen", "stufe", "TEXT"],
|
||
|
||
/* DIE KATEGORIE EINER AUFGABE (10.09.2026).
|
||
|
||
Kapitel 6.1 des Anforderungsdokuments verlangt sie an jeder
|
||
Aufgabe. Filipes Entscheidung dazu: nur bei den Modis -- fuer
|
||
Creator, Scouts und Manager bleibt das Formular, wie es war.
|
||
|
||
KEIN CHECK IN DER SPALTE, obwohl die Werte fest sind. Ein CHECK
|
||
laesst sich in SQLite nicht aendern; die Liste um eine Kategorie
|
||
zu erweitern hiesse jedes Mal, die ganze Tabelle neu zu bauen.
|
||
Geprueft wird stattdessen beim Schreiben (siehe KATEGORIEN in
|
||
workspace-aufgaben.js) -- an EINER Stelle, durch die alle
|
||
Schreibwege laufen. */
|
||
["aufgaben", "kategorie", "TEXT"],
|
||
|
||
/* WAS DOGFATHER DAZU TUN MUESSTE (10.09.2026).
|
||
|
||
Filipe: *"auch entscheiden was ich machen muss wenn das und das
|
||
erledigt wird."* Der Modi schreibt es beim Angebot dazu; wird
|
||
das Angebot angenommen, wird genau daraus seine Aufgabe.
|
||
|
||
Ein eigenes Feld und nicht im Fliesstext: Aus einem Absatz laesst
|
||
sich keine Aufgabe machen, ohne zu raten, welcher Satz gemeint
|
||
war. */
|
||
["eintraege", "einsatz", "TEXT"],
|
||
|
||
/* KATEGORIE-KANAELE (10.09.2026, Kapitel 7.2).
|
||
|
||
"Kategorie-Kanaele: z. B. Clipping-Team, Live-Moderation --
|
||
Owner/rechte Hand haben Zugriff auf alle Kanaele."
|
||
|
||
Ein Kanal ist KEINE vierte Tabelle, sondern eine dritte Art Raum.
|
||
Damit gilt fuer ihn ohne eine einzige neue Zeile alles, was schon
|
||
da ist: Verlauf, Anhaenge, Suche, Ungelesen-Zaehler, der
|
||
Live-Strom, das Wegraeumen. Eine eigene Tabelle haette all das
|
||
ein zweites Mal gebraucht -- und die zweite Fassung waere die
|
||
gewesen, in der die Zugriffsregel fehlt.
|
||
|
||
WELCHE Zustaendigkeit, steht in kategorie und kommt aus
|
||
MODI_KATEGORIEN -- derselben Liste, aus der auch die Aufgaben
|
||
ihre Kategorie nehmen. Ein Kanal "Clipping" und Aufgaben
|
||
"clipping" waeren sonst zwei Woerter fuer dieselbe Sache. */
|
||
["chat_raeume", "kategorie", "TEXT"],
|
||
|
||
/* WER DARF DIESE RUECKMELDUNG LESEN? (10.09.2026)
|
||
|
||
0 = das ganze Team, 1 = nur DogFather.
|
||
|
||
WARUM ES DIESE WAHL UEBERHAUPT GIBT: Filipe will beides -- eine
|
||
offene Teamkultur UND ehrliche Rueckmeldung an sich selbst. Das
|
||
ist kein Widerspruch, aber es sind zwei verschiedene Raeume. Wer
|
||
"was koenntest du besser machen" vor versammelter Mannschaft
|
||
sagen muss, sagt es nicht; wer alles nur unter vier Augen sagen
|
||
kann, hat kein Team-Gespraech. Also entscheidet der Schreibende,
|
||
Zeile fuer Zeile.
|
||
|
||
UND "NUR DOGFATHER" HEISST NUR DOGFATHER -- die rechte Hand
|
||
ausdruecklich nicht. Sie sieht sonst ueberall dasselbe wie er;
|
||
hier nicht, und zwar weil das Etikett sonst nicht stimmen wuerde.
|
||
Eine Zusage, die im Kleingedruckten eine Ausnahme hat, ist keine.
|
||
Sie kann selbst genauso schreiben -- auch ueber ihn. */
|
||
["eintraege", "nur_leitung", "INTEGER"],
|
||
|
||
/* WANN WURDE ES DER PERSON GESAGT? (11.09.2026)
|
||
|
||
Der Entwicklungs-Bereich ist eine Aufzeichnung fuer DogFather und
|
||
die rechte Hand -- die Person selbst sieht ihn nicht. Das ist
|
||
Absicht: Eine halb sichtbare Akte ist schlimmer als eine
|
||
geschlossene, weil niemand mehr weiss, was der andere gerade
|
||
liest.
|
||
|
||
Damit Anerkennung trotzdem ANKOMMT (die Forschung dazu ist
|
||
eindeutig: Dank und Rueckmeldung sind der staerkste Grund, warum
|
||
Freiwillige bleiben), kann jeder Eintrag als Nachricht an die
|
||
Person geschickt werden. Hier steht, WANN das geschehen ist.
|
||
|
||
Ohne diese Spalte gaebe es die Frage "habe ich ihr das eigentlich
|
||
schon gesagt?" -- und die beantwortet man nach zwei Wochen
|
||
falsch. */
|
||
["eintraege", "gesendet_am", "TEXT"],
|
||
|
||
/* ANGEHEFTETE NACHRICHTEN (10.09.2026, Kapitel 7.2).
|
||
|
||
"Ankuendigungs-Funktion: Pin-Nachrichten von Owner/rechte Hand
|
||
an alle."
|
||
|
||
Am Anheften haengt ein ZWEITER Name (angeheftet_von) und nicht
|
||
nur ein Datum. Eine Ankuendigung ohne Absender ist ein Aushang
|
||
ohne Unterschrift -- und wer sie geloest hat, ist genau die
|
||
Frage, die spaeter gestellt wird.
|
||
|
||
KEIN FREMDSCHLUESSEL AUF personen: Wird jemand geloescht, soll
|
||
die Ankuendigung nicht mitverschwinden. Dieselbe Ueberlegung wie
|
||
bei antwort_auf weiter oben. */
|
||
["chat_nachrichten", "angeheftet_am", "TEXT"],
|
||
["chat_nachrichten", "angeheftet_von", "INTEGER"],
|
||
|
||
/* DAS BESTAETIGTE ALTER (15.09.2026, Stufe 8).
|
||
|
||
Auf der Regelseite des Treffs steht seit dem 11.09. "ab 18" --
|
||
und geprueft wurde es nirgends. Eine Regel, die nur dasteht, ist
|
||
keine; im Streitfall ist sie sogar schlechter als keine, weil man
|
||
sie versprochen hat.
|
||
|
||
NUR EIN ZEITSTEMPEL, KEIN GEBURTSDATUM. Gebraucht wird die
|
||
Antwort auf "hat bestaetigt, alt genug zu sein, und wann" --
|
||
nicht der Geburtstag. Ein Geburtsdatum waere mehr Daten fuer
|
||
dieselbe Auskunft, und Datenminimierung (Art. 5 Abs. 1 lit. c)
|
||
gilt auch fuer das eigene Nachweisbeduerfnis.
|
||
|
||
WARUM KEINE SPALTE FUER DIE RECHTSGRUNDLAGE: Sie folgt aus der
|
||
Rolle -- ein Modi arbeitet hier (Vertrag), ein Community-Mitglied
|
||
ist freiwillig da (Einwilligung). Was sich ableiten laesst, wird
|
||
nicht gespeichert; sonst haette man zwei Wahrheiten, von denen
|
||
eine veraltet. Siehe grundlageVon() in workspace-aufbewahrung.js. */
|
||
["personen", "alter_bestaetigt_am", "TEXT"],
|
||
|
||
/* WOHER EINE AUFGABE KAM (15.09.2026, Stufe 9: "Idee -> Aufgabe").
|
||
|
||
MIT deklariertem Fremdschluessel, und das ist hier nicht
|
||
Geschmack: Seit dem 15.09. prueft pruef-auskunft, dass JEDE
|
||
Spalte auf _id entweder einen Fremdschluessel hat oder
|
||
namentlich eingeordnet ist. Eine undeklarierte Spalte waere dort
|
||
sofort rot -- und genau so soll es sein, denn niemand kann einer
|
||
Zahl ansehen, worauf sie zeigt.
|
||
|
||
ON DELETE SET NULL und nicht CASCADE: Wird die Idee spaeter
|
||
geloescht, bleibt die Arbeit. Sie ist inzwischen etwas Eigenes;
|
||
sie mit der Notiz zu entfernen, aus der sie entstand, waere die
|
||
teuerste Art, Ordnung zu machen. */
|
||
["aufgaben", "aus_eintrag_id", "INTEGER REFERENCES eintraege(id) ON DELETE SET NULL"],
|
||
|
||
/* FASSUNGEN STATT UNVERBUNDENER DOPPEL (15.09.2026, Stufe 9).
|
||
|
||
GEMESSEN, BEVOR ETWAS GEBAUT WURDE: Der Plan sagt "Dateiversionen
|
||
statt Ueberschreiben" -- ueberschrieben wurde aber nie.
|
||
„name_datei“ ist eindeutig und zufaellig, jedes Hochladen legt
|
||
eine neue Zeile an. Der Mangel ist ein anderer, und er ist
|
||
gefaehrlicher als Ueberschreiben:
|
||
|
||
Zwei Dateien gleichen Namens stehen BEZIEHUNGSLOS nebeneinander.
|
||
Die alte kann "freigegeben" sein, die neue "entwurf" -- und wer
|
||
die Liste ansieht, laedt die freigegebene herunter. Also die
|
||
falsche. Kein Fehler, keine Meldung, nur die alte Fassung in der
|
||
Hand.
|
||
|
||
ON DELETE SET NULL: Wird die alte Fassung geloescht, bleibt die
|
||
neue -- sie verliert nur ihre Vorgeschichte. */
|
||
["dateien", "ersetzt_id", "INTEGER REFERENCES dateien(id) ON DELETE SET NULL"],
|
||
|
||
/* ANHAENGE AN AUFGABEN (15.09.2026, Stufe 9).
|
||
|
||
KEIN ZWEITER HOCHLADEWEG. Dateien leben in der Dateiablage -- mit
|
||
ihrer Sichtbarkeit, ihren Freigaben, ihren Fassungen und ihrem
|
||
Protokoll. Ein eigener Weg an der Aufgabe waere eine zweite
|
||
Ablage mit einer zweiten Rechtelogik, und die zweite ist immer
|
||
die, die etwas durchlaesst.
|
||
|
||
Deshalb nur eine Spalte: Die Datei WEISS, zu welcher Aufgabe sie
|
||
gehoert. Alles andere bleibt, wo es ist.
|
||
|
||
ON DELETE SET NULL, NICHT CASCADE -- und das ist der wichtigste
|
||
Teil dieser Zeile: Wer eine Aufgabe loescht, will die Aufgabe
|
||
loeschen, nicht den Vertrag, der daran hing. Die Datei verliert
|
||
nur ihren Bezug und steht danach wieder in der Ablage. */
|
||
["dateien", "aufgabe_id", "INTEGER REFERENCES aufgaben(id) ON DELETE SET NULL"],
|
||
|
||
/* DER AUFWAND EINER AUFGABE (15.09.2026, Stufe 9).
|
||
|
||
"Wichtig" gibt es schon -- das ist die Prioritaet. Was fehlte, ist
|
||
die zweite Angabe, ohne die man nicht entscheiden kann: Schaffe
|
||
ich das zwischendurch, oder muss ich mir Zeit nehmen?
|
||
|
||
OHNE CHECK-LISTE in der Datenbank, und das ist Absicht: Die
|
||
erlaubten Werte stehen in workspace-womit.js, und eine CHECK-Liste
|
||
daneben waere eine zweite Wahrheit, die beim naechsten Wert
|
||
nachgezogen werden muesste -- ueber einen Tabellenneubau, an dem
|
||
im Projekt schon dreimal Spalten verlorengegangen sind. Geprueft
|
||
wird beim Schreiben, an der Stelle, die die Liste kennt. */
|
||
["aufgaben", "aufwand", "TEXT"],
|
||
|
||
/* DIE RUNDE AN DER SUPPORT-MELDUNG (25.09.2026). Filipe: „mit dem
|
||
gedanken das manchmal sachen mehrmal nicht sofort perfekt sein
|
||
werden." Auf bestehenden Datenbanken gibt es die Spalte noch
|
||
nicht -- ohne sie liefe jede Meldung in einen SQL-Fehler statt
|
||
in eine Antwort. */
|
||
["support_meldungen", "runde", "INTEGER NOT NULL DEFAULT 1"],
|
||
|
||
/* AUS WELCHER BEOBACHTUNG DIESE AUFGABE ENTSTANDEN IST (15.09.2026).
|
||
|
||
Der Schluessel aus dem Entwicklungskatalog, nichts weiter. Zusammen
|
||
mit `verantwortlich_id` ergibt er das Paar, um das es geht: DIESER
|
||
Punkt bei DIESEM Menschen.
|
||
|
||
WARUM ÜBERHAUPT: Eine Beobachtung, die nur auf einer Karte steht,
|
||
ist eine Notiz. Die Quellen zur laufenden Entwicklungsbegleitung
|
||
sagen dasselbe in einem Satz -- ein Entwicklungsplan wirkt dann,
|
||
wenn er an einer echten Aufgabe haengt, und sonst nicht.
|
||
|
||
KEIN FREMDSCHLUESSEL: Die Punkte stehen in einer Datei, nicht in
|
||
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` steht hier ABSICHTLICH NICHT. Seine Tabelle wird in
|
||
workspace-material.js angelegt, und diese Schleife laeuft auf
|
||
einer frischen Datenbank vorher -- sie haette die Spalte still
|
||
uebersprungen, und das Schreiben waere erst im Betrieb
|
||
gescheitert. Sie steht dort, wo die Tabelle wohnt. */
|
||
/* 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);
|
||
if (vorhanden.length && !vorhanden.includes(spalte)) {
|
||
d.exec(`ALTER TABLE ${tabelle} ADD COLUMN ${spalte} ${typ}`);
|
||
console.log(`[workspace] Spalte '${spalte}' in ${tabelle} ergaenzt.`);
|
||
}
|
||
} catch (fehler) {
|
||
console.error(`[workspace] Spalte '${spalte}':`, fehler?.message);
|
||
}
|
||
}
|
||
|
||
/* 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
|
||
Bauplan, weil die beiden Spalten erst durch die Schleife darüber
|
||
entstehen -- oben gäbe es sie beim ersten Start noch nicht. */
|
||
try {
|
||
/* Ohne Index waere der Suchschluessel sinnlos -- SQLite laese die
|
||
ganze Personentabelle, und genau das sollte er ersparen. Steht
|
||
aus demselben Grund hier unten wie der Termin-Index: Die Spalte
|
||
entsteht erst durch die Schleife darueber. */
|
||
d.exec("CREATE INDEX IF NOT EXISTS idx_personen_kennung ON personen (code_kennung)");
|
||
} catch (fehler) {
|
||
console.error("[workspace] Index idx_personen_kennung:", fehler?.message);
|
||
}
|
||
|
||
try {
|
||
d.exec("CREATE INDEX IF NOT EXISTS idx_termine_serie ON termine (serie_id, serie_tag)");
|
||
} catch (fehler) {
|
||
console.error("[workspace] Index idx_termine_serie:", fehler?.message);
|
||
}
|
||
|
||
/* ---- Die vorhandenen Teilnehmer in die neue Tabelle uebernehmen ----
|
||
|
||
Ohne diesen Schritt haette jeder Termin, der vor dem 05.09.2026
|
||
angelegt wurde, eine LEERE Teilnehmerliste -- und weil an dieser
|
||
Liste die Sichtbarkeit haengt, faende die eingetragene Person ihren
|
||
eigenen Termin nicht mehr. Ein stiller Datenverlust, der erst
|
||
auffaellt, wenn jemand einen Call sucht, den es noch gibt.
|
||
|
||
`INSERT OR IGNORE` macht den Lauf wiederholbar: Beim zweiten Start
|
||
ist alles schon da und nichts passiert. Deshalb steht das hier bei
|
||
den Umstellungen und nicht in einem einmaligen Skript, das jemand
|
||
vergessen koennte. */
|
||
for (const [tabelle, quelle, ziel, schluessel] of [
|
||
["termin_teilnehmer", "termine", "termin_id", "id"],
|
||
["serie_teilnehmer", "termin_serien", "serie_id", "id"],
|
||
]) {
|
||
try {
|
||
const spalten = d.prepare(`PRAGMA table_info(${quelle})`).all().map((s) => s.name);
|
||
if (!spalten.includes("teilnehmer_id")) continue;
|
||
const vorher = d.prepare(`SELECT COUNT(*) AS n FROM ${tabelle}`).get().n;
|
||
d.exec(`INSERT OR IGNORE INTO ${tabelle} (${ziel}, person_id)
|
||
SELECT ${schluessel}, teilnehmer_id FROM ${quelle}
|
||
WHERE teilnehmer_id IS NOT NULL`);
|
||
const nachher = d.prepare(`SELECT COUNT(*) AS n FROM ${tabelle}`).get().n;
|
||
if (nachher > vorher) {
|
||
console.log(`[workspace] ${nachher - vorher} vorhandene Teilnehmer nach ${tabelle} uebernommen.`);
|
||
}
|
||
} catch (fehler) {
|
||
console.error(`[workspace] Uebernahme nach ${tabelle}:`, fehler?.message);
|
||
}
|
||
}
|
||
|
||
loeschspurenAufraeumen(d);
|
||
|
||
/* =====================================================================
|
||
DIE BLAUEN HERZEN WERDEN BABYBLAU (22.09.2026)
|
||
=====================================================================
|
||
|
||
Filipe: "jedes normales blaues herz durch babyblaues herz
|
||
ersetzen ... jeder normale blaue farbe, sei es die kachel im chat
|
||
oder emojis, nur babyblau bitte."
|
||
|
||
Der Katalog (workspace-reaktionen.js) kennt seit heute nur noch
|
||
das babyblaue. WAS SCHON IN DER DATENBANK STEHT, WANDERT MIT --
|
||
ohne diese Zeilen waeren es vier Reaktionen, die `ERLAUBT` nicht
|
||
mehr kennt. Der Verlauf filtert danach: Sie verschwaenden still,
|
||
und niemand koennte sagen, wann und warum.
|
||
|
||
`UPDATE OR IGNORE`, DANN `DELETE`: Der Primaerschluessel ist
|
||
(nachricht_id, person_id, zeichen). Hat jemand auf dieselbe
|
||
Nachricht BEIDES gesetzt, liefe ein blankes UPDATE auf den
|
||
Schluessel auf. So gewinnt das vorhandene babyblaue, und das
|
||
blaue faellt weg -- zweimal dasselbe Herz an einer Nachricht
|
||
waere ohnehin keine zwei Reaktionen.
|
||
|
||
WIEDERHOLBAR: Beim zweiten Start gibt es nichts mehr zu tun. */
|
||
try {
|
||
const blau = "\u{1F499}", babyblau = "\u{1FA75}";
|
||
const offen = d.prepare(
|
||
"SELECT COUNT(*) AS n FROM chat_reaktionen WHERE zeichen = ?").get(blau).n;
|
||
if (offen > 0) {
|
||
d.prepare("UPDATE OR IGNORE chat_reaktionen SET zeichen = ? WHERE zeichen = ?")
|
||
.run(babyblau, blau);
|
||
const rest = d.prepare("DELETE FROM chat_reaktionen WHERE zeichen = ?").run(blau).changes;
|
||
console.log(`[workspace] ${offen} blaue Herzen auf babyblau umgestellt`
|
||
+ (rest ? ` (${rest} doppelte entfernt)` : ""));
|
||
}
|
||
} catch (fehler) {
|
||
console.error("[workspace] Herzen umstellen:", fehler?.message);
|
||
}
|
||
|
||
/* Content-Saeulen: die drei bis fuenf Themen, aus denen der Kanal
|
||
besteht. Aus der Recherche: 3-5 Saeulen nach der 70/20/10-Regel
|
||
(70 % Wert, 20 % Community, 10 % Eigenwerbung); eine Saeule wird
|
||
erst brauchbar, wenn Formate an ihr haengen, und einmal im Monat
|
||
schaut man nach, welche ihr Gewicht traegt.
|
||
|
||
Je Creator eigene Saeulen -- ein Gaming-Kanal und ein Bastelkanal
|
||
haben nichts gemeinsam, eine gemeinsame Liste waere fuer beide
|
||
falsch. */
|
||
try {
|
||
d.exec(`
|
||
CREATE TABLE IF NOT EXISTS content_saeulen (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
creator_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
name TEXT NOT NULL,
|
||
ziel INTEGER, -- gewuenschter Anteil in Prozent
|
||
slot INTEGER NOT NULL DEFAULT 1, -- Farbplatz 1..5, siehe content.css
|
||
erstellt TEXT NOT NULL
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_saeulen_creator ON content_saeulen (creator_id);
|
||
`);
|
||
} catch (fehler) {
|
||
console.error("[workspace] content_saeulen:", fehler?.message);
|
||
}
|
||
|
||
/* Die alte Art "produktion" wird zu "gedreht". Vorher gab es drei
|
||
Stufen (Idee, Produktion, Veroeffentlicht) -- dazwischen lag alles,
|
||
was in Wahrheit den Unterschied macht: Aufhaenger geschrieben,
|
||
abgedreht, geschnitten, terminiert. Ohne diese Umbenennung fielen
|
||
bestehende Eintraege aus jeder Spalte heraus und waeren unsichtbar. */
|
||
try {
|
||
const betroffen = d.prepare(
|
||
"SELECT COUNT(*) AS n FROM eintraege WHERE bereich = 'content' AND art = 'produktion'").get()?.n ?? 0;
|
||
if (betroffen) {
|
||
d.prepare("UPDATE eintraege SET art = 'gedreht' WHERE bereich = 'content' AND art = 'produktion'").run();
|
||
console.log(`[workspace] ${betroffen} Content-Eintraege von 'produktion' auf 'gedreht' umgestellt.`);
|
||
}
|
||
} catch (fehler) {
|
||
console.error("[workspace] Content-Arten:", fehler?.message);
|
||
}
|
||
|
||
/* ---- Status "abgebrochen" erlauben (05.09.2026) ----
|
||
|
||
Der CHECK-Constraint einer Tabelle laesst sich in SQLite nicht
|
||
aendern -- kein ALTER TABLE der Welt hilft, die Tabelle muss neu
|
||
gebaut werden. Deshalb derselbe Weg wie bei der Rolle "manager"
|
||
darunter: erst sichern, dann tauschen, danach die Verweise pruefen.
|
||
|
||
WARUM UEBERHAUPT EIN NEUER STATUS und nicht bloss ein Haken
|
||
"abgebrochen": Weil die vier Spalten des Bretts nach Status
|
||
gruppieren. Eine abgebrochene Aufgabe faellt damit von selbst aus
|
||
allen vieren heraus -- ohne dass irgendeine Abfrage angefasst
|
||
werden muss. Ein zusaetzlicher Haken haette bedeutet, JEDE Abfrage
|
||
um "AND nicht abgebrochen" zu ergaenzen, und die eine vergessene
|
||
waere ein stiller Fehler gewesen: Die Aufgabe stuende weiter im
|
||
Brett, und niemand wuesste warum. */
|
||
/* ---- BIS ZUM 21.09.2026 STAND HIER EIN NEUBAU VON HAND ----
|
||
|
||
Er zaehlte die Spalten einzeln auf -- einmal im CREATE, einmal im
|
||
INSERT ... SELECT. Achtzehn Stueck. Die Tabelle hatte zu dem
|
||
Zeitpunkt DREIUNDZWANZIG: `vorlage`, `kategorie`, `aus_eintrag_id`,
|
||
`aufwand` und `aus_punkt` kamen spaeter dazu und wurden nie
|
||
nachgetragen.
|
||
|
||
WAS DAS BEDEUTET HAETTE: Wer die Umstellung noch vor sich hat --
|
||
eine zurueckgespielte Sicherung, eine frische Anlage aus einem
|
||
alten Stand -- verliert diese fuenf Spalten MIT INHALT. Ohne
|
||
Fehlermeldung, bei unveraenderter Zeilenzahl. Die Zaehlung, die
|
||
als Sicherung gedacht war, kann einen Spaltenverlust gar nicht
|
||
sehen: 40 Zeilen vorher, 40 Zeilen nachher, alles "in Ordnung".
|
||
|
||
GENAU DAS IST IN DIESEM HAUS SCHON EINMAL PASSIERT (11.09.2026,
|
||
Eintragstabelle: 24 Spalten hinein, 21 heraus). Damals wurde die
|
||
Lehre gezogen, die Liste abzuleiten statt zu pflegen -- und
|
||
`checkListeErweitern` unten tut genau das (`PRAGMA table_info`).
|
||
Dieser Block hier war aelter und wurde bei der Umstellung
|
||
uebersehen; eine Liste, die niemand pflegt, kann nicht veralten,
|
||
aber eine zweite Fassung daneben eben doch.
|
||
|
||
Gefunden hat es nicht das Lesen, sondern pruef-abbrechen: Sie baut
|
||
absichtlich eine ALTE Datenbank und laesst die Umstellung darauf
|
||
laufen. Danach meldete der Server
|
||
"Aufgaben lesen: no such column: a.aufwand" -- und die Pruefung
|
||
bekam eine 503 statt einer Liste.
|
||
|
||
Auf dem echten Server ist die Umstellung laengst gelaufen (24
|
||
Spalten, CHECK mit 'abgebrochen'); dort ist nichts verloren
|
||
gegangen. Der Aufruf unten ist deshalb im Betrieb ein Nulldurchgang
|
||
-- er steht hier fuer den Tag, an dem jemand eine Sicherung
|
||
zurueckspielt. */
|
||
checkListeErweitern(d, "aufgaben", "status", "abgebrochen",
|
||
["offen", "arbeit", "review", "erledigt", "abgebrochen"], jetztStempel);
|
||
|
||
/* DER ZUSTAND „beworben" (22.09.2026).
|
||
|
||
Eine Bewerbung ist keine neue Tabelle, sondern ein weiterer
|
||
Zustand derselben Zeile. Das ist nicht Sparsamkeit, sondern die
|
||
richtige Form: Die Frage „wie steht diese Aufgabe bei DIESEM
|
||
Menschen" wird schon hier beantwortet, und eine Bewerbung ist
|
||
genau eine Antwort darauf. Eine zweite Tabelle daneben haette
|
||
zwei Wahrheiten ueber dieselbe Beziehung -- und jede Liste,
|
||
jede Ansicht und jede Rueckmeldung muesste beide lesen.
|
||
|
||
Der Ablauf: beworben -> angenommen (die rechte Hand oder
|
||
DogFather sagt ja) oder -> abgelehnt (mit Kommentar).
|
||
|
||
`checkListeErweitern` baut die Tabelle dafuer neu -- der einzige
|
||
Weg, eine CHECK-Regel in SQLite zu aendern. Die Spaltenliste
|
||
kommt aus PRAGMA, die Indizes gehen mit, und vorher wird
|
||
gesichert. Steht seit dem 09.09. an einer Stelle, genau damit
|
||
diese Umstellung hier keine vierte Abschrift wird. */
|
||
checkListeErweitern(d, "aufgaben_zuteilung", "zustand", "beworben",
|
||
["offen", "angenommen", "arbeit", "erledigt", "abgelehnt", "beworben"],
|
||
jetztStempel);
|
||
|
||
/* DER MELDER HAT DAS LETZTE WORT (25.09.2026).
|
||
|
||
Filipe: „erst wenn es funktioniert gedrueckt wird, will ich dass
|
||
alles richtig fertig ist."
|
||
|
||
„wartet" ist der Stand zwischen „die Leitung hat geantwortet" und
|
||
„der Melder hat bestaetigt". Auf bestehenden Datenbanken laesst
|
||
die CHECK-Regel ihn noch nicht zu -- eine Meldung in diesen Stand
|
||
zu bringen wuerde dort abgewiesen, und zwar mit einem
|
||
Datenbankfehler statt einer Meldung. */
|
||
checkListeErweitern(d, "support_meldungen", "stand", "wartet",
|
||
["neu", "in_arbeit", "wartet", "erledigt"], jetztStempel);
|
||
|
||
/* WER ENTSCHIEDEN HAT, UND WAS ER DAZU GESAGT HAT.
|
||
`grund` traegt weiterhin die Worte der Person selbst (beim
|
||
Ablehnen ihre Begruendung, beim Bewerben ihr Anliegen). Der
|
||
Kommentar der Leitung ist etwas anderes und gehoert nicht in
|
||
dasselbe Feld -- sonst ueberschreibt die Antwort die Frage. */
|
||
for (const [spalte, form] of [
|
||
["entscheid_text", "TEXT"],
|
||
["entschieden_von", "INTEGER REFERENCES personen(id) ON DELETE SET NULL"],
|
||
["entschieden_am", "TEXT"],
|
||
]) {
|
||
const da = d.prepare("PRAGMA table_info(aufgaben_zuteilung)").all()
|
||
.map((z) => z.name);
|
||
if (da.includes(spalte)) continue;
|
||
try {
|
||
d.exec(`ALTER TABLE aufgaben_zuteilung ADD COLUMN ${spalte} ${form}`);
|
||
console.log(`[workspace] Spalte '${spalte}' an der Zuteilung ergaenzt.`);
|
||
} catch (f) {
|
||
console.error(`[workspace] Spalte '${spalte}':`, f?.message);
|
||
}
|
||
}
|
||
|
||
/* ---- Der sechste Bereich: "agentur" (06.09.2026) ----
|
||
|
||
Kernprinzip 04 des Konzepts lautet woertlich "Die Agentur bleibt
|
||
angebunden". Fuenf der sechs Bereiche standen (LIVE, Content,
|
||
Technik, Community, Schutz) -- der sechste fehlte vollstaendig. Was
|
||
dadurch nirgends stand: welche Kampagne laeuft und bis wann, welche
|
||
Schulung ansteht, und vor allem der OFFIZIELLE WEG zur Agentur mit
|
||
Stand und Antwort. Das lief bisher ueber private Nachrichten und
|
||
war damit weder auffindbar noch nachvollziehbar.
|
||
|
||
Und die Trennung, die das Konzept ausdruecklich verlangt und die im
|
||
Alltag sonst verschwimmt: Betreuung und Umsetzung sind UNSERE
|
||
Seite, offizielle Wege und Plattformthemen die der Agentur. Wer
|
||
wofuer zustaendig ist, gehoert auf den Bildschirm, nicht ins
|
||
Gedaechtnis.
|
||
|
||
Derselbe Umbau wie bei "abgebrochen" darueber -- ein CHECK laesst
|
||
sich in SQLite nicht aendern, die Tabelle muss neu gebaut werden:
|
||
erst sichern, dann tauschen, Zeilen zaehlen, Verweise pruefen. */
|
||
/* ---- Der sechste Bereich: "agentur" (06.09.2026) ----
|
||
ABGELEITET STATT ABGESCHRIEBEN (11.09.2026).
|
||
|
||
Hier stand bis heute der Bauplan der Tabelle von Hand abgeschrieben
|
||
-- alle Spalten, zweimal (einmal fuer CREATE, einmal fuer INSERT).
|
||
Am 07.09.2026 fiel auf, dass dabei drei Event-Spalten fehlten; sie
|
||
wurden nachgetragen, und daneben kam ein Kommentar, der genau vor
|
||
dieser Verlustart warnt.
|
||
|
||
DIESELBE FALLE HAT DANACH EIN ZWEITES MAL ZUGESCHNAPPT. Seit dem
|
||
07.09. kamen `einsatz`, `nur_leitung` und `gesendet_am` dazu. Der
|
||
Spalten-Nachtrag legt sie an, dieser Neubau warf sie unmittelbar
|
||
danach weg -- mit Inhalt, ohne Fehlermeldung, bei unveraenderter
|
||
Zeilenzahl. Gemessen: 24 Spalten hinein, 21 heraus.
|
||
|
||
Gefunden hat es pruef-agentur, und zwar seit Tagen: 31-mal "FEHL"
|
||
mit HTTP 503. Sagen konnte sie es erst, seit sie die
|
||
Serverausgabe zeigt -- dort stand es dann in drei Zeilen
|
||
untereinander.
|
||
|
||
DESHALB WIRD NICHTS MEHR ABGESCHRIEBEN. `checkListeErweitern` holt
|
||
den Bauplan aus sqlite_master und die Spaltenliste aus
|
||
PRAGMA table_info -- sie kann gar nicht veralten, weil sie nicht
|
||
gepflegt wird. Sie sichert vorher, zaehlt die Zeilen innerhalb der
|
||
Transaktion, nimmt die Indizes mit und prueft die Verweise, also
|
||
alles, was der Block hier auch tat.
|
||
|
||
Die echten Daten auf dem Server sind nicht betroffen: Ihr CHECK
|
||
kennt 'agentur' laengst, die Umstellung laeuft dort nie wieder.
|
||
Gefaehrlich war es beim Zurueckspielen einer Sicherung von vor dem
|
||
06.09.2026 -- also genau dann, wenn man sich am wenigsten einen
|
||
stillen Datenverlust leisten kann. */
|
||
checkListeErweitern(d, "eintraege", "bereich", "agentur",
|
||
["live", "content", "technik", "community", "schutz", "agentur"], jetztStempel);
|
||
|
||
/* ---- Rolle "manager" erlauben ---- */
|
||
/* ---- AUCH HIER STAND EIN NEUBAU VON HAND (ersetzt 21.09.2026) ----
|
||
|
||
Er zaehlte NEUN Spalten auf. `personen` hat neunzehn. Verloren
|
||
gegangen waeren:
|
||
|
||
bild, ueber_mich, tiktok, instagram, youtube, twitch,
|
||
chat_kachel, code_kennung, stufe, alter_bestaetigt_am
|
||
|
||
Das ist die empfindlichste Tabelle des Hauses, und zwei Eintraege
|
||
darin sind mehr als Komfort: `bild` sind die Profilfotos, und
|
||
`alter_bestaetigt_am` ist die Altersbestaetigung. Waere sie beim
|
||
Zurueckspielen einer Sicherung verschwunden, haette niemand eine
|
||
Fehlermeldung gesehen -- alle waeren still wieder unbestaetigt
|
||
gewesen, auf einer Seite, die genau das nicht sein darf.
|
||
|
||
Derselbe Befund wie beim Block darueber, und dieselbe Loesung:
|
||
`checkListeErweitern` leitet die Spaltenliste aus
|
||
`PRAGMA table_info` ab. Eine Liste, die niemand pflegt, kann nicht
|
||
veralten.
|
||
|
||
DIE KETTE IST KUMULATIV -- jeder Schritt nennt alle bis dahin
|
||
erlaubten Werte, und dieser ist der erste. Deshalb steht er VOR
|
||
den Aufrufen fuer spicy/modi/hand/gast weiter unten; die Reihenfolge
|
||
ist Teil der Bedeutung, nicht Zufall. */
|
||
checkListeErweitern(d, "personen", "rolle", "manager",
|
||
["admin", "manager", "scout", "creator"], jetztStempel);
|
||
|
||
/* =====================================================================
|
||
ROLLE "spicy" (Spicy Media) FREISCHALTEN — 07.09.2026
|
||
|
||
Wunsch Filipe: "ich will dass ueber dogfather auch eine kategorie,
|
||
eine neue rolle entsteht die den namen traegt, spicy media."
|
||
|
||
---------------------------------------------------------------------
|
||
DAS IST DER RISKANTESTE UMBAU IM GANZEN HAUS
|
||
|
||
Eine Rolle ist ein erlaubter Wert in einer Spalte, und der steckt in
|
||
SQLite in einem CHECK. Ein CHECK laesst sich nicht aendern -- die
|
||
Tabelle muss neu gebaut werden. Bei `eintraege` war das schon
|
||
unangenehm; hier geht es um `personen`, und auf die zeigen
|
||
ZWEIUNDFUENFZIG Fremdschluessel aus einem Dutzend Tabellen. Jede
|
||
Sitzung, jede Aufgabe, jeder Termin, jede Datei haengt daran.
|
||
|
||
DESHALB WIRD DER BAUPLAN NICHT ABGESCHRIEBEN, SONDERN GELESEN.
|
||
|
||
Die beiden Umstellungen darueber schreiben ihre Spaltenliste von
|
||
Hand ab. Das ging gut, solange die Liste stimmte -- aber `personen`
|
||
hat seit damals SIEBEN Spalten dazubekommen (bild, ueber_mich,
|
||
tiktok, instagram, youtube, twitch und weitere, siehe den
|
||
Spalten-Nachtrag oben). Wer hier eine Liste abschreibt, verliert
|
||
beim naechsten Mal genau die Spalte, die jemand letzte Woche
|
||
ergaenzt hat -- samt allen Profilbildern.
|
||
|
||
Also: Der vorhandene CREATE-Text wird aus sqlite_master geholt und
|
||
darin AUSSCHLIESSLICH die Rollenliste ersetzt. Alles andere -- jede
|
||
Spalte, jeder Typ, jede Vorgabe -- bleibt woertlich stehen. Und die
|
||
Kopierliste kommt aus PRAGMA table_info, also aus der Tabelle
|
||
selbst.
|
||
|
||
WENN DIE ERSETZUNG NICHT GREIFT, WIRD NICHT UMGESTELLT. Ein
|
||
`replace`, das nichts findet, gibt den Text unveraendert zurueck --
|
||
die Umstellung liefe dann durch, baute dieselbe Tabelle noch einmal
|
||
und meldete Erfolg. Genau die Sorte gruener Haken, die nichts
|
||
geprueft hat. Deshalb wird danach nachgesehen, ob 'spicy' wirklich
|
||
drinsteht.
|
||
===================================================================== */
|
||
/* =====================================================================
|
||
MEHR TERMINARTEN (08.09.2026)
|
||
|
||
Wunsch Filipe: "da sollen auch noch kategorien wie bigmatch,
|
||
turniere, special-live ... informier dich, was man da alles noch
|
||
gebrauchen koennte."
|
||
|
||
Recherchiert (Streams Charts, Kick, StreamerCollabs): Was ein
|
||
Creator-Team im Kalender wirklich unterscheidet, sind neben den
|
||
internen Terminen die AUFTRITTE -- Wettkaempfe (BigMatch, Turnier),
|
||
gemeinsame Formate (Collab, Raid-Train) und die grossen Ausnahmen
|
||
(Special-Live, Charity). Genau diese sechs kommen dazu.
|
||
|
||
WARUM EIN TABELLENUMBAU: Die erlaubten Werte stecken in einem
|
||
CHECK, und ein CHECK laesst sich in SQLite nicht aendern. Dieselbe
|
||
Lage wie bei der Rolle 'spicy' -- und deshalb hier dasselbe,
|
||
bewaehrte Verfahren: Bauplan aus sqlite_master lesen, NUR die Liste
|
||
ersetzen, das Ergebnis pruefen, Spalten aus PRAGMA holen, Zeilen
|
||
INNERHALB der Transaktion zaehlen, Sicherung vorher.
|
||
|
||
Zwei Tabellen statt einer (termine und termin_serien), deshalb
|
||
eine Schleife -- zweimal derselbe Text waere zweimal dieselbe
|
||
Gelegenheit, eine Stelle zu vergessen. */
|
||
const ARTEN_NEU = "'termin','call','review','bigmatch','turnier','special','collab','raid','charity'";
|
||
for (const tabelle of ["termine", "termin_serien"]) {
|
||
const plan = d.prepare(
|
||
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get(tabelle)?.sql || "";
|
||
/* Geprueft wird die REGEL, nicht der ganze Bauplan: SQLite hebt ihn
|
||
woertlich auf, samt Kommentaren -- ein erklaerender Satz mit dem
|
||
Wort 'bigmatch' wuerde sonst genuegen, damit die Umstellung sich
|
||
fuer erledigt haelt. Genau dieser Fehler ist bei 'spicy' passiert. */
|
||
const artRegel = plan.match(/art\s+IN\s*\(([^)]*)\)/i)?.[1] || "";
|
||
if (!plan || !artRegel || artRegel.includes("'bigmatch'")) continue;
|
||
|
||
/* DER TABELLENNAME GEHOERT IN DEN DATEINAMEN (08.09.2026).
|
||
|
||
Hier stand `.vor-arten-${jetztStempel}` -- ohne die Tabelle. Der
|
||
Stempel wird EINMAL pro Serverstart gebildet, diese Schleife
|
||
laeuft aber ZWEIMAL. Der zweite Durchlauf wollte also dieselbe
|
||
Datei anlegen, `VACUUM INTO` weigert sich (die Datei ist schon
|
||
da), und das `break` unten beendete daraufhin die ganze Schleife.
|
||
|
||
Ergebnis: `termine` war umgestellt, `termin_serien` NICHT. Und
|
||
zwar still -- die Meldung ging in die Serverausgabe, die niemand
|
||
liest. Aufgefallen waere es erst, wenn jemand eine wiederkehrende
|
||
BigMatch-Reihe anlegt und die Datenbank sie ohne erkennbaren
|
||
Grund ablehnt.
|
||
|
||
Das `break` bleibt richtig: Wenn sich keine Sicherung anlegen
|
||
laesst, wird nicht umgebaut. Falsch war nur der Name. */
|
||
const sicherung = `${DB_PFAD}.vor-arten-${tabelle}-${jetztStempel}`;
|
||
try {
|
||
d.exec(`VACUUM INTO '${sicherung.replace(/'/g, "''")}'`);
|
||
console.log("[workspace] Sicherung vor der Artenumstellung:", sicherung);
|
||
} catch (fehler) {
|
||
console.error("[workspace] Sicherung fehlgeschlagen, Artenumstellung abgebrochen:",
|
||
fehler?.message);
|
||
break;
|
||
}
|
||
|
||
const neuerPlan = plan
|
||
/* DAS MUSTER WIRD ZUSAMMENGESETZT, NICHT IN EINEN TEMPLATE-STRING
|
||
GESCHRIEBEN. Dort wird \s beim Einlesen zu einem blossen "s" --
|
||
aus "CREATE TABLE\s+" wuerde "CREATE TABLEs+", das Muster
|
||
passte auf nichts, und die Umstellung waere STILL ausgeblieben.
|
||
Genau so stand es hier beim ersten Anlauf; nachgemessen mit
|
||
einem Einzeiler, der beide Schreibweisen gegen den echten
|
||
Bauplan haelt. */
|
||
.replace(new RegExp(
|
||
"CREATE TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?[\"'`]?" + tabelle + "[\"'`]?", "i"),
|
||
`CREATE TABLE ${tabelle}_neu`)
|
||
.replace(/art\s+IN\s*\([^)]*\)/i, `art IN (${ARTEN_NEU})`);
|
||
if (!neuerPlan.includes("'bigmatch'") || !neuerPlan.includes(`${tabelle}_neu`)) {
|
||
console.error(`[workspace] Artenumstellung abgebrochen: Bauplan von ${tabelle} `
|
||
+ "liess sich nicht umschreiben.");
|
||
continue;
|
||
}
|
||
|
||
const spalten = d.prepare(`PRAGMA table_info(${tabelle})`).all().map((z) => z.name);
|
||
if (!spalten.length) continue;
|
||
const liste = spalten.map((n) => `"${n}"`).join(", ");
|
||
|
||
d.exec("PRAGMA foreign_keys = OFF");
|
||
try {
|
||
const vorher = d.prepare(`SELECT COUNT(*) AS n FROM ${tabelle}`).get().n;
|
||
d.exec("BEGIN");
|
||
d.exec(neuerPlan);
|
||
d.exec(`INSERT INTO ${tabelle}_neu (${liste}) SELECT ${liste} FROM ${tabelle};`);
|
||
const nachher = d.prepare(`SELECT COUNT(*) AS n FROM ${tabelle}_neu`).get().n;
|
||
if (nachher !== vorher) {
|
||
d.exec("ROLLBACK");
|
||
console.error(`[workspace] Artenumstellung abgebrochen: ${vorher} Zeilen vorher, `
|
||
+ `${nachher} nachher. Sicherung: ${sicherung}`);
|
||
} else {
|
||
d.exec(`DROP TABLE ${tabelle};`);
|
||
d.exec(`ALTER TABLE ${tabelle}_neu RENAME TO ${tabelle};`);
|
||
d.exec("COMMIT");
|
||
const kaputt = d.prepare("PRAGMA foreign_key_check").all();
|
||
if (kaputt.length) {
|
||
console.error("[workspace] ACHTUNG: nach der Artenumstellung", kaputt.length,
|
||
"verwaiste Verweise. Sicherung:", sicherung);
|
||
} else {
|
||
console.log(`[workspace] Terminarten erweitert in ${tabelle}: ${nachher} Zeilen, `
|
||
+ `${spalten.length} Spalten uebernommen, Verweise geprueft.`);
|
||
}
|
||
}
|
||
} catch (fehler) {
|
||
try { d.exec("ROLLBACK"); } catch { /* schon zurueckgerollt */ }
|
||
console.error("[workspace] Artenumstellung fehlgeschlagen:", fehler?.message,
|
||
"-- Sicherung:", sicherung);
|
||
} finally {
|
||
d.exec("PRAGMA foreign_keys = ON");
|
||
}
|
||
}
|
||
|
||
/* Welche Rollen die Datenbank zulaesst. Der Ablauf steht in
|
||
rollenRegelUmstellen() -- einmal, nicht je Rolle abgeschrieben.
|
||
Beide Aufrufe stehen hier: Auf einer Datenbank, die 'spicy' schon
|
||
kennt, tut der erste nichts und nur der zweite laeuft. */
|
||
checkListeErweitern(d, "personen", "rolle", "spicy",
|
||
["spicy", "admin", "manager", "scout", "creator"], jetztStempel);
|
||
checkListeErweitern(d, "personen", "rolle", "modi",
|
||
["spicy", "admin", "manager", "scout", "creator", "modi"], jetztStempel);
|
||
checkListeErweitern(d, "personen", "rolle", "hand",
|
||
["spicy", "admin", "manager", "scout", "creator", "modi", "hand"], jetztStempel);
|
||
/* DIE COMMUNITY (11.09.2026). Dieselbe Umstellung wie die drei
|
||
darueber: Sicherung vorher, Neubau, Zaehlung innerhalb der
|
||
Transaktion, Indizes zurueck. */
|
||
checkListeErweitern(d, "personen", "rolle", "gast",
|
||
["spicy", "admin", "manager", "scout", "creator", "modi", "hand", "gast"], jetztStempel);
|
||
/* DIE LINKE HAND (21.09.2026) -- UND SIE STEHT MIT ABSICHT GANZ
|
||
UNTEN, NICHT BEI DER RECHTEN HAND.
|
||
|
||
`checkListeErweitern` baut die Regel NEU aus der Liste, die man
|
||
ihm gibt. Stand mein Schritt weiter oben, nahm der Schritt fuer
|
||
"gast" darunter die Rolle wieder heraus -- seine Liste kennt sie
|
||
ja nicht. Die Kette ist kumulativ und deshalb von der Reihenfolge
|
||
abhaengig; wer hier etwas einschiebt, gehoert ans ENDE.
|
||
|
||
Ohne diesen Schritt laesst die Datenbank die Rolle nicht zu -- und
|
||
zwar erst beim ersten echten Anlegen, nicht beim Start. */
|
||
checkListeErweitern(d, "personen", "rolle", "linke",
|
||
["spicy", "admin", "manager", "scout", "creator", "modi", "hand",
|
||
"gast", "linke"], jetztStempel);
|
||
|
||
/* DER BEREICH FUER DAS IDEEN-BOARD (10.09.2026, Kapitel 5.6).
|
||
|
||
Dieselbe Umstellung, andere Tabelle -- und genau deshalb steht sie
|
||
ab jetzt EINMAL da. Der alte agentur-Block weiter unten schreibt
|
||
den ganzen Bauplan von Hand ab; im Kommentar dort steht, dass dabei
|
||
schon einmal drei Spalten vergessen wurden. Eine vierte Abschrift
|
||
waere die vierte Gelegenheit dazu. */
|
||
checkListeErweitern(d, "eintraege", "bereich", "ideen",
|
||
["live", "content", "technik", "community", "schutz", "agentur", "ideen"], jetztStempel);
|
||
/* Die Angebote (10.09.2026) -- Vorschlaege der Modis, ueber die
|
||
DogFather und VanVan entscheiden. */
|
||
checkListeErweitern(d, "eintraege", "bereich", "angebote",
|
||
["live", "content", "technik", "community", "schutz", "agentur", "ideen", "angebote"],
|
||
jetztStempel);
|
||
/* ZWEI NEUE ZUSTAENDE. Bisher kannte ein Eintrag nur offen/erledigt.
|
||
Ein Angebot hat aber eine ENTSCHEIDUNG: angenommen oder abgelehnt
|
||
-- und das ist etwas anderes als "erledigt". Wer beides in einen
|
||
Topf wirft, kann hinterher nicht mehr sagen, was aus einem
|
||
Vorschlag geworden ist. */
|
||
/* DER BEREICH FUER DIE RUECKMELDUNG (10.09.2026, Kapitel 5 und 6). */
|
||
checkListeErweitern(d, "eintraege", "bereich", "rueckmeldung",
|
||
["live", "content", "technik", "community", "schutz", "agentur", "ideen",
|
||
"angebote", "rueckmeldung"], jetztStempel);
|
||
|
||
/* ENTWICKLUNG UND TALENTE (11.09.2026, Wunsch Filipe). */
|
||
checkListeErweitern(d, "eintraege", "bereich", "entwicklung",
|
||
["live", "content", "technik", "community", "schutz", "agentur", "ideen",
|
||
"angebote", "rueckmeldung", "entwicklung"], jetztStempel);
|
||
checkListeErweitern(d, "eintraege", "bereich", "talente",
|
||
["live", "content", "technik", "community", "schutz", "agentur", "ideen",
|
||
"angebote", "rueckmeldung", "entwicklung", "talente"], jetztStempel);
|
||
/* DIE SIEBEN BRETTER DES TREFFS (11.09.2026).
|
||
|
||
EINE Umstellung fuer alle sieben, nicht sieben einzelne. Jeder
|
||
Aufruf baut die Tabelle neu auf -- siebenmal hintereinander waeren
|
||
sieben Sicherungen, sieben Neubauten und sieben Gelegenheiten, dass
|
||
etwas schiefgeht. Der Marker ist "treff"; ist der schon in der
|
||
CHECK-Liste, passiert gar nichts. */
|
||
checkListeErweitern(d, "eintraege", "bereich", "treff",
|
||
["live", "content", "technik", "community", "schutz", "agentur", "ideen",
|
||
"angebote", "rueckmeldung", "entwicklung", "talente",
|
||
"anschlag", "ansteht", "treff", "wunsch", "highlight", "regeln", "mitmachen"],
|
||
jetztStempel);
|
||
|
||
checkListeErweitern(d, "eintraege", "status", "angenommen",
|
||
["offen", "erledigt", "angenommen", "abgelehnt"], jetztStempel);
|
||
|
||
/* DIE DRITTE RAUMART (10.09.2026, Kapitel 7.2). Auf einer frischen
|
||
Anlage steht 'kanal' schon im Bauplan -- dann findet der Aufruf die
|
||
Regel bereits erweitert und tut nichts. */
|
||
checkListeErweitern(d, "chat_raeume", "art", "kanal",
|
||
["direkt", "gruppe", "kanal"], jetztStempel);
|
||
|
||
/* EIN KANAL JE ZUSTAENDIGKEIT, und die Datenbank haelt das fest.
|
||
|
||
Die Regel steht auch in der Route -- aber eine Regel, die nur in
|
||
einer Route steht, gilt genau so lange, bis jemand eine zweite
|
||
Route schreibt. Zwei Kanaele "Clipping" waeren zwei halbe
|
||
Verlaeufe, und man merkt es erst, wenn jemand die Antwort im
|
||
falschen sucht.
|
||
|
||
TEILINDEX (`WHERE art = 'kanal'`): Gruppen und Zweier-Gespraeche
|
||
haben gar keine Kategorie; ohne die Bedingung waere schon die
|
||
zweite Gruppe mit NULL ein Verstoss -- nein, NULL zaehlt in SQLite
|
||
nicht als Dublette, aber der Index waere trotzdem groesser als
|
||
noetig und wuerde etwas versprechen, was er nicht meint.
|
||
|
||
ER STEHT HIER UNTEN und nicht im Bauplan: Die Spalte `kategorie`
|
||
entsteht auf einer bestehenden Anlage erst durch die Schleife
|
||
weiter oben -- im Bauplan gaebe es sie beim Start noch nicht. */
|
||
try {
|
||
d.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_kanal_kategorie
|
||
ON chat_raeume (kategorie) WHERE art = 'kanal'`);
|
||
} catch (fehler) {
|
||
console.error("[workspace] Kanal-Index:", fehler?.message);
|
||
}
|
||
}
|
||
|
||
export function db() {
|
||
if (_db) return _db;
|
||
if (_dbFehler) throw _dbFehler;
|
||
try {
|
||
const { DatabaseSync } = require("node:sqlite");
|
||
mkdirSync(dirname(DB_PFAD), { recursive: true });
|
||
const d = new DatabaseSync(DB_PFAD);
|
||
d.exec(`
|
||
PRAGMA journal_mode = WAL;
|
||
PRAGMA foreign_keys = ON;
|
||
|
||
/* ROLLENLISTE: 'spicy' steht hier mit drin, nicht nur in der
|
||
Umstellung (07.09.2026, gefunden von pruef-spicy). Vorher legte
|
||
eine frische Datenbank die alte Liste an, und die Umstellung
|
||
lief beim allerersten Start sofort hinterher -- Tabelle neu
|
||
bauen, Sicherung schreiben, umbenennen, fuer nichts. Ein
|
||
Bauplan, der sofort umgebaut werden muss, ist der falsche.
|
||
|
||
KEIN KOMMENTAR INNERHALB DIESES CREATE-TEXTES. SQLite hebt ihn
|
||
woertlich in sqlite_master auf -- samt Kommentaren. Ein Satz mit
|
||
dem Wort 'spicy' darin haette bedeutet, dass die Umstellung die
|
||
Tabelle fuer bereits umgestellt haelt, obwohl die CHECK-Regel
|
||
noch die alte ist. Genau das ist beim Bauen passiert: Der
|
||
Bauplan enthielt das Wort, die Regel nicht. */
|
||
CREATE TABLE IF NOT EXISTS personen (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name TEXT NOT NULL,
|
||
rolle TEXT NOT NULL CHECK (rolle IN ('spicy','admin','manager','scout','creator','modi')),
|
||
code_hash TEXT NOT NULL,
|
||
code_salt TEXT NOT NULL,
|
||
code_n INTEGER NOT NULL,
|
||
aktiv INTEGER NOT NULL DEFAULT 1,
|
||
erstellt TEXT NOT NULL,
|
||
letzter_login TEXT
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS sitzungen (
|
||
token_hash TEXT PRIMARY KEY,
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
erstellt TEXT NOT NULL,
|
||
gueltig_bis TEXT NOT NULL,
|
||
ip TEXT,
|
||
browser TEXT
|
||
);
|
||
|
||
/* Audit-Log. Im Konzept ausdrücklich gefordert ("Login-Historie,
|
||
kritische Aktionen protokollieren"). Nur anhängen, nie ändern. */
|
||
CREATE TABLE IF NOT EXISTS protokoll (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
zeitpunkt TEXT NOT NULL,
|
||
person_id INTEGER,
|
||
rolle TEXT,
|
||
aktion TEXT NOT NULL,
|
||
detail TEXT,
|
||
ip TEXT
|
||
);
|
||
|
||
/* Aufgaben. creator_id sagt, ZU WEM die Aufgabe gehört (wessen
|
||
Bereich), verantwortlich_id, WER sie erledigt. Beides getrennt,
|
||
weil im Konzept auch Aufgaben vorkommen, die das Management für
|
||
einen Creator anlegt. */
|
||
CREATE TABLE IF NOT EXISTS aufgaben (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
titel TEXT NOT NULL,
|
||
beschreibung TEXT,
|
||
status TEXT NOT NULL DEFAULT 'offen'
|
||
CHECK (status IN ('offen','arbeit','review','erledigt','abgebrochen')),
|
||
prioritaet TEXT NOT NULL DEFAULT 'mittel'
|
||
CHECK (prioritaet IN ('hoch','mittel','niedrig')),
|
||
creator_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
verantwortlich_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
frist TEXT,
|
||
erstellt TEXT NOT NULL,
|
||
erstellt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
geaendert TEXT,
|
||
erledigt_am TEXT
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_aufgaben_status ON aufgaben (status);
|
||
CREATE INDEX IF NOT EXISTS idx_aufgaben_creator ON aufgaben (creator_id);
|
||
|
||
/* Einstellungen, die sich zur Laufzeit aendern lassen. Bisher nur
|
||
eine: ob die KI-Vorschlaege angeboten werden. Als Tabelle statt
|
||
als Datei, damit sie dieselbe Sicherung bekommt wie alles
|
||
andere -- eine Einstellung, die beim Wiederherstellen fehlt,
|
||
faellt erst auf, wenn etwas nicht mehr geht. */
|
||
CREATE TABLE IF NOT EXISTS einstellungen (
|
||
schluessel TEXT PRIMARY KEY,
|
||
wert TEXT NOT NULL,
|
||
geaendert TEXT,
|
||
von INTEGER REFERENCES personen(id) ON DELETE SET NULL
|
||
);
|
||
|
||
/* BENACHRICHTIGUNGEN (05.09.2026) ---------------------------------
|
||
|
||
Wunsch Filipe, mit dem Bild eines "Benachrichtigungen aus"-
|
||
Knopfes: *"ich will auch sowas und dass es perfekt funktioniert
|
||
auf der seite fuer jeden."*
|
||
|
||
Eine Anmeldung ist die Adresse, unter der ein bestimmter
|
||
BROWSER auf einem bestimmten GERAET erreichbar ist -- nicht die
|
||
Person. Wer den Workspace auf dem Rechner und auf dem Handy
|
||
benutzt, hat zwei; beide sollen klingeln, und beide muessen
|
||
einzeln abschaltbar sein.
|
||
|
||
Der Endpunkt ist der Schluessel, nicht eine eigene Nummer: Meldet
|
||
sich derselbe Browser erneut an (nach dem Leeren der
|
||
Website-Daten etwa), soll daraus KEIN zweiter Eintrag werden,
|
||
sonst kaeme jede Nachricht doppelt.
|
||
|
||
p256dh und auth sind die Schluessel des Browsers. Ohne sie
|
||
laesst sich nichts verschluesseln -- und ohne Verschluesselung
|
||
nimmt kein Push-Dienst etwas an. */
|
||
CREATE TABLE IF NOT EXISTS push_anmeldungen (
|
||
endpunkt TEXT PRIMARY KEY,
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
p256dh TEXT NOT NULL,
|
||
auth TEXT NOT NULL,
|
||
geraet TEXT,
|
||
erstellt TEXT NOT NULL,
|
||
zuletzt_ok TEXT,
|
||
fehler INTEGER NOT NULL DEFAULT 0
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_push_person ON push_anmeldungen (person_id);
|
||
|
||
/* Was jemand bekommen WILL -- je Art einzeln.
|
||
|
||
Der Plan ist an dieser Stelle deutlich: *"je Person abschaltbar
|
||
-- pro Art, nicht alles oder nichts. Eine Benachrichtigung, die
|
||
nervt, wird abgeschaltet und dann fehlt auch die wichtige."*
|
||
|
||
Fehlt eine Zeile, gilt die Voreinstellung aus workspace-push.js
|
||
(an). Damit muss niemand erst etwas einstellen, um etwas zu
|
||
bekommen -- und wer etwas abstellt, bekommt genau das nicht
|
||
mehr. */
|
||
CREATE TABLE IF NOT EXISTS push_einstellungen (
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
art TEXT NOT NULL,
|
||
an INTEGER NOT NULL DEFAULT 1,
|
||
PRIMARY KEY (person_id, art)
|
||
);
|
||
|
||
/* Was schon verschickt wurde. Ohne dieses Gedaechtnis bekaeme
|
||
jemand dieselbe Erinnerung bei jedem Lauf erneut -- viermal am
|
||
Tag "Aufgabe ist ueberfaellig" ist der schnellste Weg, dass
|
||
jemand Benachrichtigungen komplett abschaltet.
|
||
|
||
Das Merkmal ist die Sache selbst (z. B. "aufgabe-faellig:42"),
|
||
nicht der Zeitpunkt: Dieselbe Aufgabe erinnert einmal, nicht
|
||
einmal je Stunde. */
|
||
CREATE TABLE IF NOT EXISTS push_verschickt (
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
merkmal TEXT NOT NULL,
|
||
zeit TEXT NOT NULL,
|
||
PRIMARY KEY (person_id, merkmal)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_push_verschickt_zeit ON push_verschickt (zeit);
|
||
|
||
/* Rueckmeldungen zu einer Aufgabe (Konzept: "Aufgaben & Feedback").
|
||
Ohne sie endet jede Rueckfrage ausserhalb des Systems -- in
|
||
WhatsApp, und damit ausserhalb dessen, was spaeter noch
|
||
nachvollziehbar ist.
|
||
Wer die Aufgabe sehen darf, darf auch die Rueckmeldungen sehen
|
||
und schreiben. Eine eigene Sichtbarkeitsregel gibt es bewusst
|
||
NICHT: Zwei Regeln fuer dieselbe Sache laufen irgendwann
|
||
auseinander. */
|
||
CREATE TABLE IF NOT EXISTS aufgaben_notizen (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
aufgabe_id INTEGER NOT NULL REFERENCES aufgaben(id) ON DELETE CASCADE,
|
||
person_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
text TEXT NOT NULL,
|
||
erstellt TEXT NOT NULL
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_aufgaben_notizen ON aufgaben_notizen (aufgabe_id);
|
||
|
||
/* =================================================================
|
||
WER HAT DIE AUFGABE -- UND WIE STEHT ES BEI IHM? (21.09.2026)
|
||
|
||
Filipe, Abschnitt 4: *"Wenn eine Aufgabe mehreren Modis
|
||
zugewiesen wird, darf sie im Resuemee von Dogfather und der
|
||
rechten Hand nicht lediglich als eine einzige grosse
|
||
Sammelaufgabe erscheinen. Die Aufgabe muss in der Auswertung
|
||
auf die einzelnen zugewiesenen Personen aufgeteilt werden."*
|
||
|
||
GENAU DESHALB EINE EIGENE TABELLE und nicht mehrere Spalten an
|
||
der Aufgabe. Eine Aufgabe hat EINEN Titel und EINE Frist --
|
||
aber je Mensch einen eigenen Stand, einen eigenen Grund, wenn
|
||
er ablehnt, ein eigenes Erledigt-Datum und eine eigene
|
||
Bewertung. Das in Spalten zu pressen hiesse, dieselbe Aufgabe
|
||
mehrfach anzulegen; dann waere die uebergeordnete Struktur
|
||
weg, die der Auftrag ausdruecklich erhalten will.
|
||
|
||
"zustand" ist der Stand DIESES Menschen, nicht der Aufgabe:
|
||
|
||
offen er hat sie bekommen, aber noch nicht geantwortet
|
||
angenommen er hat ja gesagt
|
||
arbeit er ist dran
|
||
erledigt er ist fertig
|
||
abgelehnt er hat nein gesagt -- "grund" sagt, warum
|
||
|
||
"aufgaben.status" bleibt daneben unberuehrt. Zwei Ebenen, weil
|
||
es zwei Fragen sind: "Wie steht die Aufgabe?" und "Wie steht
|
||
SIE bei ihm?" Ein einziges Feld koennte bei drei Leuten nicht
|
||
gleichzeitig "angenommen" und "abgelehnt" sein.
|
||
|
||
WARUM HIER EIN CHECK STEHT, oben aber keiner: Diese Tabelle
|
||
entsteht neu. Ein CHECK auf einer neuen Tabelle kostet nichts;
|
||
einer auf einer nachgetragenen Spalte kostet einen Neubau.
|
||
|
||
KEINE BACKTICKS IN DIESEM KOMMENTAR: Er steht INNERHALB eines
|
||
Template-Literals (d.exec). Ein Backtick beendet es mitten im
|
||
Satz, und die Datei ist ab da syntaktisch kaputt -- an einer
|
||
Stelle, die nichts mit dem Fehler zu tun hat.
|
||
|
||
UNIQUE (aufgabe_id, person_id): Niemand bekommt dieselbe
|
||
Aufgabe zweimal. Ohne das entstuenden bei einem doppelten
|
||
Klick zwei Zeilen, und das Resuemee zaehlte eine Aufgabe
|
||
doppelt -- ein Fehler, den man erst an einer krummen Zahl
|
||
bemerkt und dann nicht mehr erklaeren kann.
|
||
|
||
ON DELETE CASCADE: Wird die Aufgabe geloescht, gehen die
|
||
Zuteilungen mit. Fremdschluessel sind in diesem Haus
|
||
eingeschaltet (am 20.09. gemessen, nicht angenommen). */
|
||
CREATE TABLE IF NOT EXISTS aufgaben_zuteilung (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
aufgabe_id INTEGER NOT NULL REFERENCES aufgaben(id) ON DELETE CASCADE,
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
zustand TEXT NOT NULL DEFAULT 'offen'
|
||
CHECK (zustand IN ('offen','angenommen','arbeit','erledigt','abgelehnt')),
|
||
grund TEXT,
|
||
zugeteilt_am TEXT NOT NULL,
|
||
zugeteilt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
geantwortet_am TEXT,
|
||
erledigt_am TEXT,
|
||
/* DIE RUECKMELDUNG (Abschnitt 5). Sie haengt am MENSCHEN und
|
||
nicht an der Aufgabe: Bei drei Leuten koennen drei
|
||
verschiedene Rueckmeldungen richtig sein. */
|
||
bewertung TEXT CHECK (bewertung IN ('gut','nicht_gut','verbessern')),
|
||
bewertung_text TEXT,
|
||
bewertet_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
bewertet_am TEXT,
|
||
UNIQUE (aufgabe_id, person_id)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_zuteilung_aufgabe ON aufgaben_zuteilung (aufgabe_id);
|
||
CREATE INDEX IF NOT EXISTS idx_zuteilung_person ON aufgaben_zuteilung (person_id, zustand);
|
||
|
||
/* =================================================================
|
||
CHAT (06.09.2026)
|
||
|
||
Wunsch Filipe: *"eine kategorie chat, wo die creator mit ihren
|
||
manager nachrichten austauschen können, wie erinnerungen, fragen
|
||
und noch vieles mehr, wo jeder immer mit denen schreiben kann
|
||
wie verbindung auch läuft."* Auf Nachfrage: Zweier-Gespraeche
|
||
UND Gruppen, und Manager/Scouts duerfen sich auch ohne
|
||
gemeinsamen Creator schreiben.
|
||
|
||
DREI TABELLEN, und die Aufteilung hat einen Grund:
|
||
|
||
chat_raeume das Gespraech selbst
|
||
chat_teilnehmer wer darin ist -- und bis wohin er gelesen hat
|
||
chat_nachrichten was gesagt wurde
|
||
|
||
WARUM DIE TEILNEHMER EINE EIGENE TABELLE SIND und nicht zwei
|
||
Spalten am Raum: Ein Zweier-Gespraech kaeme mit "person_a,
|
||
person_b" aus, eine Gruppe nicht. Zwei verschiedene Bauweisen
|
||
fuer dieselbe Sache waeren der sichere Weg dazu, dass eine
|
||
Abfrage die eine Haelfte vergisst. So ist ein Zweier-Gespraech
|
||
schlicht eine Gruppe mit zwei Leuten.
|
||
|
||
WARUM "gelesen_bis" AM TEILNEHMER haengt und nicht an der
|
||
Nachricht: Sonst braeuchte es je Person und Nachricht eine
|
||
Zeile -- bei drei Leuten und tausend Nachrichten dreitausend
|
||
Eintraege, nur um "gelesen" zu wissen. Eine Nummer je Person
|
||
und Raum genuegt: Alles darunter ist gelesen. */
|
||
CREATE TABLE IF NOT EXISTS chat_raeume (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
art TEXT NOT NULL DEFAULT 'direkt'
|
||
CHECK (art IN ('direkt','gruppe','kanal')),
|
||
/* Gruppen und Kanaele haben einen Namen. Ein Zweier-Gespraech
|
||
heisst immer nach dem Gegenueber -- und zwar fuer jeden
|
||
anders. Ein gespeicherter Name waere fuer eine der beiden
|
||
Seiten falsch.
|
||
Bei einem Kanal steht in kategorie, WORUM es geht; der Name
|
||
wird daraus gebildet. Zwei Namen fuer dieselbe Zustaendigkeit
|
||
sind der sichere Weg zu zwei Kanaelen mit je der Haelfte der
|
||
Nachrichten darin. */
|
||
name TEXT,
|
||
kategorie TEXT,
|
||
erstellt TEXT NOT NULL,
|
||
erstellt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
/* Der Zeitpunkt der letzten Nachricht, mitgefuehrt. Ohne ihn
|
||
braeuchte die Gespraechsliste je Raum eine Unterabfrage --
|
||
bei zwanzig Raeumen zwanzig Abfragen fuer eine Liste. */
|
||
letzte_am TEXT
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_chat_raeume_letzte ON chat_raeume (letzte_am DESC);
|
||
|
||
CREATE TABLE IF NOT EXISTS chat_teilnehmer (
|
||
raum_id INTEGER NOT NULL REFERENCES chat_raeume(id) ON DELETE CASCADE,
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
seit TEXT NOT NULL,
|
||
/* Bis zu welcher Nachricht diese Person gelesen hat. */
|
||
gelesen_bis INTEGER NOT NULL DEFAULT 0,
|
||
/* Wer eine Gruppe angelegt hat, darf Leute hinzufuegen und
|
||
entfernen. In Zweier-Gespraechen bedeutungslos. */
|
||
leitung INTEGER NOT NULL DEFAULT 0,
|
||
/* Verlassene Gruppen: Die Zeile bleibt mit einem Datum stehen,
|
||
damit der Verlauf bis dahin lesbar bleibt und niemand
|
||
rueckwirkend aus der Geschichte verschwindet. */
|
||
raus_am TEXT,
|
||
PRIMARY KEY (raum_id, person_id)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_chat_teilnehmer_person ON chat_teilnehmer (person_id);
|
||
|
||
CREATE TABLE IF NOT EXISTS chat_nachrichten (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
raum_id INTEGER NOT NULL REFERENCES chat_raeume(id) ON DELETE CASCADE,
|
||
person_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
text TEXT NOT NULL,
|
||
erstellt TEXT NOT NULL,
|
||
/* Zurueckgenommen: Der Text wird geleert, die Zeile bleibt.
|
||
Ein Loch im Verlauf wirft mehr Fragen auf als der Hinweis,
|
||
dass hier etwas zurueckgenommen wurde. */
|
||
weg_am TEXT
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_chat_nachrichten_raum
|
||
ON chat_nachrichten (raum_id, id);
|
||
|
||
/* ERWAEHNUNGEN -- "@Name" IM CHAT (20.09.2026).
|
||
|
||
Filipe: "ich will das man die leute mit @ markieren kann im
|
||
chat."
|
||
|
||
WARUM EINE EIGENE TABELLE UND NICHT NUR FARBIGER TEXT:
|
||
Markieren ist kein Schmuck, sondern eine Ansprache. Wer
|
||
gemeint ist, soll es MERKEN -- auch wenn er den Raum gerade
|
||
nicht offen hat und auch dann noch, wenn er ihn Stunden
|
||
spaeter aufmacht. Dafuer muss irgendwo stehen, wer gemeint
|
||
war.
|
||
|
||
WARUM NICHT AUS DEM TEXT NACHRECHNEN: Man koennte beim Lesen
|
||
jedes Mal "@" suchen. Dann muesste man aber bei jedem Aufbau
|
||
der Gespraechsliste den Text JEDER ungelesenen Nachricht
|
||
durchgehen -- bei einem lebhaften Raum sind das hunderte. Hier
|
||
steht das Ergebnis einmal und wird nie wieder gerechnet.
|
||
|
||
WER GEMEINT IST, ENTSCHEIDET DER SERVER -- nicht der Browser.
|
||
Der Browser schickt nur Text; wer daraus eine Person wird,
|
||
loest erwaehnungenFinden() gegen die TEILNEHMER DES RAUMS
|
||
auf. So kann niemand jemanden ansprechen, der gar nicht dabei
|
||
ist, und ein von Hand getipptes "@Anna" wirkt genauso wie
|
||
eines aus der Auswahlliste.
|
||
|
||
ON DELETE CASCADE an beiden Enden, wie bei den Reaktionen:
|
||
Verschwindet die Nachricht oder der Mensch, geht der Eintrag
|
||
mit. Eine Erwaehnung ohne Nachricht waere ein Hinweis, der
|
||
nirgendwohin fuehrt.
|
||
|
||
Der zusammengesetzte Primaerschluessel sagt "jede Person hoechstens
|
||
einmal je Nachricht": Wer dreimal "@Anna" in einen Satz
|
||
schreibt, spricht sie trotzdem nur einmal an. */
|
||
CREATE TABLE IF NOT EXISTS chat_erwaehnungen (
|
||
nachricht_id INTEGER NOT NULL REFERENCES chat_nachrichten(id) ON DELETE CASCADE,
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
PRIMARY KEY (nachricht_id, person_id)
|
||
);
|
||
/* Die Frage, die im Betrieb gestellt wird, lautet "bin ICH
|
||
irgendwo erwaehnt?" -- deshalb liegt person_id vorn. */
|
||
CREATE INDEX IF NOT EXISTS idx_chat_erwaehnungen_person
|
||
ON chat_erwaehnungen (person_id, nachricht_id);
|
||
|
||
/* DIE GIF-KISTE DES RUDELS (23.09.2026).
|
||
|
||
Filipe: "dan will ich auch dass man im chat auch
|
||
sprachnachrichten und gifts reinschicken kann. also nur die
|
||
modis rechte linke hand und dogfather."
|
||
|
||
WARUM EINE EIGENE KISTE UND KEIN ANSCHLUSS AN GIPHY ODER
|
||
TENOR: Beide brauchen ein Konto, einen Schluessel und schicken
|
||
bei jedem Tastendruck mit, wonach hier gesucht wird -- an eine
|
||
fremde Firma, aus einem Arbeitsplatz heraus, in dem sonst
|
||
nichts nach aussen geht. Dazu kosten sie ab einer Menge Geld.
|
||
Beides steht quer zu dem, wie dieses Haus gebaut ist.
|
||
|
||
Die Kiste kostet nichts, laeuft auf demselben Server wie alles
|
||
andere, und sie wird mit der Zeit besser statt schlechter: Was
|
||
das Team hineinlegt, hat das Team ausgesucht. Ein fremder
|
||
Katalog mit zehn Millionen Eintraegen ist nicht mehr Auswahl,
|
||
sondern weniger -- man sucht laenger und findet Fremdes.
|
||
|
||
WARUM EINE EIGENE TABELLE UND NICHT EINFACH ALTE NACHRICHTEN
|
||
DURCHSUCHEN: Ein GIF in der Kiste ist etwas anderes als ein
|
||
GIF, das jemand einmal geschickt hat. Die Kiste ist eine
|
||
Entscheidung ("das wollen wir wieder benutzen"), der Verlauf
|
||
ist eine Geschichte. Und ein Verlauf laesst sich wegraeumen --
|
||
die Kiste duerfte davon nicht leer werden.
|
||
|
||
"pruef" IST DER INHALTSSCHLUESSEL (SHA-256 der Datei). Er
|
||
beantwortet "haben wir das schon?" richtig, naemlich am
|
||
Inhalt: Dasselbe GIF unter drei Namen ist dreimal dieselbe
|
||
Kachel in der Auswahl. Ueber den Namen zu vergleichen waere
|
||
die naheliegende Abkuerzung -- und sie versagt genau dort, wo
|
||
Dateien "giphy.gif", "giphy(1).gif", "download.gif" heissen,
|
||
also fast immer.
|
||
|
||
KEIN ON DELETE CASCADE AUF personen: Wer das Haus verlaesst,
|
||
nimmt die GIFs des Teams nicht mit. "von_id" wird null, die
|
||
Kachel bleibt. */
|
||
/* BEWERBUNGEN AUF EINE VORLAGE (23.09.2026).
|
||
|
||
Filipe: "die modis sollen bei all diesen voschlaegen auch nur
|
||
bewerben koennen. die aufgaben aus der vorlage, da sollen die
|
||
modis sich nur bewerben koennen und nur dogfather und die
|
||
rechte hand sollen annehmen oder ablehnen koennen, mit einem
|
||
text als notiz."
|
||
|
||
WARUM NICHT DIE VORHANDENE BEWERBUNG AUS aufgaben_zuteilung:
|
||
Die beantwortet "wie steht DIESE Aufgabe bei DIESEM Menschen"
|
||
-- sie braucht also eine Aufgabe, die es gibt. Hier gibt es
|
||
noch keine. Es ist eine Bitte um etwas, das erst entstehen
|
||
soll.
|
||
|
||
Der naheliegende Weg waere gewesen, beim Bewerben gleich die
|
||
Aufgabe anzulegen und die vorhandene Bewerbung daranzuhaengen.
|
||
Dann steht nach zwoelf Absagen zwoelfmal Arbeit auf dem Brett,
|
||
die niemand bestellt hat -- und um das zu vermeiden, muesste
|
||
das Ablehnen Aufgaben wieder LOESCHEN. Loeschen als Nebenwirkung
|
||
einer Absage ist genau die Sorte Regel, die irgendwann das
|
||
Falsche trifft.
|
||
|
||
Also: Die Aufgabe entsteht erst mit der Zusage. Bis dahin gibt
|
||
es nur diese Zeile.
|
||
|
||
DIE WORTE SIND DIESELBEN WIE DRUEBEN (zustand, entscheid_text,
|
||
entschieden_von, entschieden_am). Zwei Namen fuer dieselbe
|
||
Sache waeren zwei Sprachen im selben Haus.
|
||
|
||
"aufgabe_id" steht nach der Zusage darin -- damit laesst sich
|
||
von der Bewerbung aus zeigen, was aus ihr geworden ist. Kein
|
||
Fremdschluessel: Wird die Aufgabe spaeter geloescht, bleibt die
|
||
Bewerbung als Vorgang stehen; sie hat stattgefunden.
|
||
|
||
ON DELETE CASCADE auf personen: Wer das Haus verlaesst,
|
||
hinterlaesst keine Bewerbung, auf die jemand noch antworten
|
||
soll. */
|
||
CREATE TABLE IF NOT EXISTS vorlagen_bewerbungen (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
vorlage TEXT NOT NULL,
|
||
kategorie TEXT,
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
text TEXT,
|
||
zustand TEXT NOT NULL DEFAULT 'beworben',
|
||
entscheid_text TEXT,
|
||
entschieden_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
entschieden_am TEXT,
|
||
aufgabe_id INTEGER,
|
||
erstellt TEXT NOT NULL
|
||
);
|
||
/* EINMAL BEWERBEN, NICHT DREIMAL -- und zwar von der Datenbank
|
||
durchgesetzt, nicht von einer Abfrage davor, die man vergessen
|
||
kann.
|
||
|
||
NUR AUF OFFENE: Wer abgelehnt wurde, darf sich spaeter wieder
|
||
bewerben (Lage aendert sich, Woche aendert sich). Ein Index
|
||
ueber ALLE Zustaende haette genau das verboten. */
|
||
CREATE UNIQUE INDEX IF NOT EXISTS idx_vorlagen_bewerbung_offen
|
||
ON vorlagen_bewerbungen (vorlage, person_id) WHERE zustand = 'beworben';
|
||
/* Die Frage im Betrieb lautet "was wartet gerade auf Antwort?" */
|
||
CREATE INDEX IF NOT EXISTS idx_vorlagen_bewerbung_zustand
|
||
ON vorlagen_bewerbungen (zustand, id);
|
||
|
||
CREATE TABLE IF NOT EXISTS chat_gifs (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
datei TEXT NOT NULL,
|
||
name TEXT NOT NULL,
|
||
pruef TEXT NOT NULL UNIQUE,
|
||
typ TEXT NOT NULL,
|
||
groesse INTEGER NOT NULL,
|
||
breite INTEGER,
|
||
hoehe INTEGER,
|
||
von_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
erstellt TEXT NOT NULL
|
||
);
|
||
/* Gezeigt wird "das Neueste zuerst" -- wer gerade etwas
|
||
hineingelegt hat, findet es oben. */
|
||
CREATE INDEX IF NOT EXISTS idx_chat_gifs_neu ON chat_gifs (id DESC);
|
||
|
||
/* REAKTIONEN (14.09.2026).
|
||
|
||
Filipe: "ich will dass man auf die nachrichten auch reagieren
|
||
kann mit einem emoji, die nachricht selbst ohne zu antworten."
|
||
|
||
DER SCHLUESSEL IST DIE ANTWORT AUF "WIE OFT?": Eine Person darf
|
||
dieselbe Nachricht mit VERSCHIEDENEN Zeichen bedenken, aber
|
||
jedes nur EINMAL. Genau das sagt der zusammengesetzte
|
||
Primaerschluessel -- und zwar so, dass die Datenbank es
|
||
durchsetzt und nicht eine Abfrage davor, die man vergessen
|
||
kann. Ein zweiter Klick auf dasselbe Zeichen nimmt es zurueck.
|
||
|
||
ON DELETE CASCADE an beiden Enden: Wird die Nachricht geloescht
|
||
oder der Mensch entfernt, geht die Reaktion mit. Eine Reaktion
|
||
ohne Nachricht waere eine Zeile, die niemand je wiederfindet.
|
||
|
||
WARUM KEIN ZAEHLER IN chat_nachrichten: Eine mitgefuehrte Zahl
|
||
muesste bei jedem Hin und Her stimmen -- und sie ist genau die
|
||
Art Angabe, die irgendwann nicht mehr stimmt und die niemand
|
||
nachrechnet. Gezaehlt wird beim Lesen. */
|
||
CREATE TABLE IF NOT EXISTS chat_reaktionen (
|
||
nachricht_id INTEGER NOT NULL REFERENCES chat_nachrichten(id) ON DELETE CASCADE,
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
zeichen TEXT NOT NULL,
|
||
wann TEXT NOT NULL,
|
||
PRIMARY KEY (nachricht_id, person_id, zeichen)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_chat_reaktionen_nachricht
|
||
ON chat_reaktionen (nachricht_id);
|
||
|
||
/* FAVORITEN (14.09.2026).
|
||
|
||
Filipe: "man soll auch auswaehlen koennen, also paar als
|
||
vorschlaege mit der moeglichkeit andere auszuwaehlen und als
|
||
favoriten zu speichern."
|
||
|
||
AM MENSCHEN, NICHT AM GERAET. Im Browser abgelegt waeren sie
|
||
am Handy andere als am Rechner -- und beim naechsten Loeschen
|
||
der Seitendaten weg. Wer sich etwas merkt, will es ueberall
|
||
wiederfinden.
|
||
|
||
Die Spalte "platz" haelt die REIHENFOLGE. Ohne sie waere die
|
||
Vorschlagszeile bei jedem Laden anders sortiert, und man
|
||
greift daneben, weil das Zeichen gewandert ist. */
|
||
CREATE TABLE IF NOT EXISTS chat_reaktion_favoriten (
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
zeichen TEXT NOT NULL,
|
||
platz INTEGER NOT NULL,
|
||
PRIMARY KEY (person_id, zeichen)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_chat_favoriten_person
|
||
ON chat_reaktion_favoriten (person_id, platz);
|
||
|
||
/* =================================================================
|
||
EINEN AUSHANG FUER SICH WEGNEHMEN (25.09.2026)
|
||
=================================================================
|
||
|
||
Filipe: "jeder soll das fixierte individuel fuer sich loesen
|
||
koennen aber niemals so dass es sich fuer alle loest. ausser
|
||
dogfather macht es oder die rechte hand, dann ist es bei jedem
|
||
weg. ansonsten sollen alle anderen rollen es individuell fuer
|
||
sich loesen koennen. dogfather und die rechte hand sollen die
|
||
option haben fuer sich selbst oder fuer alle zu loesen."
|
||
|
||
BIS HEUTE GAB ES NUR EIN LOESEN, UND DAS GALT FUER ALLE. Wer
|
||
den Knopf sah, nahm damit jedem im Raum den Aushang weg -- und
|
||
wer ihn nicht sah, musste die Ansage vom Montag bis Freitag
|
||
ueber jedem Gespraech stehen lassen.
|
||
|
||
DIESE TABELLE IST DAS PERSOENLICHE "gesehen, weg damit". Sie
|
||
sagt NICHTS darueber, ob die Nachricht angeheftet ist -- das
|
||
steht weiter in chat_nachrichten.angeheftet_am und gilt fuer
|
||
den Raum. Zwei verschiedene Fragen, zwei verschiedene Orte:
|
||
die eine beantwortet der Raum, die andere jeder fuer sich.
|
||
|
||
KEIN EINTRAG HEISST SICHTBAR. Nicht umgekehrt -- sonst
|
||
muesste beim Anheften fuer jeden Teilnehmer eine Zeile
|
||
entstehen, und wer spaeter dazukommt, saehe den Aushang nie.
|
||
|
||
ON DELETE CASCADE an beiden Enden (KEINE Gegen-Apostrophe in
|
||
diesem Kommentar -- er steht in einem Template-Literal, und
|
||
einer davon wuerde es beenden; das hat heute schon einmal den
|
||
Start gekostet): Verschwindet die
|
||
Nachricht oder die Person, verschwindet die Notiz mit. Eine
|
||
Zeile, die auf nichts mehr zeigt, ist kein Datenbestand,
|
||
sondern Streu. */
|
||
CREATE TABLE IF NOT EXISTS chat_pin_aus (
|
||
nachricht_id INTEGER NOT NULL
|
||
REFERENCES chat_nachrichten(id) ON DELETE CASCADE,
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
am TEXT NOT NULL,
|
||
PRIMARY KEY (nachricht_id, person_id)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_chat_pin_aus_person
|
||
ON chat_pin_aus (person_id, nachricht_id);
|
||
|
||
/* Creator-Profil (Onboarding aus dem Konzept). Eine Zeile je
|
||
Creator, entsteht erst beim ersten Speichern.
|
||
admin_notiz ist bewusst Teil dieser Tabelle, wird aber nur an
|
||
das Management ausgeliefert -- siehe workspace-profil.js. */
|
||
CREATE TABLE IF NOT EXISTS profile (
|
||
person_id INTEGER PRIMARY KEY REFERENCES personen(id) ON DELETE CASCADE,
|
||
handles TEXT,
|
||
nische TEXT,
|
||
live_zeiten TEXT,
|
||
technik TEXT,
|
||
ziel_live TEXT,
|
||
ziel_content TEXT,
|
||
ziel_community TEXT,
|
||
ziel_technik TEXT,
|
||
plan_start TEXT,
|
||
plan_prio1 TEXT,
|
||
plan_prio2 TEXT,
|
||
plan_prio3 TEXT,
|
||
naechster_review TEXT,
|
||
admin_notiz TEXT,
|
||
geaendert TEXT,
|
||
geaendert_von INTEGER REFERENCES personen(id) ON DELETE SET NULL
|
||
);
|
||
|
||
/* Termine, Calls und Reviews. Der Beginn steht als lokale Zeit
|
||
(JJJJ-MM-TTThh:mm) -- genau so, wie ein datetime-local-Feld sie
|
||
liefert. Bewusst ohne Zeitzone: Alle Beteiligten sitzen in
|
||
derselben, und eine falsch umgerechnete Uhrzeit waere schlimmer
|
||
als gar keine Umrechnung. */
|
||
CREATE TABLE IF NOT EXISTS termine (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
titel TEXT NOT NULL,
|
||
beschreibung TEXT,
|
||
art TEXT NOT NULL DEFAULT 'termin'
|
||
CHECK (art IN ('termin','call','review','bigmatch','turnier','special','collab','raid','charity')),
|
||
beginn TEXT NOT NULL,
|
||
dauer_min INTEGER NOT NULL DEFAULT 30,
|
||
ort TEXT,
|
||
creator_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
teilnehmer_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
erledigt INTEGER NOT NULL DEFAULT 0,
|
||
erstellt TEXT NOT NULL,
|
||
erstellt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_termine_beginn ON termine (beginn);
|
||
|
||
/* MEHRERE TEILNEHMER AN EINEM TERMIN (05.09.2026) ------------------
|
||
|
||
Wunsch Filipe: *"wenn ich im kalender was eintrage will ich dass
|
||
ich auch 2 leute markieren kann mit denen der call ist."*
|
||
|
||
Bis hierher hatte ein Termin GENAU EIN Gegenueber
|
||
(termine.teilnehmer_id). Fuer ein Gespraech zu dritt musste man
|
||
zwei Termine anlegen -- und hatte damit zwei Wahrheiten ueber
|
||
dieselbe halbe Stunde.
|
||
|
||
Gebaut nach demselben Muster wie datei_personen weiter unten:
|
||
eine eigene kleine Tabelle statt weiterer Spalten. Zwei Spalten
|
||
"teilnehmer2_id", "teilnehmer3_id" waeren beim vierten Menschen
|
||
wieder am Ende, und jede Abfrage muesste alle einzeln
|
||
aufzaehlen.
|
||
|
||
WICHTIG -- teilnehmer_id BLEIBT und behaelt seine Bedeutung:
|
||
Es ist das HAUPT-Gegenueber, mit dem der Termin vereinbart
|
||
wurde. Diese Tabelle sagt, WER SONST NOCH dabei ist. Beim
|
||
Umstellen wird der vorhandene Wert mit uebernommen, damit die
|
||
Teilnehmerliste von Anfang an vollstaendig ist und keine
|
||
Abfrage zwei Stellen zusammensuchen muss.
|
||
|
||
An dieser Tabelle haengt die SICHTBARKEIT (siehe
|
||
termineSichtbar): Wer hier steht, sieht den Termin. Ein
|
||
vergessener Eintrag ist deshalb kein Schoenheitsfehler,
|
||
sondern ein Termin, den jemand nicht findet. */
|
||
CREATE TABLE IF NOT EXISTS termin_teilnehmer (
|
||
termin_id INTEGER NOT NULL REFERENCES termine(id) ON DELETE CASCADE,
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
PRIMARY KEY (termin_id, person_id)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_termin_teilnehmer ON termin_teilnehmer (person_id);
|
||
|
||
/* =================================================================
|
||
DER WECKER (07.09.2026)
|
||
|
||
Wunsch Filipe: "wie so ein wecker, den man auch in den
|
||
eintraegen aktivieren oder ausschalten kann, den soll man sogar
|
||
so einstellen koennen, dass er einen auch mehrmals informiert,
|
||
einmal eine woche vorher, einmal drei tage vorher und einmal am
|
||
tag selber. das soll jeder fuer sich selbst einstellen koennen."
|
||
|
||
EINE ZEILE IST EIN WECKER. Nicht ein Feld am Termin mit einer
|
||
Liste darin: Mehrere Vorlaufzeiten je Termin sind der ganze
|
||
Zweck, und "jeder fuer sich" heisst, dass zwei Personen am
|
||
SELBEN Termin verschiedene Wecker haben. Beides zusammen ist
|
||
eine klassische n:m-Beziehung, und die gehoert in eine eigene
|
||
Tabelle. Ein Feld mit kommagetrennten Zahlen waere beim ersten
|
||
"zeig mir alle faelligen Wecker" nicht mehr abfragbar.
|
||
|
||
Die Spalte minuten_vorher statt eines Zeitpunkts: Ein Zeitpunkt muesste
|
||
bei JEDER Terminverschiebung nachgezogen werden -- und genau
|
||
das vergisst man. Ein Abstand rechnet sich beim Wecken aus dem
|
||
aktuellen Beginn und ist damit immer richtig, egal wie oft der
|
||
Termin wandert.
|
||
|
||
ON DELETE CASCADE an beiden Seiten: Ein Wecker ohne Termin
|
||
weckt niemanden, und ein Wecker ohne Person auch nicht.
|
||
Zusammengesetzter Primaerschluessel: Dieselbe Person kann
|
||
denselben Abstand nicht zweimal setzen -- sonst klingelt es
|
||
doppelt, und das merkt man erst nachts.
|
||
|
||
Recherche (Google Calendar, Outlook, Morgen): Fuenf Wecker je
|
||
Termin sind das uebliche Maximum, und die verbreitete Empfehlung
|
||
fuer Wichtiges lautet "eine Woche, ein Tag, am Tag selbst".
|
||
Genau diese Staffel bietet die Oberflaeche an. */
|
||
CREATE TABLE IF NOT EXISTS termin_wecker (
|
||
termin_id INTEGER NOT NULL REFERENCES termine(id) ON DELETE CASCADE,
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
minuten_vorher INTEGER NOT NULL,
|
||
gesetzt TEXT NOT NULL,
|
||
PRIMARY KEY (termin_id, person_id, minuten_vorher)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_termin_wecker_person
|
||
ON termin_wecker (person_id);
|
||
|
||
/* Dasselbe fuer die Wiederholungen. Eine Serie ist eine Regel --
|
||
die Teilnehmer gehoeren zur Regel, nicht zur einzelnen
|
||
Auspraegung, sonst muesste man sie jede Woche neu eintragen.
|
||
Der Nachfueller kopiert sie beim Anlegen jedes Termins hierher
|
||
hinueber (siehe workspace-serien.js). */
|
||
CREATE TABLE IF NOT EXISTS serie_teilnehmer (
|
||
serie_id INTEGER NOT NULL REFERENCES termin_serien(id) ON DELETE CASCADE,
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
PRIMARY KEY (serie_id, person_id)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_serie_teilnehmer ON serie_teilnehmer (person_id);
|
||
|
||
/* Wiederkehrende Termine (02.09.2026) --------------------------------
|
||
|
||
Eine Serie ist eine REGEL, kein Termin: "jeden Dienstag um 18:00
|
||
Uhr, bis ich es abstelle". Aus ihr entstehen echte Zeilen in
|
||
der Tabelle termine.
|
||
|
||
Warum echte Zeilen und nicht bloss gerechnete Ausprägungen:
|
||
ACHT andere Stellen lesen termine direkt -- Calls, Protokolle,
|
||
Berichte, Hinweise/Ampel, Suche, Aufgaben ("nächster Termin"),
|
||
Übersicht, Startseite. Eine nur im Kalender gerechnete
|
||
Wiederholung wäre in all diesen Ansichten unsichtbar gewesen,
|
||
ohne dass es jemandem auffällt, und beim geplanten ICS-Abo
|
||
ebenfalls. Der Nachfüller hält stattdessen einen Horizont echt
|
||
gefüllt -- damit sieht jedes Modul die Wiederholung, ohne dass
|
||
dort eine einzige Zeile geändert werden musste.
|
||
|
||
uhrzeit statt beginn: Eine Regel kennt keinen Zeitpunkt, nur
|
||
eine Uhrzeit und einen Takt. Gerechnet wird auf reinen
|
||
Datumstexten -- deshalb bleibt 18:00 auch über die
|
||
Zeitumstellung hinweg 18:00 und wandert nicht auf 17:00. */
|
||
CREATE TABLE IF NOT EXISTS termin_serien (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
titel TEXT NOT NULL,
|
||
beschreibung TEXT,
|
||
art TEXT NOT NULL DEFAULT 'termin'
|
||
CHECK (art IN ('termin','call','review','bigmatch','turnier','special','collab','raid','charity')),
|
||
takt TEXT NOT NULL
|
||
CHECK (takt IN ('taeglich','werktags','woechentlich',
|
||
'zweiwoechentlich','monatlich_datum',
|
||
'monatlich_letzter','monatlich_wochentag',
|
||
'jaehrlich')),
|
||
start_tag TEXT NOT NULL,
|
||
uhrzeit TEXT NOT NULL,
|
||
ende_tag TEXT,
|
||
dauer_min INTEGER NOT NULL DEFAULT 30,
|
||
ort TEXT,
|
||
creator_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
teilnehmer_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
aktiv INTEGER NOT NULL DEFAULT 1,
|
||
erstellt TEXT NOT NULL,
|
||
erstellt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
geaendert TEXT
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_serien_aktiv ON termin_serien (aktiv);
|
||
|
||
/* Ausgelassene Tage einer Serie. Wer eine einzelne Ausprägung
|
||
löscht, meint "dieses eine Mal nicht" -- ohne diese Tabelle
|
||
legte der Nachfüller sie beim nächsten Aufruf wieder an, und der
|
||
gelöschte Termin wäre kommentarlos zurück. */
|
||
CREATE TABLE IF NOT EXISTS termin_serien_aus (
|
||
serie_id INTEGER NOT NULL REFERENCES termin_serien(id) ON DELETE CASCADE,
|
||
tag TEXT NOT NULL,
|
||
PRIMARY KEY (serie_id, tag)
|
||
);
|
||
|
||
/* Dateiablage. Auf der Platte traegt jede Datei einen erzeugten
|
||
Zufallsnamen (name_datei), der Originalname steht nur hier.
|
||
Dadurch kann ein Dateiname weder Pfade verlassen noch etwas
|
||
ueberschreiben. Der Freigabe-Ablauf aus dem Konzept steckt in
|
||
status: entwurf -> review -> freigegeben. */
|
||
CREATE TABLE IF NOT EXISTS dateien (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name_original TEXT NOT NULL,
|
||
name_datei TEXT NOT NULL UNIQUE,
|
||
groesse INTEGER NOT NULL,
|
||
typ TEXT,
|
||
status TEXT NOT NULL DEFAULT 'entwurf'
|
||
CHECK (status IN ('entwurf','review','freigegeben')),
|
||
notiz TEXT,
|
||
creator_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
hochgeladen_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
erstellt TEXT NOT NULL,
|
||
geaendert TEXT
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_dateien_creator ON dateien (creator_id);
|
||
|
||
/* Eintraege der Phase-2-Bereiche (LIVE, Content, Technik,
|
||
Community, Schutz). Eine gemeinsame Tabelle statt fuenf fast
|
||
gleicher: Die Bereiche unterscheiden sich nur in den erlaubten
|
||
Arten und darin, ob Bewertung oder Dringlichkeit dazugehoeren --
|
||
das steht in workspace-bereiche.js, nicht in der Datenbank. */
|
||
CREATE TABLE IF NOT EXISTS eintraege (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
bereich TEXT NOT NULL
|
||
CHECK (bereich IN ('live','content','technik','community','schutz','agentur','ideen')),
|
||
art TEXT NOT NULL,
|
||
titel TEXT NOT NULL,
|
||
text TEXT,
|
||
datum TEXT NOT NULL,
|
||
bewertung INTEGER CHECK (bewertung IS NULL OR (bewertung BETWEEN 1 AND 5)),
|
||
dringlichkeit TEXT NOT NULL DEFAULT 'mittel'
|
||
CHECK (dringlichkeit IN ('hoch','mittel','niedrig')),
|
||
status TEXT NOT NULL DEFAULT 'offen'
|
||
CHECK (status IN ('offen','erledigt')),
|
||
creator_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
erstellt TEXT NOT NULL,
|
||
erstellt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
geaendert TEXT
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_eintraege_bereich ON eintraege (bereich, creator_id);
|
||
/* Die Tabellen des Rudels folgen weiter unten, gleich nachdem
|
||
dieser Bauplan durch ist -- sie haengen an eintraege und
|
||
personen und koennen deshalb nicht davor stehen. Ihr SQL steht
|
||
in treff-tabellen.js, samt Begruendung je Tabelle. */
|
||
|
||
/* Wer darf welche Datei sehen. Bisher hatte eine Datei genau
|
||
EINEN Bereich (dateien.creator_id) -- damit liess sich eine
|
||
Datei nicht zweien geben, ohne sie zweimal hochzuladen.
|
||
Diese Tabelle erlaubt beliebig viele Empfaenger, Creator wie
|
||
Scouts. creator_id bleibt bestehen: Es sagt weiterhin, zu
|
||
wessen BEREICH eine Datei gehoert, waehrend hier steht, WER sie
|
||
sehen darf. Zwei verschiedene Fragen. */
|
||
CREATE TABLE IF NOT EXISTS datei_personen (
|
||
datei_id INTEGER NOT NULL REFERENCES dateien(id) ON DELETE CASCADE,
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
PRIMARY KEY (datei_id, person_id)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_datei_personen ON datei_personen (person_id);
|
||
|
||
/* Wissens-Bibliothek: Anleitungen als PDF, hinter dem Login.
|
||
Jede PDF hat GENAU EINE Hauptkategorie -- so gibt es sie nur
|
||
einmal. Zusaetzliche Themen laufen ueber Tags, damit dieselbe
|
||
Anleitung trotzdem ueber mehrere Suchbegriffe auffindbar ist.
|
||
Die Kategorien selbst stehen im Code (workspace-wissen.js),
|
||
nicht hier: Sie sind eine bewusste Gliederung, keine Nutzdaten. */
|
||
CREATE TABLE IF NOT EXISTS wissen (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
titel TEXT NOT NULL,
|
||
beschreibung TEXT,
|
||
kategorie TEXT NOT NULL,
|
||
stufe TEXT NOT NULL DEFAULT 'einsteiger'
|
||
CHECK (stufe IN ('einsteiger','fortgeschritten','profi')),
|
||
geraet TEXT NOT NULL DEFAULT 'egal'
|
||
CHECK (geraet IN ('pc','handy','beide','egal')),
|
||
tags TEXT,
|
||
name_original TEXT NOT NULL,
|
||
name_datei TEXT NOT NULL,
|
||
groesse INTEGER NOT NULL,
|
||
wichtig INTEGER NOT NULL DEFAULT 0,
|
||
veroeffentlicht TEXT NOT NULL,
|
||
aktualisiert TEXT,
|
||
erstellt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_wissen_kat ON wissen (kategorie);
|
||
|
||
/* =================================================================
|
||
LEISTUNG — die Zahlen, an denen die Betreuung haengt (06.09.2026)
|
||
|
||
DER GROSSE BEFUND aus dem Plan vom 31.08.2026: Der Workspace
|
||
organisiert ARBEIT sehr gut, aber es gibt keine einzige Zahl
|
||
ueber das GESCHAEFT. Damit haengt die ganze Betreuung in der
|
||
Luft -- der Start-Check bewertet ohne zu messen, der Report
|
||
fragt "was hat funktioniert" ohne Beleg, das 90-Tage-Ziel ist
|
||
ein Satz statt eines Fortschritts.
|
||
|
||
EINE ZEILE JE CREATOR UND TAG. Tag und nicht Woche: Feineres
|
||
laesst sich immer zusammenfassen, Groeberes nie aufteilen. Wer
|
||
mit Wochenwerten anfaengt, kann spaeter nie mehr sagen, ob der
|
||
Einbruch am Dienstag oder am Wochenende lag.
|
||
|
||
WELCHE FELDER -- und warum genau diese (Recherche 31.08. und
|
||
06.09.2026, TikTok LIVE Backstage):
|
||
|
||
diamanten die Zahl, an der TikTok und die Agentur den
|
||
Erfolg messen
|
||
dauer_min Grundlage fuer "gueltige Tage"/"aktive Stunden"
|
||
gueltiger_tag der Zaehler, an dem Ranghochstufungen haengen
|
||
zuschauer_avg sagt, ob es TRAEGT
|
||
zuschauer_max sagt, was MOEGLICH waere
|
||
verweildauer_s DIE LEITZAHL (Recherche 06.09.2026): 2026
|
||
haengt der Algorithmus alles daran, und sie
|
||
gehoert als Betriebskennzahl behandelt --
|
||
sie soll die naechste Runde steuern, nicht
|
||
die letzte erklaeren
|
||
schenker wie BREIT die Unterstuetzung steht, nicht nur
|
||
wie hoch. Ein Grossspender ist ein Risiko,
|
||
zwanzig kleine sind ein Fundament
|
||
follower_neu waechst der Kanal oder dreht er sich im Kreis
|
||
notiz ein Satz Kontext ("PK gegen X", "krank")
|
||
|
||
WARUM DIE NOTIZ DAZUGEHOERT: Ohne sie sieht man in drei Monaten
|
||
einen Einbruch und weiss nicht mehr, dass die Person Grippe
|
||
hatte. Zahlen ohne Umstaende fuehren in die Irre.
|
||
|
||
PRIMAERSCHLUESSEL (creator_id, tag): Derselbe Creator am
|
||
selben Tag kann nur EINE Zeile haben. Das ist die halbe Miete
|
||
beim Import -- wer eine Datei zweimal einliest, ueberschreibt
|
||
dann, statt zu verdoppeln. Verdoppelte Zahlen sind schlimmer
|
||
als fehlende: Sie sehen richtig aus. */
|
||
CREATE TABLE IF NOT EXISTS leistung (
|
||
creator_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
tag TEXT NOT NULL, /* JJJJ-MM-TT, Ortszeit */
|
||
diamanten INTEGER,
|
||
dauer_min INTEGER,
|
||
gueltiger_tag INTEGER NOT NULL DEFAULT 0,
|
||
zuschauer_avg INTEGER,
|
||
zuschauer_max INTEGER,
|
||
verweildauer_s INTEGER, /* Sekunden, die Leitzahl */
|
||
schenker INTEGER,
|
||
follower_neu INTEGER,
|
||
notiz TEXT,
|
||
erfasst TEXT NOT NULL,
|
||
erfasst_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
/* Woher der Wert kommt. Wichtig, wenn eine Zahl seltsam
|
||
aussieht: von Hand getippt oder aus dem Export? */
|
||
quelle TEXT NOT NULL DEFAULT 'hand'
|
||
CHECK (quelle IN ('hand','import')),
|
||
PRIMARY KEY (creator_id, tag)
|
||
);
|
||
|
||
/* ZAHLEN FUER EINEN ZEITRAUM (14.09.2026).
|
||
|
||
Filipe: "ich will dass das auch so geht, mach dass es
|
||
funktioniert ... sieh zu dass die excel auch so durch geht."
|
||
|
||
Backstage gibt eine Ausgabe fuer einen ZEITRAUM heraus --
|
||
"2026-09-01 ~ 2026-09-13", eine Zeile je Creator, alle Zahlen
|
||
aufsummiert. Bis heute wurde so eine Zeile abgewiesen, weil
|
||
daraus kein Tageswert wird.
|
||
|
||
DREI WEGE WAEREN MOEGLICH GEWESEN, und zwei davon waeren
|
||
Zahlenfaelschung:
|
||
* auf den ersten Tag schreiben -> 13 Tage auf einem Tag,
|
||
sieht richtig aus, ist es nicht;
|
||
* gleichmaessig verteilen -> erfundene Tage, die es nie gab.
|
||
Der dritte ist dieser: den Zeitraum ALS Zeitraum ablegen. Was
|
||
drinsteht, ist dann wahr -- und was es nicht sagt (welcher Tag
|
||
wie lief), behauptet es auch nicht.
|
||
|
||
EIGENE TABELLE UND NICHT EIN FELD IN "leistung": Dort ist der
|
||
Schluessel (creator, tag). Ein Zeitraum ab dem 1. September
|
||
wuerde mit dem ECHTEN 1. September zusammenstossen -- und eine
|
||
der beiden Zahlen waere weg. Getrennt kann keines das andere
|
||
ueberschreiben, und die Wochenzahlen bleiben, was sie sind:
|
||
aus Tagen gerechnet.
|
||
|
||
DER SCHLUESSEL IST (creator, von, bis): Dieselbe Datei zweimal
|
||
einzulesen ersetzt denselben Zeitraum, statt ihn zu
|
||
verdoppeln. */
|
||
CREATE TABLE IF NOT EXISTS leistung_zeitraum (
|
||
creator_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
von TEXT NOT NULL,
|
||
bis TEXT NOT NULL,
|
||
tage INTEGER NOT NULL,
|
||
diamanten INTEGER,
|
||
dauer_min INTEGER,
|
||
gueltige_tage INTEGER,
|
||
zuschauer_avg INTEGER,
|
||
zuschauer_max INTEGER,
|
||
verweildauer_s INTEGER,
|
||
schenker INTEGER,
|
||
follower_neu INTEGER,
|
||
erfasst TEXT NOT NULL,
|
||
erfasst_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
PRIMARY KEY (creator_id, von, bis)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_leistung_zeitraum_creator
|
||
ON leistung_zeitraum (creator_id, bis DESC);
|
||
CREATE INDEX IF NOT EXISTS idx_leistung_tag ON leistung (tag DESC);
|
||
|
||
/* ALLES, WAS IN DER DATEI STAND (16.09.2026)
|
||
|
||
Filipe: "ich will dass alles von der excel datei genommen wird
|
||
... wenn ich dir runter lade soll alles notiert und angezeigt
|
||
werden."
|
||
|
||
Seine echte Backstage-Ausgabe hat 41 SPALTEN. Die Tabelle
|
||
darueber nimmt acht davon. Dreiunddreissig Spalten -- Zahlen
|
||
vom letzten Monat, Prozentwerte, Matches, Multi-Gast-LIVEs,
|
||
Fanclub, Graduierungs- und Stufenstatus -- wurden beim Einlesen
|
||
weggeworfen, ohne dass irgendwo stand, dass es sie gab.
|
||
|
||
WARUM NICHT 33 WEITERE SPALTEN OBEN? Weil das eine ABGESCHRIEBENE
|
||
LISTE waere, und die hat in diesem Haus schon zweimal Daten
|
||
gekostet (siehe die Notiz zur Agentur-Umstellung). Sie waere
|
||
ausserdem beim naechsten Backstage-Update falsch: TikTok
|
||
benennt Spalten um und fuegt welche hinzu, ohne jemanden zu
|
||
fragen.
|
||
|
||
EINE ZEILE JE SPALTE, und der NAME ist der Schluessel. Damit
|
||
gilt: Was in der Datei steht, steht danach in der Datenbank --
|
||
auch eine Spalte, die es heute noch gar nicht gibt. Nichts zu
|
||
pflegen, nichts zu vergessen.
|
||
|
||
Die Spalte nr IST DIE REIHENFOLGE DER DATEI, nicht die Identitaet. Wer
|
||
die Reihenfolge zum Schluessel machte, verwechselte zwei
|
||
Spalten, sobald Backstage eine dazwischenschiebt.
|
||
|
||
BEIDES WIRD GESPEICHERT: der Text woertlich, wie er dastand
|
||
(daraus kann man immer noch alles herleiten), und die Zahl, wenn
|
||
es eine ist. Nur die Zahl zu behalten hiesse, "Nicht graduiert"
|
||
und "6Std. 24Min. 7Sek." zu verlieren; nur den Text zu behalten
|
||
hiesse, nicht rechnen zu koennen. */
|
||
CREATE TABLE IF NOT EXISTS leistung_zeitraum_feld (
|
||
creator_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
von TEXT NOT NULL,
|
||
bis TEXT NOT NULL,
|
||
nr INTEGER NOT NULL,
|
||
name TEXT NOT NULL,
|
||
text TEXT,
|
||
zahl REAL,
|
||
PRIMARY KEY (creator_id, von, bis, name)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_leistung_zfeld
|
||
ON leistung_zeitraum_feld (creator_id, von, bis, nr);
|
||
|
||
/* ZIELE je Creator. Aus dem 90-Tage-Plan, den es im Profil schon
|
||
als FREITEXT gibt -- daraus wird hier eine pruefbare Groesse.
|
||
"Mehr Reichweite" ist kein Ziel, "4 LIVE-Tage pro Woche" ist
|
||
eines.
|
||
|
||
Eine Zeile je Creator, nicht je Ziel und Zeitraum: Ein Ziel,
|
||
das sich woechentlich aendert, ist keines. Wer es anpasst,
|
||
ueberschreibt -- der Verlauf steht ohnehin in der Tabelle
|
||
"leistung". */
|
||
CREATE TABLE IF NOT EXISTS leistung_ziele (
|
||
creator_id INTEGER PRIMARY KEY REFERENCES personen(id) ON DELETE CASCADE,
|
||
diamanten_woche INTEGER,
|
||
tage_woche INTEGER,
|
||
verweildauer_s INTEGER,
|
||
gesetzt TEXT NOT NULL,
|
||
gesetzt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL
|
||
);
|
||
|
||
/* Start-Check / Erstanalyse (Konzept, Seite 5). Eine Zeile je
|
||
geprueftem Punkt -- ungeprueft heisst: gar keine Zeile. So sieht
|
||
man am Bestand sofort, wie weit die Analyse ist, ohne leere
|
||
Platzhalter mitschleppen zu muessen.
|
||
Die Punkte selbst stehen im Code, nicht hier: Waeren sie
|
||
Datensaetze, koennte jeder eigene anlegen -- und zwei Creator
|
||
waeren nicht mehr vergleichbar, was der ganze Zweck ist. */
|
||
CREATE TABLE IF NOT EXISTS startcheck (
|
||
creator_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
feld TEXT NOT NULL,
|
||
punkt TEXT NOT NULL,
|
||
bewertung TEXT CHECK (bewertung IS NULL OR bewertung IN ('ok','mittel','handlung')),
|
||
notiz TEXT,
|
||
geaendert TEXT NOT NULL,
|
||
geaendert_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
PRIMARY KEY (creator_id, feld, punkt)
|
||
);
|
||
|
||
/* Betreuung: wer kuemmert sich um welchen Creator.
|
||
Scouts betreuen Creator wie das Management, aber nur die ihnen
|
||
zugeteilten -- deshalb braucht es eine ausdrueckliche Zuordnung
|
||
und keine Rollenregel. Ein Creator hat genau eine zustaendige
|
||
Person (PRIMARY KEY), damit nie unklar ist, wer gefragt ist.
|
||
Das Management sieht ohnehin alle und braucht keinen Eintrag. */
|
||
CREATE TABLE IF NOT EXISTS betreuung (
|
||
creator_id INTEGER PRIMARY KEY REFERENCES personen(id) ON DELETE CASCADE,
|
||
betreuer_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
seit TEXT NOT NULL,
|
||
gesetzt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_betreuung_betreuer ON betreuung (betreuer_id);
|
||
|
||
/* Welcher Scout gehoert zu welchem Manager (01.09.2026).
|
||
|
||
"er soll nur die Scouts sehen, die ihm zugeteilt sind."
|
||
|
||
WARUM EINE EIGENE TABELLE und nicht ein zweiter Eintrag in
|
||
betreuung: Dort heisst die Spalte creator_id, und der ganze
|
||
uebrige Code liest sie als "das ist ein Creator". Ein Scout in
|
||
dieser Spalte waere technisch moeglich und fachlich eine Luege --
|
||
betreuteIds() gaebe dann Scout-Nummern zurueck, die anderswo als
|
||
Creator behandelt wuerden. Solche Abkuerzungen raechen sich
|
||
genau dann, wenn niemand mehr weiss, dass sie getroffen wurden.
|
||
|
||
Ein Scout gehoert zu hoechstens EINEM Manager -- deshalb ist
|
||
scout_id der Schluessel. Eine Zuteilung an mehrere waere die
|
||
Frage "wer ist zustaendig?" ohne Antwort.
|
||
|
||
ON DELETE CASCADE auf beiden Seiten: Verschwindet der Scout oder
|
||
der Manager, ist die Zuteilung gegenstandslos. Sie stehenzulassen
|
||
hiesse, dass eine geloeschte Person weiter Sichtbarkeit steuert. */
|
||
CREATE TABLE IF NOT EXISTS scout_zuteilung (
|
||
scout_id INTEGER PRIMARY KEY REFERENCES personen(id) ON DELETE CASCADE,
|
||
manager_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
seit TEXT NOT NULL,
|
||
gesetzt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_scout_zuteilung_manager
|
||
ON scout_zuteilung (manager_id);
|
||
|
||
/* Meeting-Protokoll zu einem Termin (Konzept, Seite 13).
|
||
Bewusst eine eigene Tabelle statt neuer Spalten in termine: Es
|
||
gibt hier keine Migrationen, und ein Protokoll gehoert ohnehin
|
||
nur zu einem Bruchteil der Termine. Ein Termin hat hoechstens
|
||
ein Protokoll -- daher UNIQUE. */
|
||
CREATE TABLE IF NOT EXISTS protokolle (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
termin_id INTEGER NOT NULL UNIQUE REFERENCES termine(id) ON DELETE CASCADE,
|
||
punkte TEXT,
|
||
entscheidungen TEXT,
|
||
naechster_termin_id INTEGER REFERENCES termine(id) ON DELETE SET NULL,
|
||
erstellt TEXT NOT NULL,
|
||
erstellt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
geaendert TEXT
|
||
);
|
||
|
||
/* "Jeder Call endet mit klaren To-dos, die direkt ins Board
|
||
uebernommen werden." Die To-dos leben deshalb NICHT im Protokoll,
|
||
sondern sind echte Aufgaben. Diese Tabelle merkt sich nur, aus
|
||
welchem Gespraech eine Aufgabe entstanden ist. */
|
||
CREATE TABLE IF NOT EXISTS protokoll_aufgaben (
|
||
protokoll_id INTEGER NOT NULL REFERENCES protokolle(id) ON DELETE CASCADE,
|
||
aufgabe_id INTEGER NOT NULL REFERENCES aufgaben(id) ON DELETE CASCADE,
|
||
PRIMARY KEY (protokoll_id, aufgabe_id)
|
||
);
|
||
|
||
/* Scout-CRM: die Pipeline vom ersten Fund bis zur Uebergabe.
|
||
creator_id ist gesetzt, sobald aus einem uebergebenen Lead eine
|
||
echte Person angelegt wurde -- damit bleibt nachvollziehbar, wer
|
||
einen Creator gefunden hat, auch Jahre spaeter. */
|
||
CREATE TABLE IF NOT EXISTS leads (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name TEXT NOT NULL,
|
||
plattform TEXT,
|
||
handle TEXT,
|
||
status TEXT NOT NULL DEFAULT 'neu'
|
||
CHECK (status IN ('neu','angesprochen','gespraech','interessiert','uebergeben','abgelehnt')),
|
||
prioritaet TEXT NOT NULL DEFAULT 'mittel'
|
||
CHECK (prioritaet IN ('hoch','mittel','niedrig')),
|
||
aktivitaet TEXT,
|
||
potenzial TEXT,
|
||
notizen TEXT,
|
||
letzte_nachricht TEXT,
|
||
naechster_followup TEXT,
|
||
scout_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
creator_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
erstellt TEXT NOT NULL,
|
||
erstellt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
geaendert TEXT,
|
||
uebergeben_am TEXT
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_leads_scout ON leads (scout_id, status);
|
||
|
||
CREATE TABLE IF NOT EXISTS versuche (
|
||
ip TEXT NOT NULL,
|
||
zeitpunkt TEXT NOT NULL
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_versuche_ip ON versuche (ip, zeitpunkt);
|
||
CREATE INDEX IF NOT EXISTS idx_sitzungen_gueltig ON sitzungen (gueltig_bis);
|
||
`);
|
||
/* Die Tabellen des Treffs. Eigene Datei, weil workspace-treff.js
|
||
aus DIESER Datei importiert -- ein Importkreis waere die Folge,
|
||
und ueber einen Kreis kommen Konstanten als undefined an, ohne
|
||
dass irgendwo ein Fehler erscheint. */
|
||
treffTabellen(d);
|
||
/* Der vertrauliche Meldeweg (19.09.2026). Haengt an personen und
|
||
kommt deshalb hierher, nicht in den Bauplan darueber. */
|
||
hilfeTabellen(d);
|
||
supportTabellen(d);
|
||
/* Der Notizblock (26.09.2026). Haengt an personen (CASCADE) und
|
||
kommt deshalb hierher, nicht in den Bauplan darueber. */
|
||
notizTabellen(d);
|
||
reaktionTabellen(d);
|
||
/* Die Wege zum Unterstützen (24.09.2026). Sie hängen an personen
|
||
(geaendert_von) und kommen deshalb hierher, nicht in den Bauplan
|
||
darüber. */
|
||
unterstuetzungTabellen(d);
|
||
umstellungen(d);
|
||
_db = d;
|
||
return d;
|
||
} catch (fehler) {
|
||
_dbFehler = fehler;
|
||
console.error("[workspace] Datenbank nicht verfügbar:", fehler?.message);
|
||
throw fehler;
|
||
}
|
||
}
|
||
|
||
/* ---------- Hilfsmittel ---------------------------------------------- */
|
||
|
||
const jetzt = () => new Date().toISOString();
|
||
|
||
/* Die echte Besucher-IP.
|
||
|
||
Warum das nötig ist: Die Kette lautet Besucher -> Cloudflare -> Caddy ->
|
||
Express. `trust proxy` steht auf 1, Express nimmt daher den letzten
|
||
Eintrag aus X-Forwarded-For -- und das ist die Adresse von Cloudflare,
|
||
nicht die des Besuchers. Folge: Im Protokoll stand bei jedem dieselbe
|
||
IP, und viel schlimmer -- ALLE Nutzer teilten sich einen einzigen
|
||
Sperr-Zähler. Ein paar Tippfehler von drei Leuten hätten den Zugang für
|
||
alle gesperrt.
|
||
|
||
Cloudflare setzt die echte Adresse in CF-Connecting-IP und überschreibt
|
||
den Kopf bei jeder Anfrage, ein Besucher kann ihn also nicht fälschen.
|
||
Einschränkung: Wer die Adresse des Servers kennt und ihn unter Umgehung
|
||
von Cloudflare direkt anspricht, könnte den Kopf frei setzen. Deshalb
|
||
wird auf die Peer-Adresse zurückgefallen, sobald der Kopf fehlt -- und
|
||
beide Werte landen im Protokoll, damit so etwas auffällt. */
|
||
export function echteIp(req) {
|
||
const cf = req.get("cf-connecting-ip");
|
||
if (cf && cf.length <= 45) return cf.trim();
|
||
return req.ip || "?";
|
||
}
|
||
|
||
function hashe(code, salt, N) {
|
||
return scryptSync(code, salt, SCRYPT.keylen,
|
||
{ N, r: SCRYPT.r, p: SCRYPT.p, maxmem: 256 * 1024 * 1024 }).toString("hex");
|
||
}
|
||
|
||
/* =====================================================================
|
||
DER SUCHSCHLUESSEL ZU EINEM CODE (09.09.2026)
|
||
|
||
Beantwortet in Mikrosekunden, WEN man pruefen soll -- nicht, ob der
|
||
Code stimmt. Das bleibt Sache von scrypt darunter.
|
||
|
||
WARUM EIN HMAC UND KEIN EINFACHER HASH: Ein einfacher Hash ueber den
|
||
Code waere ueberall gleich. Mit einem Schluessel, der nur in dieser
|
||
einen Datenbank steht, ist der Wert ausserhalb wertlos -- er laesst
|
||
sich nirgends nachschlagen und nirgends wiedererkennen.
|
||
|
||
WARUM DER SCHLUESSEL IN DIE DATENBANK GEHOERT UND NICHT IN DIE
|
||
UMGEBUNG: Eine neue Pflichtangabe in server/.env ist eine Angabe, die
|
||
beim naechsten Aufsetzen fehlen kann. Sie wuerde nicht laut scheitern,
|
||
sondern leise: Der stille Zugang fuende niemanden mehr, und die
|
||
Anmeldung saehe aus wie ein falscher Code. Aus der Datenbank kann sie
|
||
nicht verschwinden -- sie wird mitgesichert wie alles andere.
|
||
|
||
OHNE SCHLUESSEL GIBT ES KEINE ANTWORT, nicht etwa eine schlechtere:
|
||
`null` heisst "konnte nicht nachsehen", und die Anmeldung behandelt
|
||
das wie "nicht gefunden". Ein Rueckfall auf einen ungeschluesselten
|
||
Wert waere die stille Verschlechterung, die niemandem auffaellt. */
|
||
let _kennungSchluessel = null;
|
||
function kennungSchluessel() {
|
||
if (_kennungSchluessel) return _kennungSchluessel;
|
||
try {
|
||
const d = db();
|
||
const lies = () => d.prepare(
|
||
"SELECT wert FROM einstellungen WHERE schluessel = 'code_kennung_schluessel'").get()?.wert;
|
||
let wert = lies();
|
||
if (!wert) {
|
||
/* Bewusst NICHT ueber einstellungSetzen(): Das schriebe einen
|
||
Eintrag ins Protokoll, und ein Protokolleintrag ist genau die
|
||
Spur, die es hier nicht geben soll. */
|
||
d.prepare(`INSERT INTO einstellungen (schluessel, wert, geaendert, von)
|
||
VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING`)
|
||
.run("code_kennung_schluessel", randomBytes(32).toString("hex"),
|
||
new Date().toISOString());
|
||
/* Danach nochmal LESEN statt den erzeugten Wert zu benutzen:
|
||
Legen zwei Vorgaenge ihn im selben Augenblick an, gewinnt einer
|
||
-- und beide muessen mit demselben weiterrechnen. */
|
||
wert = lies();
|
||
}
|
||
_kennungSchluessel = wert || null;
|
||
return _kennungSchluessel;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
export function codeKennung(code) {
|
||
const schluessel = kennungSchluessel();
|
||
if (!schluessel || !code) return null;
|
||
return createHmac("sha256", schluessel).update(String(code)).digest("hex");
|
||
}
|
||
|
||
/* Vergleich in konstanter Zeit. Ein normaler ===-Vergleich bricht beim
|
||
ersten abweichenden Zeichen ab; aus den Laufzeitunterschieden lässt sich
|
||
ein Geheimnis Zeichen für Zeichen erraten. */
|
||
function gleich(a, b) {
|
||
const x = Buffer.from(String(a), "utf8");
|
||
const y = Buffer.from(String(b), "utf8");
|
||
if (x.length !== y.length) return false;
|
||
return timingSafeEqual(x, y);
|
||
}
|
||
|
||
const tokenHash = (t) => createHash("sha256").update(t).digest("hex");
|
||
|
||
export function protokolliere(aktion, { personId = null, rolle = null, detail = null, ip = null } = {}) {
|
||
try {
|
||
db().prepare(
|
||
"INSERT INTO protokoll (zeitpunkt, person_id, rolle, aktion, detail, ip) VALUES (?,?,?,?,?,?)"
|
||
).run(jetzt(), personId, rolle, aktion, detail, ip);
|
||
} catch { /* Ein fehlgeschlagener Protokolleintrag darf nichts blockieren. */ }
|
||
}
|
||
|
||
function zuVieleVersuche(ip) {
|
||
const grenze = new Date(Date.now() - VERSUCHE_FENSTER_MIN * 60_000).toISOString();
|
||
db().prepare("DELETE FROM versuche WHERE zeitpunkt < ?").run(grenze);
|
||
const { anzahl } = db()
|
||
.prepare("SELECT COUNT(*) AS anzahl FROM versuche WHERE ip = ? AND zeitpunkt >= ?")
|
||
.get(ip, grenze);
|
||
return anzahl >= VERSUCHE_MAX;
|
||
}
|
||
|
||
/* ---------- Sitzung ---------------------------------------------------- */
|
||
|
||
export function sitzungLesen(req) {
|
||
const token = req.cookies?.[COOKIE];
|
||
if (!token) return null;
|
||
try {
|
||
const reihe = db().prepare(`
|
||
SELECT s.gueltig_bis, p.id, p.name, p.rolle, p.aktiv
|
||
FROM sitzungen s JOIN personen p ON p.id = s.person_id
|
||
WHERE s.token_hash = ?`).get(tokenHash(token));
|
||
if (!reihe || !reihe.aktiv) return null;
|
||
if (reihe.gueltig_bis < jetzt()) {
|
||
db().prepare("DELETE FROM sitzungen WHERE token_hash = ?").run(tokenHash(token));
|
||
return null;
|
||
}
|
||
|
||
/* ==== DAS FENSTER GLEITET MIT (20.09.2026) =======================
|
||
|
||
Wer die Seite benutzt, bleibt angemeldet. Verlaengert wird erst,
|
||
wenn weniger als die HAELFTE des Fensters uebrig ist -- sonst
|
||
waere hier ein Schreibzugriff bei jedem einzelnen Aufruf, auch
|
||
bei jedem Herzschlag des Ereignisstroms.
|
||
|
||
DER KEKS MUSS MIT. Ohne ihn liefe die Zeile in der Datenbank
|
||
weiter und der Browser wuerfe den Keks trotzdem weg -- die
|
||
Person waere abgemeldet, obwohl der Server sie noch kennt. Das
|
||
ist die Sorte Fehler, die niemand findet, weil beide Seiten fuer
|
||
sich richtig aussehen.
|
||
|
||
`req.res` STATT EINES ZWEITEN ARGUMENTS: Diese Funktion wird an
|
||
26 Stellen aufgerufen -- in jedem Fachmodul einmal. Einen
|
||
Parameter zu ergaenzen hiesse, 26 Aufrufe zu aendern und beim
|
||
27. Modul daran zu denken. Express haengt die Antwort ohnehin an
|
||
die Anfrage; wo sie fehlt (etwa in einer Pruefung), wird eben
|
||
nur die Datenbank verlaengert. */
|
||
const restMs = new Date(reihe.gueltig_bis).getTime() - Date.now();
|
||
if (restMs < SITZUNG_MS / 2) {
|
||
const neuBis = new Date(Date.now() + SITZUNG_MS).toISOString();
|
||
db().prepare("UPDATE sitzungen SET gueltig_bis = ? WHERE token_hash = ?")
|
||
.run(neuBis, tokenHash(token));
|
||
try {
|
||
req.res?.cookie?.(COOKIE, token, {
|
||
httpOnly: true, secure: !!req.secure, sameSite: "lax",
|
||
path: "/workspace", maxAge: SITZUNG_MS,
|
||
});
|
||
} catch { /* kein res (z. B. in einer Pruefung) -- die Zeile reicht */ }
|
||
}
|
||
/* DIE ADRESSE ENTSCHEIDET MIT (10.09.2026).
|
||
|
||
Seit die Modi-App unter crew.dogfather-universe.com laeuft, gehoert
|
||
jede Sitzung zu genau EINER Adresse: Modis dorthin, alle anderen auf
|
||
den Workspace. Die Regel steht in crew-adresse.js und ist dort ohne
|
||
laufenden Server pruefbar.
|
||
|
||
WARUM HIER UND NICHT IN DEN MODULEN: Durch diese Funktion geht jedes
|
||
der 26 Fachmodule und jede Seitenschranke. Eine Middleware daneben
|
||
koennte man in einem neuen Modul vergessen -- und ein vergessener
|
||
Rechteschutz faellt nicht auf, weil dann alles geht.
|
||
|
||
`null` heisst hier dasselbe wie bei einer abgelaufenen Sitzung:
|
||
nicht angemeldet. Die Seitenschranke schickt zur Zugangswand, die
|
||
Schnittstellen antworten mit 401. Beides ist genau richtig -- auf
|
||
dieser Adresse ist diese Person schlicht niemand. */
|
||
if (!sitzungPasstZurAdresse(reihe.rolle, req.get?.("host"))) return null;
|
||
|
||
/* 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. */
|
||
/* ==== 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"
|
||
: istPruefAdresse(req.get?.("host")) ? null
|
||
: "agentur";
|
||
|
||
/* DER AUSSCHLUSS WIRKT HIER -- an derselben Stelle wie alles andere
|
||
(11.09.2026).
|
||
|
||
Ein Ausschluss aus dem Treff ist nicht "darf nichts mehr
|
||
schreiben", sondern "ist nicht mehr da". Stuende die Pruefung an
|
||
den Schreibwegen, koennte ein Ausgeschlossener weiter mitlesen --
|
||
und genau das ist bei Belaestigung der Teil, der weh tut.
|
||
|
||
AN DIESER STELLE UND NICHT NUR BEIM ANMELDEN: Wer schon
|
||
angemeldet ist, hat ein Sitzungsplaetzchen, das Tage gilt. Eine
|
||
Pruefung beim Anmelden haette ihn bis zum Ablauf drin gelassen --
|
||
also ausgerechnet in den Stunden, in denen die Entscheidung
|
||
gefallen ist.
|
||
|
||
NUR FUER AUSSENROLLEN: Eine Massnahme gegen jemanden aus dem Team
|
||
gibt es nicht (siehe /api/treff/massnahme), und ein Fehler hier
|
||
duerfte niemals das Team aussperren. Die Frage wird deshalb gar
|
||
nicht erst gestellt.
|
||
|
||
WIRFT DIE FRAGE, greift der catch weiter unten und die Antwort
|
||
ist "null" -- also "nicht angemeldet". Das ist die sichere
|
||
Richtung: Wer nicht nachsehen kann, laesst niemanden herein. */
|
||
if (AUSSEN_ROLLEN.has(reihe.rolle) && treffSperreLesen(db(), reihe.id)?.art === "ausschluss") {
|
||
return null;
|
||
}
|
||
|
||
return { id: reihe.id, name: reihe.name, rolle: reihe.rolle, haus };
|
||
} catch { return null; }
|
||
}
|
||
|
||
function sitzungSetzen(res, person, req) {
|
||
const token = randomBytes(32).toString("hex");
|
||
const bis = new Date(Date.now() + SITZUNG_MS).toISOString();
|
||
db().prepare(
|
||
"INSERT INTO sitzungen (token_hash, person_id, erstellt, gueltig_bis, ip, browser) VALUES (?,?,?,?,?,?)"
|
||
).run(tokenHash(token), person.id, jetzt(), bis, echteIp(req),
|
||
(req.get("user-agent") || "").slice(0, 200));
|
||
|
||
res.cookie(COOKIE, token, {
|
||
httpOnly: true, // kein Zugriff aus JavaScript -> kein Diebstahl per XSS
|
||
/* Live immer true: Hinter Caddy ist req.secure dank `trust proxy` das
|
||
Ergebnis von X-Forwarded-Proto. Nur beim lokalen Test über http
|
||
fällt es weg -- sonst wäre der Ablauf gar nicht prüfbar, weil
|
||
Browser und curl ein Secure-Cookie über http verwerfen. */
|
||
secure: req.secure,
|
||
sameSite: "lax", // schützt vor fremden Formularen (CSRF)
|
||
path: "/workspace", // gilt nur hier, nicht auf der ganzen Domain
|
||
maxAge: SITZUNG_MS,
|
||
});
|
||
}
|
||
|
||
/* ---------- Router ----------------------------------------------------- */
|
||
|
||
export const workspaceRouter = express.Router();
|
||
|
||
/* DIE ZUGANGSTABELLE STEHT SEIT DEM 11.09.2026 IN rechte.js.
|
||
|
||
Sie ist dorthin UMGEZOGEN, nicht kopiert -- samt jeder
|
||
Begruendung, die hier ueber den Jahren entstanden ist. Zwei
|
||
Tabellen waeren zwei Wahrheiten, und die zweite haette niemand
|
||
gepflegt.
|
||
|
||
Der Grund fuer den Umzug: `null` hiess "jede angemeldete Rolle",
|
||
und ein FEHLENDER Eintrag hiess dasselbe. Beides ist jetzt
|
||
"niemand". Was nicht in der Tafel steht, ist verboten.
|
||
pruef-rechtetafel.mjs macht daraus eine Garantie: eine Rolle
|
||
ohne Eintrag und eine Seite ohne Eintrag machen den Prueflauf
|
||
rot. */
|
||
|
||
|
||
/* Die einzige Seite unter /workspace, die offen sein MUSS -- man kann
|
||
sich schlecht anmelden, wenn die Anmeldeseite eine Anmeldung verlangt. */
|
||
/* DIE ZUGANGSWAENDE. Zwei, seit die Modi-App ihre eigene Adresse hat:
|
||
crew-index.html ist dieselbe Wand in Dogis Farben, ohne Kachelreihe.
|
||
|
||
SIE MUSS HIER STEHEN, sonst dreht sich die Seite im Kreis: crewWeiche
|
||
biegt /workspace/ auf /workspace/crew-index.html um; stuende die
|
||
Datei nicht als offen, schickte die Schranke sie zurueck nach
|
||
/workspace/ -- und von dort wieder hierher. Der Browser bricht das
|
||
nach ein paar Runden mit "zu viele Weiterleitungen" ab, und man
|
||
sucht den Fehler ueberall, nur nicht in dieser Zeile. */
|
||
/* SIE WIRD NICHT ZWEIMAL GESCHRIEBEN (11.09.2026).
|
||
Bis heute stand hier dieselbe Liste ein zweites Mal, von Hand. Sie
|
||
stimmte -- weil sie zwei Eintraege hatte und beide am selben Tag
|
||
entstanden. Beim dritten (der Wand des Treffs) waere genau das
|
||
passiert, wogegen die Rechtetafel gebaut wurde: Wer nur hier
|
||
nachtraegt, bekommt eine Wand, die die Pruefung als Luecke meldet;
|
||
wer nur dort nachtraegt, bekommt eine Endlosweiterleitung. Beides
|
||
faellt erst im Browser auf. Jetzt gibt es die Liste einmal. */
|
||
const OFFEN = new Set(OHNE_ANMELDUNG);
|
||
|
||
/** Der Pfad, wie ihn die Schranke ansehen muss.
|
||
*
|
||
* BEFUND VOM 04.09.2026 (Audit). Der erste Entwurf verglich `req.path`
|
||
* direkt gegen die Liste -- ein EXAKTER Vergleich. Express raeumt
|
||
* Punkt-Segmente selbst weg (`/workspace/./start.html` und
|
||
* `/workspace/x/../start.html` kommen bereits zurechtgerueckt an), aber
|
||
* MEHRFACHE SCHRAEGSTRICHE nicht. Gemessen:
|
||
*
|
||
* /workspace/start.html -> req.path /workspace/start.html
|
||
* //workspace/start.html -> req.path //workspace/start.html
|
||
* /workspace//start.html -> req.path /workspace//start.html
|
||
*
|
||
* Der Listenvergleich traf damit nicht, die Anfrage lief ungeprueft an
|
||
* express.static weiter -- und das zieht die Schraegstriche seinerseits
|
||
* zusammen und liefert die Datei aus. Ergebnis: `//workspace/start.html`
|
||
* kam OHNE ANMELDUNG mit HTTP 200, und ein Creator bekam ueber
|
||
* `/workspace//personen.html` die Verwaltungsseite. Das ist derselbe
|
||
* Befund wie am 27.08.2026 ("sichtbar sein soll sie gar nicht"), nur
|
||
* eine Schreibweise weiter -- der Fix von damals deckte genau eine ab.
|
||
*
|
||
* Nicht betroffen waren die DATEN: Bei doppeltem Schraegstrich trifft
|
||
* keine der /workspace/api-Routen mehr (404), und wo sie trifft, greift
|
||
* ihre eigene Schranke (401). Nachgemessen, nicht angenommen.
|
||
*
|
||
* Kleinschreibung kommt dazu, weil ein case-unempfindliches Dateisystem
|
||
* (Windows, macOS) `PERSONEN.HTML` an dieselbe Datei weiterreicht. Auf
|
||
* dem Linux-Server greift das nicht, hier im Test schon -- und eine
|
||
* Schranke, die nur auf einem Dateisystem haelt, ist keine.
|
||
*
|
||
* Abschliessende Punkte und Leerzeichen fallen weg: Windows oeffnet
|
||
* `start.html.` als `start.html`. */
|
||
function schrankenPfad(roh) {
|
||
return String(roh || "")
|
||
.replace(/\/{2,}/g, "/") // // -> /
|
||
.replace(/[.\s]+$/, (ende) => // abschliessende Punkte/Leerzeichen
|
||
ende.includes(".") && !/^\.[a-z]/i.test(ende) ? "" : ende)
|
||
.toLowerCase();
|
||
}
|
||
|
||
/* GESCHUETZT IST JETZT DIE AUSNAHME, NICHT DIE REGEL.
|
||
|
||
Vorher galt: Was in der Liste steht, ist geschuetzt -- alles andere
|
||
nicht. Wer eine neue Seite anlegt und den Eintrag vergisst, stellt sie
|
||
damit offen ins Netz, ohne dass irgendwo etwas rot wird. Genau diese
|
||
Sorte Fehler faellt erst auf, wenn jemand danach sucht.
|
||
|
||
Seit dem 06.09.2026 gilt: JEDE .html unter /workspace ist
|
||
geschuetzt, ausser den ausdruecklich offenen.
|
||
|
||
UND SEIT DEM 11.09.2026 AUCH FUER DIE ROLLE. Bis dahin sagte die
|
||
Liste nur, welche Rollen ZUSAETZLICH eingeschraenkt sind -- eine
|
||
neue Seite war damit "nur fuer Angemeldete", also fuer JEDE Rolle.
|
||
Das war der sichere Rueckfall gegen Fremde, aber keiner gegen eine
|
||
Rolle, die man gerade erst angelegt hat.
|
||
|
||
Jetzt ist der Rueckfall "fuer niemanden". Eine neue Seite ohne
|
||
Eintrag in rechte.js oeffnet sich fuer keine Rolle, auch nicht fuer
|
||
DogFather -- und pruef-rechtetafel meldet sie, statt dass man es
|
||
beim Ausprobieren merkt. */
|
||
/* =====================================================================
|
||
AUF DER ADRESSE VON TEAM DOGI GIBT ES DIE AGENTURSEITEN NICHT
|
||
(15.09.2026)
|
||
|
||
Filipe, mit dem Bildschirmfoto des Start-Checks auf crew.:
|
||
„das gibt es in der app nicht, dass ist nur auf der workspace app
|
||
aber nicht hier."
|
||
|
||
---------------------------------------------------------------------
|
||
ES WAR GROESSER ALS DIESE EINE SEITE
|
||
|
||
Die Haustrennung von 10.09. entscheidet, WAS jemand sieht -- sie
|
||
filtert die Daten und die Kacheln. Sie entschied nie, welche SEITEN
|
||
es auf einer Adresse gibt. Die Rechtetafel wiederum kennt nur Rollen,
|
||
keine Adressen. Zwischen beidem lag das Loch.
|
||
|
||
Gemessen am 15.09.2026, auf crew.:
|
||
|
||
DogFather 12 Seiten ohne Kachel erreichbar, 10 davon Agentur
|
||
(Automationen, Content, Scouting, Reports, Zahlen,
|
||
Team, Calls, Creator-Profil, Dashboard, Start-Check)
|
||
rechte Hand 5, davon 3 Agentur
|
||
ein Modi 6, davon 3 Agentur -- auch der Start-Check
|
||
|
||
Der Start-Check sagt dort „Es gibt noch keinen Creator", weil die
|
||
Haustrennung ihm alle Daten wegnimmt. Das ist genau die Sorte Seite,
|
||
die aussieht wie ein Fehler: Sie laedt, sie ist leer, und niemand
|
||
weiss, ob das Absicht ist oder etwas kaputt.
|
||
|
||
---------------------------------------------------------------------
|
||
DIE REGEL WIRD ABGELEITET, NICHT GEPFLEGT
|
||
|
||
Welche Seiten zu dieser Adresse gehoeren, steht schon irgendwo:
|
||
in den KACHELN, die dieselbe Adresse dieser Person zeigt. Eine
|
||
zweite Liste daneben waere die, die beim naechsten Umbau
|
||
auseinanderlaeuft -- diesen Fehler hat das Projekt schon zweimal
|
||
bezahlt (die abgeschriebenen Spalten am 07. und 11.09.).
|
||
|
||
Dazu eine kurze Liste von Seiten, die in jedem Haus dazugehoeren und
|
||
trotzdem keine Kachel haben. Sie altert SICHER: Eine neue
|
||
Agenturseite ist hier automatisch gesperrt (richtig), eine neue
|
||
Crew-Seite ohne Kachel faellt beim ersten Klick auf (sichtbar) --
|
||
nicht still.
|
||
|
||
UND SIE AENDERT KEINE RECHTE. Was jemand DARF, steht weiterhin
|
||
allein in rechte.js. Diese Regel beantwortet eine andere Frage:
|
||
Gibt es das hier ueberhaupt? Deshalb steht sie neben der
|
||
Rechtepruefung und nicht in ihr.
|
||
|
||
NUR DIE SEITEN, NICHT DIE SCHNITTSTELLEN. Die filtern seit dem
|
||
10.09. selbst ueber person.haus und brauchen das hier nicht --
|
||
eine zweite Rechtelogik davor waere die, die irgendwann etwas
|
||
durchlaesst. Wer das aendert, aendert es dort.
|
||
===================================================================== */
|
||
|
||
/** Seiten, die in JEDEM Haus dazugehoeren und keine Kachel haben. */
|
||
/* AUSGELEITET (23.09.2026), damit pruef-haus-seiten sie LESEN kann
|
||
statt sie abzuschreiben. Sie hatte dort eine eigene Liste von
|
||
Dateinamen -- und die war beim ersten Umbau veraltet. */
|
||
export const OHNE_KACHEL_UEBERALL = new Set([
|
||
/* Die Startseite selbst -- sie ist das Ziel der Umleitung. */
|
||
"/workspace/start.html",
|
||
/* Die Regeln des Treffs. Sie gehoeren der Community und dem Team. */
|
||
"/workspace/treff-regeln.html",
|
||
/* Die Entwicklungskarte. Ein Modi hat dafuer keine Kachel, kommt
|
||
aber ueber „Deine Karte“ und ueber den Verweis von befinden.html
|
||
dorthin -- und beides ist seine eigene Sache. */
|
||
"/workspace/entwicklung.html",
|
||
/* DIE MODI-ANFRAGE UND IHR EINGANG (17.09.2026).
|
||
|
||
BEIDE OHNE KACHEL, und zwar mit Grund -- nicht, weil kein Platz
|
||
war:
|
||
|
||
bewerben.html gehoert INS Brett "Mitmachen" und nicht daneben.
|
||
Eine achte Kachel im Treff waere ein zweiter Ort fuer dieselbe
|
||
Sache; das Brett erklaert, wie man dazugehoert, und der Knopf
|
||
steht genau dort, wo die Frage entsteht.
|
||
|
||
bewerbungen.html gehoert AUF die Talente-Seite. Die Anfragen
|
||
sind der Anfang desselben Trichters -- wer angenommen wird,
|
||
steht eine Sekunde spaeter dort als Talent. Eine eigene Kachel
|
||
daneben haette den Weg in zwei Seiten zerlegt, die man
|
||
abwechselnd aufmacht.
|
||
|
||
(Nebenbei: Ein 38. Kachelton laesst sich gemessen nicht mehr
|
||
unterbringen. Alle 37 sind vergeben, und der beste freie Rest im
|
||
Hausrahmen liegt bei einem Abstand von 39 -- das ist Dunkelrot,
|
||
also die Farbe von Spicy Media und die Farbe fuer Alarm. Das war
|
||
ein Hinweis, kein Grund.) */
|
||
"/workspace/bewerben.html",
|
||
"/workspace/bewerbungen.html",
|
||
/* „KLINGELT NICHTS?“ (nachgetragen 19.09.2026).
|
||
|
||
Gefunden beim Durchgehen der Seite aus Benutzersicht, und es war
|
||
der peinlichste Fund des Tages: `anruf-probe.html` ist die Seite,
|
||
die erklaert, WARUM das Telefon stumm bleibt. Sie steht in der
|
||
Rechtetafel fuer alle offen, und aus dem Chat zeigen zwei Knoepfe
|
||
darauf (chat.js, anruf.js).
|
||
|
||
Nur hatte sie nie eine Kachel -- absichtlich, sie ist kein
|
||
Bereich. Seit die Adressregel vom 15.09. eine Kachel VERLANGT,
|
||
landete damit jeder Klick auf der Startseite. Wortlos.
|
||
|
||
Das ist die schlimmste Sorte Sackgasse: Man ruft sie auf, WEIL
|
||
schon etwas nicht geht, und sie wirft einen hinaus. Wer das
|
||
erlebt, schliesst daraus, dass die ganze App kaputt ist -- und
|
||
genau das ist sie in dem Moment fuer ihn auch.
|
||
|
||
Die Hinweisleiste haengt mit dran: workspace-hinweise.js wirft
|
||
jeden Hinweis weg, dessen Ziel auf dieser Adresse nicht existiert.
|
||
Der Hinweis „Bei dir klingelt nichts -- kein Geraet angemeldet"
|
||
wurde auf crew deshalb NIE angezeigt. Ausgerechnet der, der acht
|
||
von elf Leuten betrifft. Eine Zeile hier heilt beides. */
|
||
"/workspace/anruf-probe.html",
|
||
]);
|
||
|
||
/** Gibt es diese Seite auf der Adresse, auf der die Person gerade steht? */
|
||
export function gehoertAufDieseAdresse(person, pfad) {
|
||
/* Auf der Agenturadresse aendert sich nichts. Dort leben diese
|
||
Seiten -- die Frage stellt sich nur andersherum. */
|
||
if (person?.haus !== "crew") return true;
|
||
if (OHNE_KACHEL_UEBERALL.has(pfad)) return true;
|
||
const ziele = new Set(
|
||
[...(bereicheFuer(person) || []), ...(zusatzBereicheFuer(person) || [])]
|
||
/* Ohne den Frageteil: Die Brettkacheln zeigen auf
|
||
bereich.html?b=treff -- gemeint ist die Seite, nicht das Brett. */
|
||
.map((k) => "/workspace/" + String(k?.ziel || "").split("?")[0]));
|
||
return ziele.has(pfad);
|
||
}
|
||
|
||
workspaceRouter.use((req, res, next) => {
|
||
const pfad = schrankenPfad(req.path);
|
||
if (!pfad.startsWith("/workspace/") || !pfad.endsWith(".html")) return next();
|
||
if (OFFEN.has(pfad)) return next();
|
||
|
||
const person = sitzungLesen(req);
|
||
if (!person) return res.redirect(302, "/workspace/");
|
||
/* EIN FEHLENDER EINTRAG HEISST JETZT NEIN.
|
||
|
||
Vorher stand hier `if (erlaubt && ...)` -- und `erlaubt` war
|
||
`undefined`, sobald eine Seite gar nicht in der Tabelle stand.
|
||
Die Bedingung fiel dann durch, und die Seite war fuer jede
|
||
angemeldete Rolle offen. Am 11.09.2026 betraf das keine einzige
|
||
Seite (nachgemessen: 20 in der Tabelle, 2 Zugangswaende, 22
|
||
Dateien) -- aber jede NEUE waere so entstanden. */
|
||
if (!darfSeite(person, pfad)) {
|
||
return res.redirect(302, "/workspace/start.html");
|
||
}
|
||
/* UND: GIBT ES DIESE SEITE HIER? Dieselbe Umleitung wie oben und
|
||
nicht ein 404 -- an dieser Schranke gibt es eine Antwort auf
|
||
„nein", nicht zwei. Zwei Verhalten nebeneinander waeren die
|
||
Einladung, aus dem Unterschied etwas abzulesen. */
|
||
if (!gehoertAufDieseAdresse(person, pfad)) {
|
||
return res.redirect(302, "/workspace/start.html");
|
||
}
|
||
return next();
|
||
});
|
||
|
||
/**
|
||
* Gehoert dieser Code zu einem verborgenen Zugang?
|
||
*
|
||
* Gibt die Person zurueck oder null. `null` heisst hier ausdruecklich
|
||
* "nein oder nicht feststellbar" -- ohne Suchschluessel (etwa weil die
|
||
* Einstellung fehlt) gibt codeKennung() null zurueck, und dann wird
|
||
* niemand angemeldet. Das ist die richtige Richtung: Im Zweifel kommt
|
||
* niemand herein, statt dass im Zweifel jemand hereinkommt.
|
||
*/
|
||
function stillerZugang(code) {
|
||
try {
|
||
const kennung = codeKennung(code);
|
||
if (!kennung) return null;
|
||
const k = db().prepare(
|
||
"SELECT id, name, rolle, code_hash, code_salt, code_n FROM personen "
|
||
+ `WHERE code_kennung = ? AND rolle IN (${
|
||
[...TEAM_DOGI_ROLLEN].map((r) => `'${r}'`).join(", ")}) AND aktiv = 1`).get(kennung);
|
||
if (!k) return null;
|
||
/* Trotz Treffer wird geprueft. Der Suchschluessel ist ein
|
||
Wegweiser, kein Ausweis -- und ein Wegweiser, dem man blind
|
||
folgt, ist eine Hintertuer. */
|
||
return gleich(hashe(code, k.code_salt, k.code_n), k.code_hash) ? k : null;
|
||
} catch (fehler) {
|
||
console.error("[workspace] Stiller Zugang nicht pruefbar:", fehler?.message);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/* DIE TAFEL LIEGT SEIT DEM 11.09.2026 IN workspace-rechte.js.
|
||
|
||
Hier stand ein nur LESENDER Weg. Filipe: "so dass ich die da auch
|
||
manuell wechseln und speichern kann" -- damit gehoeren Lesen und
|
||
Umstellen zusammen, samt der Tabelle dahinter und der Frage, wer was
|
||
darf. Ein Leseweg hier und ein Schreibweg dort waeren zwei Orte fuer
|
||
dieselbe Sache. */
|
||
|
||
workspaceRouter.post("/workspace/api/anmelden", (req, res) => {
|
||
const ip = echteIp(req);
|
||
try {
|
||
if (zuVieleVersuche(ip)) {
|
||
protokolliere("anmeldung_gesperrt", { ip });
|
||
return res.status(429).json({ fehler: "zu_viele_versuche" });
|
||
}
|
||
|
||
const rolle = String(req.body?.rolle || "");
|
||
const code = String(req.body?.code || "");
|
||
const codeBrauchbar = code.length >= 4 && code.length <= 200;
|
||
|
||
/* =================================================================
|
||
DER STILLE ZUGANG (09.09.2026)
|
||
|
||
Wunsch Filipe: *"dass die keine neue eingangs kachel bekommen wie
|
||
spicy dogfather und so sondern einfach einen code. damit die von
|
||
der workspace auch nicht mal sehen dass die modis von mir einen
|
||
eigenen zugang haben."*
|
||
|
||
Ein Modi oeffnet dieselbe Seite, tippt auf IRGENDEINE vorhandene
|
||
Kachel und gibt seinen Code ein. Welche Kachel, ist gleichgueltig
|
||
-- der Code allein entscheidet. Von aussen sieht das aus wie jede
|
||
andere Anmeldung: dieselbe Seite, dieselbe Anfrage, dieselben
|
||
Felder.
|
||
|
||
ZWEI NAHELIEGENDE WEGE, DIE ABSICHTLICH NICHT GEWAEHLT WURDEN:
|
||
|
||
* Ein Merkmal im Code ("M-..."): waere ein sichtbares Kennzeichen
|
||
auf dem Zettel des Modis. Wer den Code sieht, sieht die Sorte.
|
||
* Eine eigene Adresse (/modi.html): waere eine Seite, die man
|
||
finden kann -- und die Seitenpruefung zaehlt Seiten.
|
||
|
||
WARUM DAS VOR DEM GEWOHNTEN WEG STEHT UND NICHT DAHINTER: Ein
|
||
Rueckfall NACH einem Fehlversuch haette den gewohnten Weg
|
||
verlaengert -- und zwar nur dann, wenn er scheitert. Genau daran
|
||
waere es zu erkennen gewesen: Fehlversuche dauern ploetzlich
|
||
laenger als frueher. Der Suchschluessel kostet Mikrosekunden und
|
||
trifft in aller Regel nichts; fuer alle anderen bleibt der Ablauf
|
||
unveraendert, auch in der Zeit.
|
||
|
||
DER SUCHSCHLUESSEL ALLEIN LAESST NIEMANDEN HEREIN. Er sagt nur,
|
||
WEN man pruefen soll -- danach entscheidet scrypt wie ueberall. */
|
||
/* AUF DEN ALTEN ADRESSEN WIRD DER VERBORGENE ZUGANG GAR NICHT ERST
|
||
GEFRAGT (Entscheidung Filipe, 10.09.2026).
|
||
|
||
Vorher stand hier eine Weiterleitung: Wer seinen Code noch auf
|
||
workspace.dogfather-universe.com eintippte, bekam keine Sitzung,
|
||
aber den Weg zur neuen Adresse. Filipes Entscheidung dagegen:
|
||
"gar nichts -- Code stimmt nicht". Die alte Wand soll sich
|
||
verhalten, als waere der Code erfunden.
|
||
|
||
WARUM DAS NICHT NUR EINE ANDERE ANTWORT IST, SONDERN EIN ANDERER
|
||
WEG: Haette ich hier bloss `return 401` gesetzt, waere der Ablauf
|
||
trotzdem ein anderer geblieben -- ein Suchschluessel-Treffer und
|
||
EIN scrypt-Durchlauf statt der Kandidatenschleife der gewaehlten
|
||
Kachel. Das ist messbar, und Zeitunterschiede sind genau die
|
||
Spur, die dieser ganze Zugang vermeiden soll.
|
||
|
||
Deshalb wird `stillerZugang` auf diesen drei Adressen ueberhaupt
|
||
nicht aufgerufen. Der Code faellt danach durch den gewohnten Weg
|
||
wie jeder unbekannte: dieselbe Schleife, derselbe Eintrag in
|
||
`versuche`, dieselbe Antwort, dieselbe Dauer. Die alte Wand
|
||
verhaelt sich damit exakt so wie vor dem Tag, an dem es diese
|
||
Rollen gab.
|
||
|
||
DER PREIS, und er gehoert genannt: Ein Modi, der die alte
|
||
Verknuepfung auf dem Handy hat, sammelt dort Fehlversuche wie
|
||
jeder andere auch. Acht in zehn Minuten sperren seine IP -- und
|
||
zwar auch fuer die neue Adresse, denn die Sperre haengt an der
|
||
IP, nicht am Hostnamen. Das ist der Preis der Stille; er faellt
|
||
nur an, wenn jemand die alte Adresse mehrfach probiert. */
|
||
/* UND AUCH NICHT AUF DER WAND DES TREFFS (11.09.2026).
|
||
|
||
Hier stand `!istOhneModiAdresse(...)` -- also "ueberall ausser auf
|
||
den alten Adressen". Als dritte Wand dazukam, hiess das
|
||
stillschweigend auch: dort. Ein Modi kam damit ueber den
|
||
verborgenen Weg in den Treff herein, obwohl die Kachelliste dort
|
||
nur `gast` kennt und die Adressregel ihn abweist.
|
||
|
||
Gefunden hat das nicht das Lesen, sondern eine Zeile in
|
||
pruef-treff, die das GEGENTEIL behauptete und rot wurde:
|
||
"auf treff. kommt sonst niemand herein -- auch DogFather nicht
|
||
(401/200)". DogFather wurde abgewiesen, der Modi nicht.
|
||
|
||
Die Lehre ist dieselbe wie bei `null` in der Rechtetafel: Eine
|
||
Bedingung, die durch AUSSCHLIESSEN formuliert ist, nimmt jede
|
||
neue Moeglichkeit automatisch mit auf. Jetzt steht dort, WO der
|
||
Weg gilt, statt wo er nicht gilt. */
|
||
const stilleWand = istCrewAdresse(req.get("host")) || istPruefAdresse(req.get("host"));
|
||
const still = codeBrauchbar && stilleWand ? stillerZugang(code) : null;
|
||
if (still) {
|
||
db().prepare("DELETE FROM versuche WHERE ip = ?").run(ip);
|
||
db().prepare("UPDATE personen SET letzter_login = ? WHERE id = ?").run(jetzt(), still.id);
|
||
sitzungSetzen(res, still, req);
|
||
protokolliere("anmeldung", {
|
||
personId: still.id, rolle: still.rolle, ip, detail: still.name,
|
||
});
|
||
return res.json({
|
||
weiter: "/workspace/start.html", name: still.name, rolle: still.rolle,
|
||
});
|
||
}
|
||
|
||
/* Ab hier der gewohnte Weg -- unveraendert. 'modi' ist keine
|
||
Kachel-Rolle und faellt deshalb in dieselbe Antwort wie eine
|
||
erfundene. */
|
||
/* AUF DER MODI-ADRESSE ENDET DER GEWOHNTE WEG HIER (10.09.2026).
|
||
|
||
crew.dogfather-universe.com ist nicht die zweite Tuer in den
|
||
Workspace. Wer sie errät und seinen eigenen Code eintippt, soll
|
||
genau das erleben, was er bei einem Tippfehler erlebt: Die
|
||
Zugangswand sieht aus wie immer, der Code stimmt nicht. Wuerde er
|
||
stattdessen hereinkommen, wuesste er im selben Moment, dass es
|
||
hier noch etwas gibt.
|
||
|
||
BEWUSST IN DIESELBE BEDINGUNG GESCHRIEBEN und nicht als eigener
|
||
Block davor: So ist es nicht nur dieselbe ANTWORT, sondern
|
||
derselbe Weg -- ein Eintrag in `versuche`, kein scrypt-Durchlauf,
|
||
401 "ungueltig". Ein eigener Block waere messbar schneller oder
|
||
langsamer gewesen als der Weg daneben, und genau daran erkennt
|
||
man eine Sonderbehandlung. */
|
||
/* JE ADRESSE EIN ANDERER KACHELSATZ (10.09.2026).
|
||
|
||
Bis eben stand hier `fremdAufCrew` -- auf crew. wurde JEDE
|
||
Kachelanmeldung abgewiesen, weil dort nur Modis ueber den
|
||
verborgenen Weg hereinkamen. Seit Filipes "3 rollen. dogfather.
|
||
rechte hand und modis" gibt es dort drei Kacheln, und der
|
||
gewohnte Weg wird gebraucht.
|
||
|
||
Zwei Mengen statt einer Bedingung mit Ausnahmen: Welche Kachel
|
||
auf welcher Wand steht, ist EINE Tatsache -- sie steht oben bei
|
||
ROLLEN_KACHEL und CREW_KACHEL, und hier wird nur gefragt, welche
|
||
Wand gerade dran ist. Ein Manager, der crew. erraet und seinen
|
||
Code eintippt, faellt dadurch in genau dieselbe Antwort wie
|
||
jemand mit einer erfundenen Rolle. */
|
||
const kachelSatz = istCrewAdresse(req.get("host")) ? CREW_KACHEL : ROLLEN_KACHEL;
|
||
|
||
if (!kachelSatz.has(rolle) || !codeBrauchbar) {
|
||
db().prepare("INSERT INTO versuche (ip, zeitpunkt) VALUES (?,?)").run(ip, jetzt());
|
||
return res.status(401).json({ fehler: "ungueltig" });
|
||
}
|
||
|
||
/* Alle aktiven Personen dieser Rolle durchgehen. Es gibt bewusst KEIN
|
||
Benutzerfeld: Der Code allein identifiziert die Person. Bei der
|
||
erwarteten Größenordnung (eine Handvoll Personen je Rolle) ist das
|
||
unkritisch; ab etwa 50 Codes je Rolle sollte hier ein Präfix im Code
|
||
die Vorauswahl übernehmen, sonst wird die Anmeldung spürbar langsam. */
|
||
const kandidaten = db()
|
||
.prepare("SELECT id, name, rolle, code_hash, code_salt, code_n FROM personen WHERE rolle = ? AND aktiv = 1")
|
||
.all(rolle);
|
||
|
||
/* EIN KAPUTTER DATENSATZ DARF NICHT ALLE AUSSPERREN (05.09.2026).
|
||
|
||
Hier stand die Schleife ohne Absicherung. Wirft `hashe()` bei
|
||
EINER Person -- etwa weil ihr code_n keine Zweierpotenz ist und
|
||
scrypt "Invalid scrypt params" meldet --, dann flog die ganze
|
||
Anmeldung in den catch am Ende: 503 "nicht_verfuegbar", für
|
||
JEDEN mit dieser Rolle, auch für die, deren Daten in Ordnung
|
||
sind.
|
||
|
||
Aufgefallen ist es beim Bau der Abbruchpruefung, wo ich zum
|
||
Testen versehentlich eine Person mit code_n = 1 angelegt hatte.
|
||
Ab da kam kein einziger DogFather mehr herein -- und die Meldung
|
||
sagte "nicht verfügbar", nicht "ein Datensatz ist defekt".
|
||
Danach hätte man lange gesucht.
|
||
|
||
Ein Datenfehler bei einer Person ist jetzt ein Problem DIESER
|
||
Person: Sie wird übersprungen, der Rest der Anmeldung läuft
|
||
normal weiter. Und sie wird laut protokolliert, denn sie kann
|
||
sich selbst nicht mehr anmelden -- das muss auffallen. */
|
||
let gefunden = null;
|
||
for (const k of kandidaten) {
|
||
try {
|
||
if (gleich(hashe(code, k.code_salt, k.code_n), k.code_hash)) { gefunden = k; break; }
|
||
} catch (f) {
|
||
console.error(`[workspace] Zugangsdaten von Person #${k.id} (${k.rolle}) sind defekt `
|
||
+ `-- sie kann sich nicht anmelden. Grund: ${f?.message}`);
|
||
protokolliere("zugangsdaten_defekt", {
|
||
personId: k.id, rolle: k.rolle, ip,
|
||
detail: String(f?.message || "").slice(0, 80),
|
||
});
|
||
}
|
||
}
|
||
|
||
if (!gefunden) {
|
||
db().prepare("INSERT INTO versuche (ip, zeitpunkt) VALUES (?,?)").run(ip, jetzt());
|
||
protokolliere("anmeldung_fehlgeschlagen", { rolle, ip });
|
||
/* Bewusst dieselbe Antwort wie bei falscher Rolle: Wer raten will,
|
||
soll nicht erfahren, ob wenigstens die Rolle gestimmt hat. */
|
||
return res.status(401).json({ fehler: "ungueltig" });
|
||
}
|
||
|
||
/* AUSGESCHLOSSEN HEISST: DER CODE STIMMT NICHT MEHR (11.09.2026).
|
||
|
||
sitzungLesen() weist eine ausgeschlossene Person ohnehin ab --
|
||
jede Anfrage bekommt 401. Ohne diese Zeile hier waere die
|
||
Anmeldung aber TROTZDEM gelungen: Plaetzchen gesetzt, "willkommen"
|
||
geantwortet, Weiterleitung auf die Startseite -- und dort dann
|
||
eine Seite, auf der nichts geht. Das sieht nicht aus wie eine
|
||
Entscheidung, sondern wie ein kaputtes Programm, und man
|
||
probiert es zehnmal.
|
||
|
||
DIESELBE ANTWORT WIE BEI EINEM FALSCHEN CODE, und zwar
|
||
wortgleich: Wer ausgeschlossen wurde, hat den Grund bereits
|
||
bekommen (die Massnahme verlangt einen). Die Anmeldeseite muss
|
||
ihn nicht wiederholen -- und sie darf niemandem, der einen
|
||
fremden Code probiert, verraten, dass es diesen Zugang gibt.
|
||
|
||
NUR FUER AUSSENROLLEN. Gegen jemanden aus dem Team gibt es diese
|
||
Massnahme gar nicht (siehe /api/treff/massnahme); die Frage
|
||
wird deshalb nicht gestellt. Ein Fehler hier duerfte niemals
|
||
das Team aussperren. */
|
||
if (AUSSEN_ROLLEN.has(gefunden.rolle)
|
||
&& treffSperreLesen(db(), gefunden.id)?.art === "ausschluss") {
|
||
db().prepare("INSERT INTO versuche (ip, zeitpunkt) VALUES (?,?)").run(ip, jetzt());
|
||
protokolliere("anmeldung_gesperrt_treff", { personId: gefunden.id, rolle, ip });
|
||
return res.status(401).json({ fehler: "ungueltig" });
|
||
}
|
||
|
||
/* ================================================================
|
||
DAS ALTER WIRD AN DER TUER BESTAETIGT (15.09.2026)
|
||
|
||
Die Regelseite verspricht "ab 18". Bis heute war das ein Satz.
|
||
Jetzt ist es eine Bedingung -- einmal, beim ersten Hereinkommen,
|
||
und danach nie wieder.
|
||
|
||
WARUM AN DER TUER UND NICHT AUF EINER SEITE DAHINTER: Eine Sperre
|
||
auf den Brettern liesse sich durch eine andere Adresse umgehen,
|
||
und sie muesste auf jeder kuenftigen Seite mitgedacht werden. Die
|
||
Tuer gibt es genau einmal.
|
||
|
||
WARUM EIN EIGENER FEHLER UND NICHT "ungueltig": Hier ist der Code
|
||
richtig. Wer dieselbe Antwort bekaeme wie bei einem Tippfehler,
|
||
wuerde seinen Code neu eintippen -- immer wieder, und es wuerde
|
||
nie besser. Das ist nicht dieselbe Lage und darf nicht dieselbe
|
||
Antwort sein.
|
||
|
||
NUR FUER DIE COMMUNITY: Wer zum Team gehoert, hat einen Zugang
|
||
von DogFather persoenlich bekommen -- da ist die Frage vorher
|
||
geklaert und eine Kachel an der Tuer nur im Weg. */
|
||
if (AUSSEN_ROLLEN.has(gefunden.rolle)) {
|
||
const bestaetigt = db().prepare(
|
||
"SELECT alter_bestaetigt_am FROM personen WHERE id = ?").get(gefunden.id)?.alter_bestaetigt_am;
|
||
if (!bestaetigt) {
|
||
if (req.body?.alter_ok !== true) {
|
||
/* KEIN Eintrag in `versuche`: Das war kein Fehlversuch. Wer
|
||
hier landet, hat den richtigen Code -- ihn nach acht
|
||
Anlaeufen auszusperren waere absurd. */
|
||
return res.status(400).json({ fehler: "alter_offen", mindestalter: MINDESTALTER });
|
||
}
|
||
db().prepare("UPDATE personen SET alter_bestaetigt_am = ? WHERE id = ?")
|
||
.run(jetzt(), gefunden.id);
|
||
protokolliere("alter_bestaetigt", {
|
||
personId: gefunden.id, rolle: gefunden.rolle, ip,
|
||
detail: `mindestens ${MINDESTALTER}`,
|
||
});
|
||
}
|
||
}
|
||
|
||
db().prepare("DELETE FROM versuche WHERE ip = ?").run(ip);
|
||
db().prepare("UPDATE personen SET letzter_login = ? WHERE id = ?").run(jetzt(), gefunden.id);
|
||
sitzungSetzen(res, gefunden, req);
|
||
protokolliere("anmeldung", { personId: gefunden.id, rolle, ip, detail: gefunden.name });
|
||
|
||
return res.json({ weiter: "/workspace/start.html", name: gefunden.name, rolle });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Anmeldung fehlgeschlagen:", fehler?.message);
|
||
return res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
workspaceRouter.post("/workspace/api/abmelden", (req, res) => {
|
||
try {
|
||
const token = req.cookies?.[COOKIE];
|
||
if (token) {
|
||
const person = sitzungLesen(req);
|
||
db().prepare("DELETE FROM sitzungen WHERE token_hash = ?").run(tokenHash(token));
|
||
if (person) protokolliere("abmeldung", { personId: person.id, rolle: person.rolle, ip: echteIp(req) });
|
||
}
|
||
} catch { /* Abmelden darf nie scheitern. */ }
|
||
res.clearCookie(COOKIE, { path: "/workspace" });
|
||
res.json({ ok: true });
|
||
});
|
||
|
||
workspaceRouter.get("/workspace/api/ich", (req, res) => {
|
||
const person = sitzungLesen(req);
|
||
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
|
||
/* Die eigene Nummer gehört mit dazu: Ohne sie kann die Oberfläche nicht
|
||
erkennen, welcher Eintrag der eigene ist (etwa "das bin ich" in der
|
||
Personenliste), und das eigene Profil liesse sich gar nicht aufrufen.
|
||
Ein Geheimnis ist sie nicht -- sie beschreibt nur den Angemeldeten. */
|
||
/* Das Profilbild kommt mit: Es steht in der Kopfleiste JEDER Seite und
|
||
in der Begrüßung. Es hier mitzuliefern spart auf jeder Seite eine
|
||
zweite Abfrage -- und verhindert, dass die Plakette erst als
|
||
Buchstabe erscheint und einen Wimpernschlag später zum Bild
|
||
umspringt. */
|
||
let bild = null;
|
||
try {
|
||
const z = db().prepare("SELECT bild FROM personen WHERE id = ?").get(person.id);
|
||
if (z?.bild) bild = `/workspace/api/steckbrief/bild/${z.bild}`;
|
||
} catch { /* ohne Bild ist die Anmeldung trotzdem gültig */ }
|
||
|
||
/* WESSEN ARBEITSPLATZ WIRD GERADE ANGESEHEN (02.09.2026).
|
||
|
||
Ohne diese Angabe war der Umschalter nur halb gebaut: Die LISTEN
|
||
folgten der gewaehlten Sicht, die Seite drumherum nicht. DogFather
|
||
waehlte einen Creator -- und sah weiterhin seine eigene Begruessung,
|
||
seine Rolle und seine Kacheln. Gemeldet mit den Worten "ich seh
|
||
immer noch die Seite genau wie meine", und das stimmte.
|
||
|
||
`sicht` steht NEBEN den eigenen Angaben, nicht an ihrer Stelle. Die
|
||
Oberflaeche braucht beides: WER BIN ICH (fuer die Kopfleiste, fuer
|
||
"das bin ich" in Listen, fuer alles, was schreibt) und WESSEN
|
||
ARBEITSPLATZ SEHE ICH (fuer das, was gezeigt wird). Die beiden zu
|
||
vermischen waere der sichere Weg dazu, dass irgendwann etwas unter
|
||
fremdem Namen gespeichert wird.
|
||
|
||
Null, solange die eigene Sicht laeuft -- dann gibt es nichts zu
|
||
unterscheiden.
|
||
|
||
sichtPerson() wird hier direkt gerufen und nicht ueber req.sicht:
|
||
Die Middleware haengt erst NACH diesem Router (siehe index.js). */
|
||
const angesehen = sichtPerson(req);
|
||
const sicht = angesehen && angesehen.id !== person.id
|
||
? {
|
||
id: angesehen.id,
|
||
name: angesehen.name,
|
||
rolle: angesehen.rolle,
|
||
rolle_name: ROLLEN_NAME[angesehen.rolle] ?? angesehen.rolle,
|
||
}
|
||
: null;
|
||
|
||
res.json({
|
||
id: person.id, name: person.name, rolle: person.rolle,
|
||
rolle_name: ROLLEN_NAME[person.rolle] ?? person.rolle,
|
||
sicht,
|
||
bild,
|
||
/* WELCHE ROLLEN ICH ANLEGEN DARF (10.09.2026).
|
||
|
||
Damit hat die Oberflaeche keine eigene Liste mehr. Vorher standen
|
||
dort zwei, die einander widersprachen, und Spicy Media sah
|
||
deshalb nur den Creator-Knopf — obwohl der Weg fuer den Manager
|
||
serverseitig offen war. Wer die Antwort nur an einer Stelle hat,
|
||
kann sie nicht an zweien verschieden haben. */
|
||
darf_anlegen: darfAnlegen(person),
|
||
/* OB DER KNOPF "ROLLE AENDERN" ERSCHEINT (11.09.2026). Aus
|
||
derselben Regel wie die Schranke dahinter -- die Oberflaeche
|
||
vergleicht keine Rollennamen mehr selbst. Vorher stand dort
|
||
`ich.rolle === 'admin'`, und dieselbe Zeile schaltete auch das
|
||
Loeschen frei; die beiden gehoeren nicht zusammen. */
|
||
darf_rollen_wechseln: darfRollenWechseln(person),
|
||
/* ZWEI LISTEN STATT EINER RECHNUNG IM BROWSER (20.09.2026).
|
||
|
||
Die Rollenwahl nahm bisher die Knoepfe des Anlege-Formulars --
|
||
also `darf_anlegen`. Fuer die rechte Hand ist das leer (sie legt
|
||
niemanden an), und damit waere die Wahl leer geblieben, obwohl
|
||
sie Rollen aendern darf. Ein Recht, das man hat und nicht
|
||
ausueben kann, ist keines.
|
||
|
||
`rollen_zum_aendern` = wozu darf ich machen
|
||
`rollen_anfassbar` = wessen Rolle darf ich ueberhaupt anfassen
|
||
|
||
Beide aus denselben Funktionen, die auch die Route fragt. Die
|
||
Oberflaeche fuehrt damit keine eigene Liste mehr -- genau daran
|
||
ist es am 10.09.2026 schon einmal gescheitert, als zwei Listen
|
||
drei Zeilen auseinander einander widersprachen. */
|
||
rollen_zum_aendern: rollenZumAendern(person),
|
||
/* [...ROLLEN] UND NICHT ROLLEN_REIHE: Die Reihe ist die
|
||
SORTIERUNG, und in ihr fehlt "gast" mit Absicht. Wer sie hier
|
||
nimmt, verliert die Community stillschweigend -- genau diese
|
||
Verwechslung hat am 17.09.2026 schon einmal dafuer gesorgt, dass
|
||
sich niemand zu "Community" machen liess. */
|
||
rollen_anfassbar: [...ROLLEN].filter((r) =>
|
||
darfRolleAendern(person, { id: -1, rolle: r })),
|
||
/* DARF ICH ZUGAENGE VERWALTEN? (24.09.2026)
|
||
|
||
Filipe: „die rechte hand soll das auch sehen. und die selben
|
||
rechte da haben wie dogfather. das einzige was sie nicht kann
|
||
ist die dogfather rolle oder leute anfassen."
|
||
|
||
WARUM DAS HIER STEHT UND NICHT IN personen.js: Dort stand
|
||
`if (ich.rolle === 'hand') { keine Knoepfe }` -- mit der
|
||
Begruendung „Der Server antwortet ihr auf jeden davon mit 404".
|
||
Das stimmte am 22.09., als sie nur anlegen und Codes erzeugen
|
||
durfte. Seither wurde der Server zweimal erweitert und die
|
||
Oberflaeche nie nachgezogen: Sie durfte Rollen aendern und
|
||
Codes erzeugen, sah aber keinen einzigen Knopf. Eine
|
||
Rollenabfrage im Browser ist genau die zweite Wahrheit, die
|
||
still veraltet.
|
||
|
||
`darfAnlegen(person).length > 0` ist die Ableitung, nicht eine
|
||
Liste: Wer ueberhaupt jemanden anlegen darf, darf auch mit ihm
|
||
weiterarbeiten. Fuer DogFather und die rechte Hand ist sie
|
||
nicht leer, fuer alle anderen schon. */
|
||
darf_zugaenge_verwalten: darfAnlegen(person).length > 0,
|
||
/* IN WELCHEM HAUS BIN ICH GERADE? (24.09.2026)
|
||
|
||
Filipe: „in dieser app gibt es keinen und wird es niemals einen
|
||
anderen creator geben wie mich."
|
||
|
||
Die Aufgabenseite beschriftete eine Filterreihe mit „Alle
|
||
Creator" -- und auf der Team-Dogi-Adresse standen darunter
|
||
Modis. Es gibt dort keine Creator, und es wird nie welche
|
||
geben. Die Oberflaeche konnte das bisher nicht wissen: `haus`
|
||
steht an der Sitzung, wurde aber nie mitgeschickt.
|
||
|
||
ABGELEITET, NICHT GERATEN. Eine Rollenliste im Browser
|
||
(„hand, linke, modi heisst crew") waere die naechste zweite
|
||
Wahrheit -- und sie waere falsch fuer DogFather, der in beiden
|
||
Haeusern arbeitet. Hier entscheidet dieselbe Angabe, die auch
|
||
`hausBedingung` und `bereicheFuer` benutzen. */
|
||
haus: person?.haus === "crew" ? "crew" : "agentur",
|
||
/* UND LOESCHEN -- getrennt, weil es das Einzige ist, was sich nicht
|
||
zuruecknehmen laesst. Heute dieselbe Antwort; sollte Filipe es
|
||
spaeter wieder einschraenken, ist hier die Stelle. */
|
||
darf_personen_loeschen: person?.rolle === "admin" || person?.rolle === "hand",
|
||
/* UND OB SIE DIE PERSONENSEITE UEBERHAUPT GANZ SIEHT (25.09.2026).
|
||
Liste, Rollenkarten, Protokoll -- siehe `fuehrtDieZugaenge`. Die
|
||
Oberflaeche hat das bisher am Rollennamen entschieden und dabei
|
||
zwei Tage lang die Liste versteckt, die sie laengst geladen
|
||
hatte. */
|
||
darf_zugaenge_fuehren: fuehrtDieZugaenge(person),
|
||
/* OB DER CHAT-KNOPF IN DER KOPFLEISTE ERSCHEINT (18.09.2026).
|
||
|
||
Er wurde auf JEDER Seite gebaut. Fuer ein Mitglied der Community
|
||
fuehrte er ins Leere: `chat.html` steht ihm nicht offen, der
|
||
Klick landete wieder auf der Startseite. Gemessen von
|
||
pruef-community-sicht: zehn Seiten, zehn tote Wege -- und der
|
||
Knopf steht oben rechts, wo man ihn am ehesten drueckt.
|
||
|
||
DIE ANTWORT KOMMT AUS DERSELBEN TABELLE wie die Schranke selbst.
|
||
Die Oberflaeche vergleicht keine Rollennamen -- das waere eine
|
||
zweite Wahrheit, und bei der naechsten Rechteaenderung liefe sie
|
||
auseinander. Genau die Ueberlegung steht schon zwei Zeilen
|
||
darueber; hier gilt sie noch einmal. */
|
||
darf_chat: darfSeite(person, "/workspace/chat.html"),
|
||
/* UND OB SIE TELEFONIEREN DARF (19.09.2026).
|
||
|
||
Nicht als Bequemlichkeit: Ohne diese Angabe holt `anruf.js` beim
|
||
Laden jeder Chatseite die Verbindungsadressen -- und bekommt
|
||
fuer die Community 404. Der Browser schreibt das in die Konsole,
|
||
BEVOR JavaScript es abfangen kann; abfangen hilft also nicht,
|
||
die Anfrage darf gar nicht erst gestellt werden.
|
||
|
||
Ein 404 bei jedem Seitenaufruf ist Rauschen, und Rauschen macht
|
||
den naechsten ECHTEN Fehler unsichtbar -- dieselbe Regel wie bei
|
||
der Warnung, die immer kommt. Gefunden hat es die Pruefung im
|
||
Browser, nicht das Lesen.
|
||
|
||
AUS DERSELBEN FUNKTION wie der Riegel am Anruf-Router. Zwei
|
||
Rechnungen waeren die Stelle, an der die Seite ein Telefon
|
||
anbietet, das der Server ablehnt. */
|
||
darf_anrufen: darfTelefonieren(person),
|
||
/* DARF ICH AUFGABEN AN ANDERE GEBEN? (20.09.2026)
|
||
|
||
AUSDRUECKLICH GESAGT, NICHT ERRATEN. Das Aufgabenbrett fragte
|
||
bisher `LEITUNG.has(ich.rolle)` -- eine zweite Fassung derselben
|
||
Regel im Browser. Seit die rechte Hand verteilen darf, waere sie
|
||
die falsche gewesen: Der Server haette es erlaubt, die Seite
|
||
haette die Felder nicht gezeigt. Eine Berechtigung, die man
|
||
hat und nicht sieht, ist keine.
|
||
|
||
Aus DERSELBEN Funktion, die auch die Route benutzt. */
|
||
darf_verteilen: darfAufgabenVerteilen(person),
|
||
/* UND OB SIE ENTSCHEIDET, WER EINE AUFGABE MACHT (22.09.2026).
|
||
|
||
Das ist eine ANDERE Frage als `darf_verteilen`, und die
|
||
Oberflaeche braucht beide: Die linke Hand verteilt (also zeigt
|
||
ihr die Seite den Verteilen-Kasten), entscheidet aber nicht
|
||
(also sieht sie keine Annehmen/Ablehnen-Knoepfe an fremden
|
||
Bewerbungen, sondern einen Bewerben-Knopf fuer sich selbst).
|
||
|
||
Aus DERSELBEN Funktion, die auch die Wege absichern -- eine
|
||
Rollenliste im Browser waere die zweite Wahrheit, die beim
|
||
naechsten Umbau auseinanderlaeuft. */
|
||
darf_entscheiden: entscheidetUeberAufgaben(person),
|
||
/* Damit die Oberflaeche den Knopf gar nicht erst anbietet -- ein
|
||
Knopf, der mit 403 antwortet, ist schlimmer als keiner. */
|
||
darf_aufgaben_anlegen: darfAufgabenAnlegen(person),
|
||
/* WO AUFGABEN ANGELEGT WERDEN (22.09.2026).
|
||
|
||
Filipe, zum Knopf auf dem Aufgabenbrett: „dieser button kann da
|
||
jetzt doch endlich verschwinden, auf dieser seite sollen ja
|
||
keine aufgaben mehr verteilt werden." Und zur
|
||
Entwicklungsseite: „dieser buttion da soll nicht einen zu der
|
||
seite aufgaben fuehren sondern da in dieser seite die aufgaben
|
||
erstellen und vergeben koennen."
|
||
|
||
ABER NICHT UEBERALL, UND DAS IST GEMESSEN. Die Entwicklungs-
|
||
seite steht vier Rollen gar nicht offen, die sehr wohl Aufgaben
|
||
anlegen duerfen:
|
||
|
||
Rolle/Haus aufgaben.html entwicklung.html darfAnlegen
|
||
manager/agentur ja NEIN ja
|
||
creator/agentur ja NEIN ja
|
||
scout/agentur ja NEIN ja
|
||
spicy/agentur ja NEIN ja
|
||
|
||
Haette ich den Knopf einfach entfernt, koennten vier Rollen gar
|
||
keine Aufgabe mehr anlegen -- und Filipe hat beim Schreiben auf
|
||
den Team-Dogi-Bildschirm gesehen, nicht auf ihren.
|
||
|
||
DER ORT WIRD AUS DEN KACHELN ABGELEITET, nicht aus dem
|
||
Seitenrecht und schon gar nicht aus dem Haus.
|
||
|
||
Der erste Entwurf fragte `darfSeite(..., entwicklung.html)`.
|
||
Gemessen war das falsch: DogFather DARF die Seite auf der
|
||
Agenturadresse oeffnen (er darf alles), hat dort aber keine
|
||
Kachel dorthin -- der Knopf waere ihm weggenommen worden und
|
||
der Ersatz unauffindbar gewesen.
|
||
|
||
Massgeblich ist deshalb, ob die Entwicklungsseite ueberhaupt zu
|
||
seinen Bereichen gehoert. Das ist dieselbe Quelle, aus der die
|
||
Startseite ihre Kacheln baut -- eine zweite Liste waere die,
|
||
die auseinanderlaeuft. */
|
||
aufgaben_anlegen_auf: [
|
||
...(bereicheFuer(person) || []),
|
||
...(zusatzBereicheFuer(person) || []),
|
||
].some((k) => String(k.ziel || "").startsWith("entwicklung.html"))
|
||
? "entwicklung" : "aufgaben",
|
||
/* FUEHRT JEMAND TEAM DOGI? (22.09.2026)
|
||
Damit die Entwicklungsseite die Kartenliste GAR NICHT ERST
|
||
abfragt, wenn es sie fuer diese Person nicht gibt. Der Code
|
||
behandelte den 404 schon richtig -- aber der Browser
|
||
protokolliert ihn trotzdem, und im Handy-Rundgang stand
|
||
deshalb bei jedem Modi "404 (Not Found)" in der Konsole.
|
||
Genau derselbe Fall wie am 19.09.2026 bei /anruf/adressen.
|
||
|
||
EIGENES FELD, kein Stellvertreter: `darf_verteilen` sieht
|
||
aehnlich aus, ist aber nicht dasselbe -- ein Manager darf
|
||
verteilen und fuehrt Team Dogi nicht. Zwei Regeln, zwei
|
||
Felder; eines fuer beides waere die Stelle, an der es beim
|
||
naechsten Umbau auseinanderlaeuft. */
|
||
fuehrt_team: fuehrtTeamDogi(person),
|
||
/* DIE KACHELN, WENN SIE NICHT IM BROWSER STEHEN DUERFEN.
|
||
|
||
Fuer die fuenf bekannten Rollen steht hier `null`, und die
|
||
Oberflaeche nimmt wie bisher ihre eigene Liste -- der Ablauf
|
||
aendert sich fuer sie also nicht.
|
||
|
||
NACH DER ANGESEHENEN PERSON, nicht nach der eigenen: Sieht sich
|
||
DogFather den Arbeitsplatz eines Modis an, soll er DESSEN Kacheln
|
||
sehen. Genau daran war beim Sicht-Umschalter schon einmal zu
|
||
erkennen, dass er nicht greift ("ich seh immer noch die Seite
|
||
genau wie meine"). */
|
||
bereiche: nurOffeneKacheln(bereicheFuer(angesehen || person), angesehen || person),
|
||
rolle_text: rollentextFuer(angesehen || person),
|
||
marke: markeFuer(angesehen || person, req.get("host")),
|
||
/* Der Titel haengt allein an der Adresse -- nicht an der Person und
|
||
auch nicht an der angesehenen Sicht. Wer den Sicht-Umschalter
|
||
benutzt, wechselt die Zahlen, nicht das Haus. */
|
||
titel: titelFuer(req.get("host")),
|
||
/* Kacheln, die zur eigenen Liste DAZUkommen -- im Gegensatz zu
|
||
`bereiche`, das sie ersetzt. Leer fuer alle, die keine haben. */
|
||
bereiche_zusatz: nurOffeneKacheln(zusatzBereicheFuer(angesehen || person),
|
||
angesehen || person),
|
||
/* DIE EIGENEN SEITEN -- damit die Oberflaeche ihre EIGENE
|
||
Kachelliste (assets/js/bereiche.js) ebenfalls filtern kann.
|
||
|
||
Geliefert wird, was die Person DARF, nicht was sie nicht darf:
|
||
Eine Liste der verbotenen Seiten waere eine Aufzaehlung dessen,
|
||
was es sonst noch gibt -- und damit genau die Auskunft, die
|
||
niemand bekommen soll. */
|
||
seiten: seitenFuer((angesehen || person).rolle),
|
||
});
|
||
});
|
||
|
||
/* ---------- Verwaltung (nur über die Kommandozeile) --------------------
|
||
Wird von workspace-code.js benutzt. Bewusst nicht über das Netz
|
||
erreichbar: Codes werden auf dem Server erzeugt, einmal angezeigt und
|
||
nie gespeichert -- weder in Git noch in einer Notiz. */
|
||
|
||
/* Alphabet ohne 0/O und 1/I/l: Diese Codes werden abgetippt und
|
||
weitergegeben, Verwechslungen kosten sonst unnötig Nerven. */
|
||
const ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||
|
||
export function codeErzeugen(gruppen = 4, laenge = 4) {
|
||
const roh = randomBytes(gruppen * laenge);
|
||
let aus = "";
|
||
for (let i = 0; i < gruppen * laenge; i++) {
|
||
if (i && i % laenge === 0) aus += "-";
|
||
aus += ALPHABET[roh[i] % ALPHABET.length];
|
||
}
|
||
return aus;
|
||
}
|
||
|
||
/* `akteur` ist WER die Aktion ausloest -- nicht, wen sie betrifft. Das
|
||
muss getrennt bleiben: Stand im Protokoll die neu angelegte Person als
|
||
person_id, las sich der Eintrag so, als haette sie sich selbst angelegt.
|
||
Wer betroffen ist, steht im Text. Ohne Akteur (Kommandozeile) bleibt
|
||
das Feld leer. */
|
||
/* =====================================================================
|
||
Betreuung — wer darf sich um welchen Creator kuemmern.
|
||
|
||
Scouts betreuen Creator wie das Management, aber nur die ihnen
|
||
zugeteilten. Diese Regel steht bewusst NUR hier: Sie wird von sechs
|
||
Modulen gebraucht (Profile, Bereiche, Reports, Aufgaben, Kalender,
|
||
Dateien), und eine Rechteregel, die an sechs Stellen steht, ist eine
|
||
Rechteregel, die irgendwann an fuenf Stellen stimmt.
|
||
===================================================================== */
|
||
|
||
/** Welcher Kalendertag ist heute -- nach der ORTSZEIT, nicht nach UTC.
|
||
*
|
||
* Steht hier und nicht in jedem Modul: Sie wurde an vier Stellen
|
||
* gebraucht und an dreien falsch gerechnet (toISOString liefert UTC).
|
||
* Server und Benutzer stehen beide auf Europe/Berlin; zwischen
|
||
* Mitternacht und 2 Uhr lieferte die UTC-Rechnung den Vortag.
|
||
*
|
||
* NICHT verwenden, um mit Datumsangaben zu RECHNEN -- dafuer bleibt es
|
||
* bei UTC-Mittag (siehe kalender.js): Wer mit lokalen Zeiten rechnet,
|
||
* verliert bei der Zeitumstellung einen Tag. */
|
||
export function heuteLokal() {
|
||
const d = new Date();
|
||
const p = (n) => String(n).padStart(2, "0");
|
||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
||
}
|
||
|
||
/** Derselbe Tag, um `versatz` Tage verschoben. Negativ heißt zurück.
|
||
*
|
||
* ZWEI DINGE AM 22.09.2026 NACHGEZOGEN, beide durch einen echten Schaden:
|
||
*
|
||
* (1) `versatz = 0`. Dogi-Media rief `tagLokal()` ohne Argument auf und
|
||
* bekam `"NaN-NaN-NaN"` zurück -- eine Zeichenkette, die aussieht
|
||
* wie ein Datum und sich wie keines verhält. Im Vergleich
|
||
* `"2026-12-31" < "NaN-NaN-NaN"` gewinnt das N, also galt JEDES
|
||
* Stück mit Enddatum vom ersten Tag an als abgelaufen, und
|
||
* "Kommt noch" gab es nie. Kein Absturz, keine Meldung, keine
|
||
* Zeile im Protokoll -- nur eine Seite, die etwas anderes zeigt
|
||
* als die Wahrheit. Die drei Kopien dieser Funktion waren an
|
||
* dieser Stelle auseinandergelaufen: helfer-tag.mjs hatte die
|
||
* Vorbelegung, die beiden anderen nicht.
|
||
*
|
||
* (2) Der Wurf bei einer Zahl, die keine ist. Eine Vorbelegung deckt
|
||
* nur den leeren Aufruf ab; `tagLokal(irgendwas)` mit einer
|
||
* undefinierten Variablen läge weiter still daneben. Ein Datum,
|
||
* das sich nicht ausrechnen lässt, muss SCHEITERN und nicht
|
||
* schweigen -- sonst wandert der Unsinn in die Datenbank und in
|
||
* die Anzeige, und gefunden wird er Wochen später. */
|
||
export function tagLokal(versatz = 0) {
|
||
if (!Number.isFinite(Number(versatz))) {
|
||
throw new TypeError(`tagLokal: "${versatz}" ist keine Zahl von Tagen.`);
|
||
}
|
||
const d = new Date(Date.now() + versatz * 86400_000);
|
||
const p = (n) => String(n).padStart(2, "0");
|
||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
||
}
|
||
|
||
/* Ids der Creator, die diese Person betreut.
|
||
|
||
GILT SEIT DEM 01.09.2026 AUCH FUER MANAGER, nicht mehr nur fuer
|
||
Scouts. Vorher stand hier "das Management sieht ohnehin alles" -- und
|
||
genau das soll nicht mehr sein:
|
||
|
||
"NUR DIE ROLLE DOGFATHER SOLL WIRKLICH WEITERHIN ALLEINE ALLES
|
||
SEHEN KOENNEN. DIE ANDEREN SOLLEN NUR IHRE ZUGETEILTEN AUFGABEN
|
||
VON IHREN CREATOR SEHEN."
|
||
|
||
Die Datenbank konnte das laengst: In `betreuung` steht eine beliebige
|
||
Person als Betreuer, auch ein Manager oder DogFather. Nur diese
|
||
Funktion hat alle ausser Scouts abgewiesen -- ein Manager bekam
|
||
deshalb eine leere Liste und faellt in den Regeln unten auf "sieht
|
||
alles" oder (beim Aufgabenbrett) auf "sieht nichts" zurueck.
|
||
|
||
Ein Creator bleibt aussen vor: Er betreut niemanden, er wird betreut. */
|
||
export function betreuteIds(person) {
|
||
if (!person || (person.rolle !== "scout" && person.rolle !== "manager")) return [];
|
||
try {
|
||
const eigene = db().prepare("SELECT creator_id FROM betreuung WHERE betreuer_id = ?")
|
||
.all(person.id).map((z) => z.creator_id);
|
||
if (person.rolle !== "manager") return eigene;
|
||
|
||
/* DIE KETTE: Manager -> seine Scouts -> deren Creator (01.09.2026).
|
||
|
||
Entschieden auf die Frage "sieht er dann auch die Creator dieser
|
||
Scouts?" -- "ja, alles seiner Scouts". Das ist die uebliche
|
||
Ordnung: Wer einen Scout fuehrt, muss sehen, woran der arbeitet.
|
||
Ohne das muesste jeder Creator einem Manager EINZELN zugewiesen
|
||
werden, und beim ersten vergessenen faende er ein Loch in seiner
|
||
Uebersicht, ohne zu merken, dass es eines ist.
|
||
|
||
Ein Set, weil ein Creator auf beiden Wegen kommen kann: direkt
|
||
zugeteilt UND ueber seinen Scout. Doppelte Nummern wuerden in den
|
||
IN-Listen zu doppelten Fragezeichen -- fachlich harmlos, aber
|
||
jede Abfrage unnoetig laenger. */
|
||
const scouts = scoutsVon(person.id);
|
||
if (!scouts.length) return eigene;
|
||
const ueberScouts = db().prepare(
|
||
`SELECT creator_id FROM betreuung
|
||
WHERE betreuer_id IN (${scouts.map(() => "?").join(",")})`)
|
||
.all(...scouts).map((z) => z.creator_id);
|
||
return [...new Set([...eigene, ...ueberScouts])];
|
||
} catch {
|
||
return []; // im Zweifel nichts sehen, nie mehr
|
||
}
|
||
}
|
||
|
||
/** Die Scouts, die diesem Manager zugeteilt sind. Fuer alle anderen
|
||
* leer -- ein Scout fuehrt keine Scouts, ein Creator schon gar nicht.
|
||
* DogFather braucht sie nicht: Er sieht ohnehin alles. */
|
||
export function scoutsVon(managerId) {
|
||
if (!managerId) return [];
|
||
try {
|
||
return db().prepare("SELECT scout_id FROM scout_zuteilung WHERE manager_id = ?")
|
||
.all(managerId).map((z) => z.scout_id);
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/** Wessen Leads darf diese Person sehen? Gibt die Personennummern
|
||
* zurueck, deren Pipeline sichtbar ist -- der eigene immer dabei.
|
||
* Ein Scout sieht nur sich, ein Manager sich und seine Scouts. */
|
||
function pipelineIdsRoh(person) {
|
||
if (!person) return [];
|
||
if (person.rolle !== "manager") return [person.id];
|
||
return [...new Set([person.id, ...scoutsVon(person.id)])];
|
||
}
|
||
|
||
/** Zuteilung setzen oder loesen (null loest sie).
|
||
* Wer das DARF, entscheidet die Route -- hier steht nur, WIE. */
|
||
export function scoutZuteilungSetzen(scoutId, managerId, akteur = null) {
|
||
const d = db();
|
||
if (managerId === null) {
|
||
d.prepare("DELETE FROM scout_zuteilung WHERE scout_id = ?").run(scoutId);
|
||
return;
|
||
}
|
||
d.prepare(`
|
||
INSERT INTO scout_zuteilung (scout_id, manager_id, seit, gesetzt_von)
|
||
VALUES (?,?,?,?)
|
||
ON CONFLICT(scout_id) DO UPDATE SET
|
||
manager_id = excluded.manager_id,
|
||
seit = excluded.seit,
|
||
gesetzt_von = excluded.gesetzt_von`)
|
||
.run(scoutId, managerId, new Date().toISOString(), akteur);
|
||
}
|
||
|
||
/* =====================================================================
|
||
WEN DARF DIESE PERSON ÜBERHAUPT SEHEN? (03.09.2026)
|
||
|
||
Wunsch: "jeder manager soll auch immer nur seine und die seiner
|
||
scouts zugeteilten creator und creator daten sehen. und nicht die der
|
||
anderen."
|
||
|
||
Die Kette Manager -> Scout -> Creator gab es schon (betreuteIds). Was
|
||
fehlte, war ihre ANWENDUNG: Über zwanzig Stellen prüften die ROLLE
|
||
statt der ZUTEILUNG -- "ist Leitung? dann alles". Eine Lecksuche über
|
||
alle Leseschnittstellen (server/pruef-manager-sicht.mjs) fand am
|
||
03.09.2026 dreizehn davon: der Kalender zeigte fremde Fristen, die
|
||
Personenlisten fremde Namen, Report, Steckbrief, Profil, Start-Check,
|
||
Schulung und die Suche jeweils alles.
|
||
|
||
Deshalb stehen die beiden Antworten jetzt HIER, an einer Stelle, und
|
||
werden überall geholt statt jedes Mal neu formuliert.
|
||
|
||
RÜCKGABE null HEISST "ALLE" -- und zwar nur für DogFather. Das ist
|
||
bewusst kein leeres Feld: Eine leere Liste bedeutet "niemand", und
|
||
die Verwechslung der beiden ist genau der Fehler, der aus einer
|
||
Sperre eine Freigabe macht. Wer null bekommt, lässt die Einschränkung
|
||
ganz weg; wer ein Feld bekommt, schränkt darauf ein -- auch wenn es
|
||
leer ist.
|
||
===================================================================== */
|
||
|
||
/** Die Creator, deren Daten diese Person sehen darf.
|
||
* null = alle (nur DogFather). */
|
||
function sichtbareCreatorIdsRoh(person) {
|
||
if (!person) return [];
|
||
if (siehtAlles(person)) return null;
|
||
if (person.rolle === "creator") return [person.id];
|
||
return betreuteIds(person); // Manager: eigene + die seiner Scouts
|
||
}
|
||
|
||
/** Die PERSONEN, die in Listen und Auswahlfeldern auftauchen dürfen --
|
||
* Namen sind auch Daten. null = alle (nur DogFather).
|
||
*
|
||
* Enthält immer die Person selbst: Wer sich in einer Auswahl nicht
|
||
* findet, kann sich nichts selbst zuweisen. Bei einem Manager kommen
|
||
* seine Scouts dazu -- er führt sie, er muss sie eintragen können. */
|
||
/* =====================================================================
|
||
WEN DARF DIESE PERSON NICHT SEHEN? (09.09.2026)
|
||
|
||
Wunsch Filipe, ausdruecklich und dringlich: *"und noch gaaaaaanz
|
||
wichtig keiner soll vanvan sehen ausser ich, ueberall soll keiner
|
||
vanvan sehen ausser dogfather."*
|
||
|
||
Es gab davon bisher nur eine Haelfte: In der Personenliste wurde der
|
||
zweite Admin-Zugang fuer Spicy Media ausgeblendet (Wunsch vom
|
||
31.08.). Ueberall sonst -- Chat, Kalender, Aufgaben, Dateien, Suche
|
||
-- war er sichtbar, und `sichtbarePersonenIds` hat ihn sogar
|
||
ausdruecklich JEDER Rolle gezeigt, weil sie alle Admins einsammelt
|
||
("DogFather ist fuer alle sichtbar").
|
||
|
||
WORAN DIE BEIDEN AUSEINANDERGEHALTEN WERDEN: In der Datenbank tragen
|
||
beide die Rolle `admin`, es gibt kein unterscheidendes Feld. Der
|
||
Unterschied, den es wirklich gibt, ist das Alter -- DogFather ist
|
||
der erste Zugang des Hauses. Deshalb gilt: der Admin mit der
|
||
KLEINSTEN Nummer ist DogFather, alle weiteren Admin-Zugaenge sind
|
||
verborgen.
|
||
|
||
DREI AUSNAHMEN, und nur diese drei:
|
||
* DogFather selbst sieht alle.
|
||
* Ein verborgener Zugang sieht sich selbst (sonst faende er sein
|
||
eigenes Profil nicht).
|
||
* Gibt es nur EINEN Admin, ist nichts zu verbergen.
|
||
|
||
DIE SCHWACHSTELLE STEHT HIER, damit sie niemand suchen muss: Wuerde
|
||
Zugang 1 geloescht, rueckte der naechste Admin nach und waere
|
||
ploetzlich sichtbar. Sollte das eintreten, gehoert ein
|
||
ausdrueckliches Merkmal in die Tabelle (`haupt`-Feld). Solange
|
||
DogFather der erste Zugang ist, ist die Nummer die ehrlichste
|
||
Antwort ohne Datenbankumbau -- und sie haengt nicht am NAMEN, der
|
||
sich aendern kann.
|
||
|
||
WARUM AN DIESER STELLE UND NICHT IN DEN ABFRAGEN: Es gibt 23
|
||
Abfragen allein in workspace-personen.js, die Personen lesen. Eine
|
||
Regel, die man an 23 Stellen wiederholt, ist 23 Gelegenheiten, sie
|
||
zu vergessen -- und beim Vergessen faellt hier niemand auf die Nase,
|
||
sondern es sieht jemand etwas, das er nicht sehen soll. Deshalb
|
||
sitzt sie in den SECHS Listenfunktionen, durch die alles laeuft.
|
||
===================================================================== */
|
||
export function verborgeneIds(person) {
|
||
try {
|
||
const d = db();
|
||
|
||
/* ERSTER TEIL: die verborgenen Admin-Zugaenge (Regel vom 09.09.2026).
|
||
Der Admin mit der kleinsten Nummer ist DogFather, jeder weitere
|
||
Admin-Zugang ist verborgen. */
|
||
const admins = d.prepare(
|
||
"SELECT id FROM personen WHERE rolle = 'admin' ORDER BY id").all().map((z) => z.id);
|
||
const versteckteAdmins = admins.length >= 2 ? admins.slice(1) : [];
|
||
|
||
/* ZWEITER TEIL: das Modi-Team.
|
||
|
||
Hier haengt die Regel an der ROLLE und nicht an einer Nummer --
|
||
und das ist der Unterschied, auf den es ankommt. Beim ersten Teil
|
||
steht die Schwachstelle im Kommentar oben ausdruecklich da: Wuerde
|
||
Zugang 1 geloescht, rueckte der naechste nach und waere ploetzlich
|
||
sichtbar. Eine Rolle kann das nicht passieren. Wer einen Modi
|
||
verbergen will, muss ihm nichts anhaengen und nichts nachhalten --
|
||
er IST verborgen, solange er ein Modi ist.
|
||
|
||
WER DARF SIE SEHEN, und nur diese zwei:
|
||
* die DogFather-Rolle (admin) -- also auch die rechte Hand,
|
||
denn sie traegt dieselbe Rolle. So gewollt (Kapitel 3 des
|
||
Anforderungsdokuments: gleicher Ueberblick).
|
||
* ein Modi selbst -- sie sind untereinander ein Team.
|
||
|
||
Manager, Scouts, Creator und Spicy Media sehen sie nicht. Nicht
|
||
"sehen sie ohne Einzelheiten", sondern gar nicht: kein Name, kein
|
||
Eintrag, keine Zahl, die sie mitzaehlt. */
|
||
const modis = d.prepare(
|
||
`SELECT id FROM personen WHERE rolle IN (${
|
||
[...TEAM_DOGI_ROLLEN].map((r) => `'${r}'`).join(", ")})`).all().map((z) => z.id);
|
||
|
||
/* Ohne bekannte Person wird BEIDES verborgen. Das ist die strengere
|
||
Antwort, und bei einer Sichtbarkeitsregel ist die strengere die
|
||
richtige: Wer nicht weiss, wer fragt, zeigt nichts. */
|
||
const darfAdminsSehen = !!person
|
||
&& (person.id === admins[0] || versteckteAdmins.includes(person.id));
|
||
const darfModisSehen = !!person
|
||
&& (person.rolle === "admin" || TEAM_DOGI_ROLLEN.has(person.rolle));
|
||
|
||
const weg = [];
|
||
if (!darfAdminsSehen) weg.push(...versteckteAdmins);
|
||
if (!darfModisSehen) weg.push(...modis);
|
||
/* Doppelte heraus: Ein Modi-Zugang koennte theoretisch auch in der
|
||
ersten Liste stehen, wenn jemand die Rollen umhaengt. */
|
||
return [...new Set(weg)];
|
||
} catch {
|
||
/* Ohne Datenbank lieber nichts verbergen als abstuerzen -- diese
|
||
Funktion darf keine Seite lahmlegen. Sie laeuft in jedem
|
||
Listenaufruf. */
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/** Dieselbe Regel als Filter auf eine fertige Liste. */
|
||
export function ohneVerborgene(ids, person) {
|
||
const weg = verborgeneIds(person);
|
||
if (!weg.length) return ids;
|
||
if (ids === null) {
|
||
/* `null` heisst bisher "sieht alles". Sobald es etwas zu verbergen
|
||
gibt, darf das nicht mehr gelten -- die Liste wird deshalb
|
||
AUSGESCHRIEBEN. Das ist strenger, nicht lockerer, und die
|
||
Aufrufer bauen daraus ohnehin ein `IN (...)`. */
|
||
try {
|
||
return db().prepare(
|
||
`SELECT id FROM personen WHERE id NOT IN (${weg.map(() => "?").join(",")})`)
|
||
.all(...weg).map((z) => z.id);
|
||
} catch { return null; }
|
||
}
|
||
return ids.filter((i) => !weg.includes(i));
|
||
}
|
||
|
||
function sichtbarePersonenIdsRoh(person) {
|
||
if (!person) return [];
|
||
if (siehtAlles(person)) return null;
|
||
const creator = sichtbareCreatorIds(person) || [];
|
||
const scouts = person.rolle === "manager" ? scoutsVon(person.id) : [];
|
||
/* DOGFATHER IST FUER ALLE SICHTBAR (07.09.2026, Wunsch Filipe:
|
||
"dogfather und agentur sollen die alle sehen").
|
||
|
||
Das ist keine Aufweichung der Regel, sondern ihre Vervollstaendigung:
|
||
Jede Rolle arbeitet mit DogFather -- er gibt frei, er entscheidet,
|
||
er steht in jedem Protokoll. Ihn aus der Personenliste
|
||
herauszuhalten hiess bisher, dass sein Name in Terminen und
|
||
Aufgaben auftaucht, aber nirgends ein Gesicht dazugehoert.
|
||
|
||
Sichtbar heisst hier NUR: Name, Rolle und Bild -- dieselben drei
|
||
Angaben wie bei jedem anderen in dieser Liste. Alles Weitere (seine
|
||
Eintraege, seine Chats, seine Zahlen) haengt weiterhin an den
|
||
eigenen Sichtbarkeitsregeln der jeweiligen Module und ist davon
|
||
nicht beruehrt. */
|
||
const dogi = [];
|
||
try {
|
||
for (const z of db().prepare(
|
||
"SELECT id FROM personen WHERE rolle = 'admin' AND aktiv = 1").all()) dogi.push(z.id);
|
||
} catch { /* ohne Datenbank bleibt die Liste, wie sie war */ }
|
||
|
||
/* DAS MODI-TEAM SIEHT SICH UNTEREINANDER (Entscheidung Filipe,
|
||
09.09.2026: "ja, sie sind untereinander ein Team").
|
||
|
||
Ohne diese Zeilen waere jeder Modi allein: kein Team-Gruppenchat,
|
||
keine Schicht, die man tauschen kann, keine Eskalation, die man
|
||
weiterreicht, ohne dass DogFather sie einzeln durchstellt.
|
||
|
||
Es ist KEIN Loch in der Verborgenheit, sondern ihre andere Seite.
|
||
Was diese Liste zusaetzlich hergibt, wird gleich danach durch
|
||
ohneVerborgene() wieder eingeschraenkt -- und dort steht, dass ein
|
||
Modi nur fuer die DogFather-Rolle und fuer Modis sichtbar ist. Wer
|
||
kein Modi ist, bekommt diese Nummern hier gar nicht erst. */
|
||
const modis = [];
|
||
if (TEAM_DOGI_ROLLEN.has(person.rolle)) {
|
||
try {
|
||
const liste = [...TEAM_DOGI_ROLLEN].map((r) => `'${r}'`).join(", ");
|
||
for (const z of db().prepare(
|
||
`SELECT id FROM personen WHERE rolle IN (${liste}) AND aktiv = 1`).all()) modis.push(z.id);
|
||
} catch { /* ohne Datenbank bleibt die Liste, wie sie war */ }
|
||
}
|
||
return [...new Set([person.id, ...creator, ...scouts, ...dogi, ...modis])];
|
||
}
|
||
|
||
/** WER BETREUT MICH -- die Blickrichtung nach oben.
|
||
*
|
||
* betreuteIds() fragt "wen führe ich?". Diese Funktion fragt das
|
||
* Gegenteil: "wer führt mich?" -- also der eigene Scout, dessen
|
||
* Manager, und bei einem Scout sein Manager.
|
||
*
|
||
* Es gab die Richtung bisher nicht, weil sie für Sichtbarkeit nie
|
||
* gebraucht wurde: Ein Creator soll die Daten seines Scouts nicht
|
||
* sehen. Für eine EINLADUNG ist die Frage aber eine andere -- siehe
|
||
* einladbareIds(). */
|
||
function betreuerIdsRoh(person) {
|
||
if (!person) return [];
|
||
try {
|
||
const d = db();
|
||
if (person.rolle === "creator") {
|
||
const betreuer = d.prepare("SELECT betreuer_id FROM betreuung WHERE creator_id = ?")
|
||
.all(person.id).map((z) => z.betreuer_id);
|
||
if (!betreuer.length) return [];
|
||
/* Über den Scout auch dessen Manager: Wer mit seinem Scout einen
|
||
Call hat, hat ihn oft mit dessen Manager zusammen. */
|
||
const manager = d.prepare(
|
||
`SELECT manager_id FROM scout_zuteilung
|
||
WHERE scout_id IN (${betreuer.map(() => "?").join(",")})`)
|
||
.all(...betreuer).map((z) => z.manager_id);
|
||
return [...new Set([...betreuer, ...manager])];
|
||
}
|
||
if (person.rolle === "scout") {
|
||
return d.prepare("SELECT manager_id FROM scout_zuteilung WHERE scout_id = ?")
|
||
.all(person.id).map((z) => z.manager_id);
|
||
}
|
||
return [];
|
||
} catch {
|
||
return []; // im Zweifel niemand -- nie mehr, als sicher ist
|
||
}
|
||
}
|
||
|
||
/** WEN DARF ICH ZU EINEM TERMIN DAZUSTELLEN?
|
||
*
|
||
* Eingeführt am 05.09.2026 auf den Wunsch, die Teilnehmerwahl solle
|
||
* *"für jeden verfügbar sein"*.
|
||
*
|
||
* WARUM NICHT EINFACH sichtbarePersonenIds: Die Frage ist eine
|
||
* andere. Sichtbarkeit heißt "wessen Daten darf ich sehen" und zeigt
|
||
* bewusst nach unten -- ein Creator sieht dort nur sich selbst.
|
||
* Einladen heißt "mit wem arbeite ich zusammen" und geht in BEIDE
|
||
* Richtungen: Ein Creator lädt seinen Scout ein, ein Scout seinen
|
||
* Manager.
|
||
*
|
||
* Und warum nicht die eine Funktion erweitern: sichtbarePersonenIds
|
||
* hängt auch an der AUFGABENZUWEISUNG. Wer dort einen Betreuer
|
||
* hinzufügt, gibt einem Creator nebenbei die Möglichkeit, seinem
|
||
* Scout Aufgaben zu erteilen. Zwei Fragen, zwei Funktionen.
|
||
*
|
||
* Was ein Einladen bewirkt, ist eng: Die Person sieht diesen einen
|
||
* Termin in ihrem Kalender. Keine Daten, keine Rechte, kein Zugriff.
|
||
* Deshalb ist die weitere Menge hier vertretbar.
|
||
*
|
||
* null = alle (nur DogFather), wie überall in dieser Datei. */
|
||
function einladbareIdsRoh(person) {
|
||
if (!person) return [];
|
||
if (siehtAlles(person)) return null;
|
||
const sichtbar = sichtbarePersonenIds(person) || [];
|
||
/* DogFather gehört immer dazu: Er ist der Einzige, mit dem jede
|
||
Rolle zu tun hat, und ihn nicht einladen zu können wäre der erste
|
||
Fall, der auffällt. */
|
||
const chefs = db().prepare("SELECT id FROM personen WHERE rolle = 'admin' AND aktiv = 1")
|
||
.all().map((z) => z.id);
|
||
return [...new Set([...sichtbar, ...betreuerIds(person), ...chefs])];
|
||
}
|
||
|
||
/** MIT WEM DARF ICH SCHREIBEN? (06.09.2026, für den Chat)
|
||
*
|
||
* Entschieden von Filipe: entlang der Betreuung -- PLUS Manager und
|
||
* Scouts untereinander, auch ohne gemeinsamen Creator, "für Absprachen
|
||
* im Team".
|
||
*
|
||
* WARUM NICHT einladbareIds() WIEDERVERWENDEN, obwohl es fast passt:
|
||
* Dort geht es darum, wen man zu einem TERMIN dazustellen darf. Das
|
||
* ist harmlos -- die Person sieht einen Eintrag im Kalender. Ein Chat
|
||
* ist etwas anderes: Er legt ein dauerhaftes Gespräch an, schickt eine
|
||
* Benachrichtigung und macht den eigenen Namen sichtbar. Zwei Fragen,
|
||
* zwei Funktionen -- sonst verschiebt eine spätere Änderung an der
|
||
* einen stillschweigend die andere.
|
||
*
|
||
* WAS SICH DADURCH ÄNDERT gegenüber der Terminwahl: Ein Manager
|
||
* erreicht jetzt auch Scouts, die nicht seine sind, und umgekehrt.
|
||
* Ein Creator NICHT -- er bleibt bei seinen Betreuern und DogFather.
|
||
* Creator untereinander bleiben getrennt; sie sollen die Namen der
|
||
* anderen Creator weiterhin nicht sehen.
|
||
*
|
||
* null = alle (nur DogFather). */
|
||
function schreibbareIdsRoh(person) {
|
||
if (!person) return [];
|
||
if (siehtAlles(person)) return null;
|
||
|
||
const basis = new Set(einladbareIds(person) || []);
|
||
|
||
/* Das Team untereinander. Ein Scout, der eine Frage zu einem
|
||
fremden Creator hat, muss den zuständigen Manager erreichen können
|
||
-- sonst läuft alles über DogFather, und das ist genau der
|
||
Flaschenhals, den ein Chat auflösen soll. */
|
||
if (person.rolle === "manager" || person.rolle === "scout") {
|
||
try {
|
||
for (const z of db().prepare(
|
||
"SELECT id FROM personen WHERE rolle IN ('manager','scout') AND aktiv = 1").all()) {
|
||
basis.add(z.id);
|
||
}
|
||
} catch { /* im Zweifel nur die Betreuungskette */ }
|
||
}
|
||
|
||
/* DOGFATHER UND SPICY MEDIA SIND FUER JEDEN ERREICHBAR (07.09.2026).
|
||
|
||
Filipe: "im chat muss die spicy rolle und die dogfather rolle fuer
|
||
jeden zugaenglich sein."
|
||
|
||
DogFather kam bisher ueber einladbareIds() mit hinein; Spicy Media
|
||
nicht -- die Rolle steht in keiner Betreuungskette, sie steht
|
||
daneben. Genau deshalb war sie fuer einen Creator im Chat gar nicht
|
||
vorhanden, obwohl sie fuer alle zustaendig ist.
|
||
|
||
Beide werden hier ausdruecklich hinzugefuegt statt sich auf einen
|
||
Nebeneffekt zu verlassen: Eine Zustaendigkeit, die nur zufaellig
|
||
aus einer anderen Regel herausfaellt, faellt beim naechsten Umbau
|
||
genauso zufaellig wieder heraus.
|
||
|
||
Die Gegenrichtung stimmt ohne Zutun: Beide Rollen haben
|
||
`siehtAlles` und damit ohnehin `null` = jeder. */
|
||
try {
|
||
for (const z of db().prepare(
|
||
"SELECT id FROM personen WHERE rolle IN ('admin','spicy') AND aktiv = 1").all()) {
|
||
basis.add(z.id);
|
||
}
|
||
} catch { /* im Zweifel bleibt die Betreuungskette */ }
|
||
|
||
/* Sich selbst nicht -- ein Gespräch mit sich allein ist keines. */
|
||
basis.delete(person.id);
|
||
return [...basis];
|
||
}
|
||
|
||
/** Darf diese Person mit jener schreiben? Eine Frage, eine Antwort --
|
||
* damit die Regel nicht an fünf Stellen einzeln nachgebaut wird. */
|
||
export function darfSchreibenMit(person, andereId) {
|
||
const ids = schreibbareIds(person);
|
||
if (ids === null) return Number(andereId) !== Number(person.id);
|
||
return ids.includes(Number(andereId));
|
||
}
|
||
|
||
/** SQL-Baustein daraus: "diese Spalte ist eine Person, die ich sehen
|
||
* darf". Gibt null zurück, wenn nicht eingeschränkt werden muss. */
|
||
export function personenWo(person, spalte) {
|
||
const ids = sichtbarePersonenIds(person);
|
||
if (ids === null) return null;
|
||
if (!ids.length) return { wo: "0=1", werte: [] };
|
||
return { wo: `${spalte} IN (${ids.map(() => "?").join(",")})`, werte: ids };
|
||
}
|
||
|
||
/* SQL-Baustein "diese Spalte gehoert zu einem meiner Creator".
|
||
Gibt null zurueck, wenn es nichts zu ergaenzen gibt -- eine leere
|
||
IN-Liste waere ungueltiges SQL. */
|
||
export function betreutWo(person, spalte) {
|
||
const ids = betreuteIds(person);
|
||
if (!ids.length) return null;
|
||
return { wo: `${spalte} IN (${ids.map(() => "?").join(",")})`, werte: ids };
|
||
}
|
||
|
||
/* =====================================================================
|
||
FREI EINGETRAGENE NAMEN (02.09.2026)
|
||
|
||
Ein Feld wie "Mit wem" oder "Creator" nimmt entweder eine Person aus
|
||
dem Workspace ODER einen frei getippten Namen. Beides zugleich waere
|
||
eine Aussage, die niemand aufloesen kann.
|
||
|
||
WAS EIN FREIER NAME NICHT KANN, und warum das hier steht:
|
||
Er ist eine Beschriftung, kein Konto. Er sieht nichts, bekommt nichts
|
||
angezeigt, hat kein Aufgabenbrett. Bei "Creator" heisst das: Der
|
||
Eintrag gehoert zu niemandem im System und taucht deshalb in keiner
|
||
personenbezogenen Auswertung auf. Filipe hat das am 02.09.2026
|
||
ausdruecklich so gewaehlt, nachdem der Nachteil benannt war; das
|
||
Formular sagt es an der Stelle noch einmal.
|
||
|
||
Damit daraus kein STILLER Ausfall wird, ist die Gegenmassnahme
|
||
eingebaut: externSql. Jede Abfrage, die bisher den Namen der
|
||
verknuepften Person las, faellt jetzt auf den freien Text zurueck.
|
||
Der Eintrag verschwindet dadurch aus keiner Liste, keiner Suche und
|
||
keiner Uebersicht -- er ist nur eben als "(extern)" gekennzeichnet.
|
||
===================================================================== */
|
||
|
||
export const EXTERN_MAX = 80;
|
||
|
||
/** Prueft einen frei eingetragenen Namen und schreibt ihn nach `aus`.
|
||
* `feld` ist die Spalte ohne Endung, also "creator" oder "teilnehmer".
|
||
*
|
||
* Ist eine Person gewaehlt, wird der freie Name verworfen statt
|
||
* abgelehnt: Wer erst jemanden auswaehlt und dann tippt, meint das
|
||
* Getippte nicht mehr -- eine Fehlermeldung waere hier nur im Weg. */
|
||
export function externPruefen(körper, aus, feld, fehler) {
|
||
const schluessel = `${feld}_extern`;
|
||
if (körper[schluessel] === undefined) return;
|
||
const t = String(körper[schluessel] ?? "").trim();
|
||
if (!t) { aus[schluessel] = null; return; }
|
||
if (t.length > EXTERN_MAX) { fehler.push("Der eingetragene Name ist zu lang."); return; }
|
||
/* Steuerzeichen raus. Sie waeren unsichtbar und koennten eine Zeile
|
||
in Listen und Ausgaben zerreissen. */
|
||
aus[schluessel] = t.replace(/[\u0000-\u001f\u007f]/g, "");
|
||
aus[`${feld}_id`] = null;
|
||
}
|
||
|
||
/** SQL-Baustein: der Name der Person -- und wenn keine verknuepft ist,
|
||
* der frei eingetragene Text, gekennzeichnet.
|
||
*
|
||
* In SQLite ergibt NULL || 'x' wieder NULL. Ist die Spalte leer, faellt
|
||
* COALESCE deshalb korrekt auf NULL durch und nicht auf " (extern)". */
|
||
export const externSql = (personSpalte, externSpalte) =>
|
||
`COALESCE(${personSpalte}, ${externSpalte} || ' (extern)')`;
|
||
|
||
/* Wer sieht welchen Termin -- und damit auch: welche Wiederholung.
|
||
|
||
NUR DogFather sieht alles (01.09.2026). Vorher stand hier istLeitung()
|
||
-- damit sah auch jeder Manager jeden Creator. Ein Manager faellt
|
||
jetzt in dieselbe Regel wie ein Scout: nur die Creator, die ihm
|
||
zugeteilt sind. Ausdruecklicher Wunsch: "NUR DIE ROLLE DOGFATHER SOLL
|
||
WIRKLICH ALLEINE ALLES SEHEN."
|
||
|
||
Geaendert wird ausschliesslich, wer was SIEHT. Was ein Manager DARF
|
||
(freigeben, aendern, Personen verwalten), haengt weiterhin an
|
||
istLeitung und bleibt unveraendert -- sonst haette dieser eine Wunsch
|
||
stillschweigend seine halben Rechte mitgenommen.
|
||
|
||
Der Praefix ist der Tabellenname im jeweiligen SQL: "t" fuer termine,
|
||
"s" fuer termin_serien. Diese Regel steht bewusst nur EINMAL: Eine
|
||
zweite, fast gleiche Fassung fuer die Serien waere frueher oder
|
||
spaeter auseinandergelaufen -- und dann haette eine Wiederholung
|
||
jemandem etwas gezeigt, was der einzelne Termin ihm verbirgt. */
|
||
export function termineSichtbar(person, praefix = "t") {
|
||
const p = praefix;
|
||
|
||
/* =====================================================================
|
||
JEDER SIEHT NUR SEINE EIGENEN TERMINE (07.09.2026)
|
||
|
||
Wunsch Filipe, zuerst: "calls oder termine soll jeder nur sehen
|
||
die er selber macht oder jeden betrifft." Wenige Stunden spaeter,
|
||
nachdem er das Ergebnis gesehen hat, praeziser: "jeder soll und
|
||
darf im kalender immer nur seine eigenen eintraege nur sehen."
|
||
Die zweite Fassung gilt -- Begruendung unten beim entfallenen
|
||
Fall "geht alle an".
|
||
|
||
DAS GILT AUCH FUER DOGFATHER, und das ist die eigentliche
|
||
Aenderung. Bis hierher bekam er `1=1` -- er sah jeden Termin von
|
||
jedem. Bei einer Handvoll Leuten war das ein Ueberblick; bei
|
||
dreissig ist es eine Wand aus fremden Verabredungen, in der die
|
||
eigenen untergehen. Ein Kalender, in dem alles steht, ist kein
|
||
Kalender mehr.
|
||
|
||
"EIGEN" IST HIER GENAU DEFINIERT, sonst wird es Auslegung: Ich habe
|
||
den Termin angelegt (erstellt_von), er ist mir zugeordnet
|
||
(creator_id), ich bin das Gegenueber (teilnehmer_id) -- oder ich
|
||
stehe in der Teilnehmerliste. Sonst nichts. Kein Rollenrecht, keine
|
||
Betreuung, keine Ausnahme fuer teilnehmerlose Termine.
|
||
|
||
Diese Regel ersetzt die Rollenfrage vollstaendig: Es gibt hier
|
||
nichts mehr, was DogFather sieht und ein Creator nicht. Was ein
|
||
Betreuer ueber seine Creator wissen muss, steht in deren Bereichen
|
||
und Aufgaben -- nicht in ihrem Kalender.
|
||
===================================================================== */
|
||
|
||
/* MITGEZAEHLT WIRD AUCH DIE TEILNEHMERLISTE (05.09.2026).
|
||
|
||
Seit ein Termin mehrere Teilnehmer haben kann, reicht
|
||
`teilnehmer_id` nicht mehr: Das ist nur das Haupt-Gegenueber. Wer
|
||
als zweiter oder dritter dabei ist, steht in termin_teilnehmer --
|
||
und ohne diese Zeile saehe er den Termin nicht, an dem er
|
||
teilnimmt.
|
||
|
||
Die beiden Aufrufer arbeiten auf verschiedenen Tabellen: der
|
||
Kalender auf `termine` (praefix t), die Wiederholungen auf
|
||
`termin_serien` (praefix s). Deshalb wird hier die passende
|
||
Nebentabelle gewaehlt statt einer festen -- eine falsche Zuordnung
|
||
waere kein Fehler, den man sieht, sondern eine Liste, in der
|
||
jemandem etwas fehlt. */
|
||
const nebenTabelle = p === "s"
|
||
? { tabelle: "serie_teilnehmer", spalte: "serie_id" }
|
||
: { tabelle: "termin_teilnehmer", spalte: "termin_id" };
|
||
const dabei = `EXISTS (SELECT 1 FROM ${nebenTabelle.tabelle} tn`
|
||
+ ` WHERE tn.${nebenTabelle.spalte} = ${p}.id AND tn.person_id = ?)`;
|
||
|
||
const eigen = `(${p}.creator_id = ? OR ${p}.teilnehmer_id = ? OR ${p}.erstellt_von = ?`
|
||
+ ` OR ${dabei})`;
|
||
const werte = [person.id, person.id, person.id, person.id];
|
||
|
||
/* HIER STAND EIN ZWEITER FALL, UND ER IST AM 07.09.2026 ENTFALLEN.
|
||
|
||
Er hiess "geht alle an": ein Termin ohne Gegenueber und ohne
|
||
Teilnehmer war fuer jeden sichtbar, nach der Ueberlegung "wer
|
||
niemanden eintraegt, meint alle".
|
||
|
||
Die Ueberlegung war falsch, und Filipe hat es an einem echten Fall
|
||
gezeigt: In Cigdems Kalender stand sein "Manager Meeting" -- SEIN
|
||
Termin, den er fuer sich eingetragen hatte. Wer keinen Teilnehmer
|
||
eintraegt, meint eben meistens nicht "alle", sondern "mich". Und
|
||
die Auslegung lag nicht bei dem, der den Termin anlegt, sondern
|
||
hier im Code -- das ist die eigentliche Schwaeche gewesen.
|
||
|
||
Filipe: "jeder soll und darf im kalender immer nur seine eigenen
|
||
eintraege nur sehen."
|
||
|
||
Was alle angeht, geht sie jetzt an, weil jemand sie EINTRAEGT:
|
||
"bei calls und protocoll will ich dass jeder seine termine von
|
||
seinem kalender sieht und die wenn sie markiert werden im termin."
|
||
Die Teilnehmerliste ist damit die einzige Antwort auf die Frage,
|
||
wen ein Termin etwas angeht -- eine sichtbare Entscheidung im
|
||
Formular statt einer unsichtbaren Regel im Server.
|
||
|
||
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. */
|
||
/* EINE AUSNAHME, UND ZWAR GENAU EINE (11.09.2026).
|
||
|
||
Filipe: "mein kalender (dogfather) auf dieser seite hier soll
|
||
komplett verbunden sein mit dem kalender in der workspace seite
|
||
ABER NUR IN DER DOGFATHER ROLLE BEI DOGFATHER: NICHT BEI VANVAN."
|
||
|
||
Ein Kalender ist etwas anderes als eine Liste. Bei den Aufgaben,
|
||
den Bereichen und den Dateien ist die Trennung eine Hilfe -- man
|
||
will das andere Haus gerade NICHT sehen. Beim Kalender waere sie
|
||
eine Falle: Wer auf der Team-Seite einen Termin einträgt und die
|
||
Haelfte seines Tages nicht sieht, legt ihn auf eine Zeit, in der er
|
||
schon woanders sitzt. Ein Kalender, der nur die halbe Wahrheit
|
||
zeigt, ist schlimmer als keiner.
|
||
|
||
NUR FUER DIE DOGFATHER-ROLLE, und das ist sein ausdrueckliches
|
||
Wort. Die rechte Hand behaelt die Trennung: Sie arbeitet auf dieser
|
||
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. */
|
||
/* ==== 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})${ausSpalte} AND ${ohneAnderesHaus(p, person, spalten)}`,
|
||
werte,
|
||
};
|
||
}
|
||
return { wo: eigen, werte };
|
||
}
|
||
|
||
/* =====================================================================
|
||
DIE SICHT EINES ANDEREN — nur fuer DogFather.
|
||
|
||
Wunsch vom 01.09.2026: "ich will, dass ich alles von den Scouts und
|
||
Managern einsehen kann, dass ich auswaehlen kann, was und von wem ich
|
||
sehen will. Und das soll ich als DogFather-Rolle ueberall haben, auf
|
||
jeder Seite. NUR WIR BEIDE SOLLEN DIESE OPTION HABEN."
|
||
|
||
WIE ES GEBAUT IST, und warum genau so.
|
||
|
||
Die naheliegende Loesung waere ein neuer Filter je Seite gewesen:
|
||
"zeige mir Aufgaben von Person X". Das haette bedeutet, in zwoelf
|
||
Modulen eine zweite Rechteregel zu schreiben -- und eine Rechteregel,
|
||
die an zwoelf Stellen steht, stimmt irgendwann an elf.
|
||
|
||
Stattdessen wird die PERSON ausgetauscht, nicht die Regel: Waehlt
|
||
DogFather einen Scout, laeuft jede vorhandene sichtbar()-Funktion
|
||
unveraendert mit diesem Scout als Person. Er sieht dann exakt das,
|
||
was der Scout sieht -- nicht mehr und nicht weniger. Keine einzige
|
||
neue Rechteregel, und was heute richtig ist, bleibt es auch, wenn
|
||
sich eine Regel spaeter aendert.
|
||
|
||
DREI SICHERUNGEN, die nicht verhandelbar sind:
|
||
|
||
1. NUR ADMIN. Die Rolle wird HIER geprueft, nicht in der Oberflaeche.
|
||
Wer kein DogFather ist, kann den Wert anhaengen, so oft er will --
|
||
er wird stillschweigend ignoriert. Ein Manager duerfte sonst durch
|
||
Anhaengen von ?sicht=<Nummer> in fremde Bereiche sehen.
|
||
|
||
2. NUR LESEN. Ersetzt wird ausschliesslich req.sicht, niemals
|
||
req.person. Alles, was schreibt oder Rechte prueft, arbeitet
|
||
weiterhin mit dem echten Angemeldeten. Sonst haette ein
|
||
Schreibvorgang plotzlich im Namen eines anderen stattgefunden --
|
||
und im Protokoll staende der falsche Name.
|
||
|
||
3. NUR AKTIVE, ECHTE PERSONEN. Eine geloeschte oder abgeschaltete
|
||
Nummer faellt auf die eigene Sicht zurueck, statt eine leere Seite
|
||
zu zeigen, die aussieht wie ein Fehler.
|
||
===================================================================== */
|
||
|
||
/** Durch wessen Augen wird gerade gesehen? Gibt IMMER eine Person
|
||
* zurueck -- im Normalfall den Angemeldeten selbst. */
|
||
export function sichtPerson(req) {
|
||
/* req.person setzt jedes Modul in seiner eigenen angemeldet()-Huerde.
|
||
Diese Funktion laeuft aber schon davor (global, siehe index.js) --
|
||
deshalb liest sie die Sitzung notfalls selbst. */
|
||
const ich = req?.person || sitzungLesen(req);
|
||
if (!ich) return null;
|
||
if (ich.rolle !== "admin") return ich;
|
||
|
||
const id = Number(req.query?.sicht || 0);
|
||
if (!Number.isInteger(id) || id <= 0 || id === ich.id) return ich;
|
||
|
||
try {
|
||
const z = db().prepare("SELECT id, name, rolle FROM personen WHERE id = ? AND aktiv = 1").get(id);
|
||
/* Nicht gefunden oder abgeschaltet: zurueck auf die eigene Sicht.
|
||
Eine leere Seite waere hier die schlechtere Antwort -- sie sieht
|
||
aus wie ein Fehler, obwohl nur die Auswahl veraltet ist. */
|
||
if (!z) return ich;
|
||
|
||
/* IN WELCHEM HAUS STEHT DIE ANGESEHENE PERSON? (11.09.2026)
|
||
|
||
Ohne diese vier Zeilen fehlte der angesehenen Person das Feld
|
||
`haus` -- und `nurHaus()` und `hausBedingung()` fangen beide mit
|
||
`person?.haus !== "crew"` an. Eine Sicht ohne Haus war damit
|
||
automatisch eine Agentursicht: DogFather sah in der Sicht auf
|
||
einen Modi die Dateien, die Personen und die Berichte des
|
||
ANDEREN Hauses -- also mehr, als die Person selbst je sieht, und
|
||
nicht das, was sie sieht.
|
||
|
||
Das Haus haengt hier an der ROLLE, nicht an der Adresse. Eine
|
||
rechte Hand und ein Modi gibt es nur im Haus von Team Dogi; eine
|
||
Managerin, eine Scout und eine Creatorin nur im Agenturhaus.
|
||
Wuerde stattdessen die Adresse entscheiden, saehe dieselbe
|
||
Person je nach aufgerufener Wand etwas anderes -- und genau das
|
||
soll die Sicht ja gerade nicht.
|
||
|
||
DogFather selbst ist in beiden Haeusern zu Hause. Sieht er sich
|
||
durch die Augen eines zweiten Kontos mit seiner Rolle an, gilt
|
||
das Haus, in dem er gerade steht. */
|
||
const haus = TEAM_DOGI_ROLLEN.has(z.rolle) ? "crew"
|
||
: z.rolle === "admin" ? (ich.haus || "agentur")
|
||
: "agentur";
|
||
return { ...z, haus };
|
||
} catch {
|
||
return ich; /* im Zweifel die eigene Sicht, nie eine fremde */
|
||
}
|
||
}
|
||
|
||
/** Middleware: setzt req.sicht. Steht danach jedem Modul zur Verfuegung.
|
||
* Module, die sie nicht benutzen, arbeiten unveraendert weiter -- req.sicht
|
||
* ist dort schlicht dasselbe wie req.person. */
|
||
export function sichtSetzen(req, res, next) {
|
||
const s = sichtPerson(req);
|
||
if (s) req.sicht = s;
|
||
next();
|
||
}
|
||
|
||
/* Darf diese Person den Bereich dieses Creators sehen und bearbeiten? */
|
||
export function darfCreator(person, creatorId) {
|
||
if (!person || !creatorId) return false;
|
||
/* NUR DogFather pauschal (03.09.2026). Hier stand istLeitung -- und
|
||
damit war jede Managerin fuer JEDEN Creator zustaendig, auch fuer
|
||
die einer fremden Managerin.
|
||
|
||
Diese eine Zeile hing an mehreren Wegen gleichzeitig: Profil,
|
||
Start-Check und die Uebersicht je Creator liessen sich damit ueber
|
||
die blosse Kenntnis einer Nummer abfragen. Man musste nichts
|
||
umgehen, es reichte, eine Zahl in die Adresse zu schreiben.
|
||
|
||
Ein Manager faellt jetzt in dieselbe Zeile wie ein Scout -- die
|
||
Kette "eigene plus die meiner Scouts" steckt in betreuteIds. */
|
||
if (siehtAlles(person)) return true;
|
||
if (person.rolle === "creator") return person.id === Number(creatorId);
|
||
return betreuteIds(person).includes(Number(creatorId));
|
||
}
|
||
|
||
/* Zustaendige Person setzen oder entfernen (null loest die Zuordnung). */
|
||
export function betreuungSetzen(creatorId, betreuerId, akteur = null) {
|
||
const d = db();
|
||
if (betreuerId === null) {
|
||
d.prepare("DELETE FROM betreuung WHERE creator_id = ?").run(creatorId);
|
||
} else {
|
||
d.prepare(`
|
||
INSERT INTO betreuung (creator_id, betreuer_id, seit, gesetzt_von)
|
||
VALUES (?,?,?,?)
|
||
ON CONFLICT(creator_id) DO UPDATE SET
|
||
betreuer_id = excluded.betreuer_id,
|
||
seit = excluded.seit,
|
||
gesetzt_von = excluded.gesetzt_von`).run(
|
||
creatorId, betreuerId, new Date().toISOString(), akteur?.id ?? null);
|
||
}
|
||
protokolliere("betreuung_gesetzt", {
|
||
personId: akteur?.id ?? null, rolle: akteur?.rolle ?? null, ip: akteur?.ip ?? null,
|
||
detail: `Creator #${creatorId} -> ${betreuerId === null ? "niemand" : "#" + betreuerId}`,
|
||
});
|
||
}
|
||
|
||
/* ---------- Einstellungen -------------------------------------------------
|
||
Bewusst mit Vorgabewert und ohne Fehlerfall: Faellt die Datenbank aus,
|
||
soll eine fehlende Einstellung nicht die Seite mitreissen. */
|
||
|
||
export function einstellung(schluessel, vorgabe = null) {
|
||
try {
|
||
return db().prepare("SELECT wert FROM einstellungen WHERE schluessel = ?")
|
||
.get(schluessel)?.wert ?? vorgabe;
|
||
} catch {
|
||
return vorgabe;
|
||
}
|
||
}
|
||
|
||
/* WELCHE EINSTELLUNGEN NICHT IM KLARTEXT INS PROTOKOLL GEHOEREN
|
||
---------------------------------------------------------------------
|
||
Gefunden am 23.09.2026, beim Vorbereiten des KLIPY-Zugangs.
|
||
|
||
einstellungSetzen() schrieb bis dahin IMMER Name UND Wert ins
|
||
Protokoll. Fuer "nachtruhe_an = 1" ist das genau richtig -- man
|
||
will sehen, wer wann was umgestellt hat. Fuer einen Schluessel
|
||
ist es das Gegenteil.
|
||
|
||
NACHGEMESSEN in der echten Datenbank, nicht vermutet:
|
||
|
||
kopie_schluessel (31.08.) steht VOLLSTAENDIG im Protokoll
|
||
vapid_paar (05.09.) wurde bei 120 Zeichen abgeschnitten,
|
||
kurz VOR dem privaten Teil
|
||
|
||
Dass der zweite heil blieb, lag an einer Laengengrenze, nicht an
|
||
einer Absicht. Das ist keine offene Tuer -- das Protokoll liegt
|
||
hinter nurAdmin --, aber der Wert wandert in JEDE Sicherung und in
|
||
jeden Export. Und der naechste Schluessel waere denselben Weg
|
||
gegangen.
|
||
|
||
ES IST EINE REGEL UND KEINE LISTE. Eine Liste
|
||
["kopie_schluessel","vapid_paar"] waere beim naechsten Geheimnis
|
||
still unvollstaendig -- derselbe Fehler wie bei den abgeschriebenen
|
||
Spaltennamen am 11.09. Wer eine Einstellung so BENENNT, meint auch
|
||
ein Geheimnis. Faellt ein harmloser Name versehentlich darunter,
|
||
ist der Schaden, dass im Protokoll etwas weniger Text steht.
|
||
|
||
WAS STEHEN BLEIBT: dass sich die Einstellung geaendert hat, wann,
|
||
und durch wen. Nur der Wert fehlt. Ein Protokolleintrag ohne
|
||
Eintrag waere die schlechtere Loesung -- dann saehe niemand mehr,
|
||
dass jemand den Schluessel ausgetauscht hat. */
|
||
const GEHEIME_EINSTELLUNG = /(schluessel|schlüssel|geheim|token|passwort|kennwort|secret|paar|(^|[_-])key([_-]|$))/i;
|
||
|
||
export function einstellungSetzen(schluessel, wert, akteur = null) {
|
||
db().prepare(`
|
||
INSERT INTO einstellungen (schluessel, wert, geaendert, von) VALUES (?,?,?,?)
|
||
ON CONFLICT(schluessel) DO UPDATE SET
|
||
wert = excluded.wert, geaendert = excluded.geaendert, von = excluded.von`).run(
|
||
schluessel, String(wert), new Date().toISOString(), akteur?.id ?? null);
|
||
const roh = String(wert ?? "");
|
||
const detail = GEHEIME_EINSTELLUNG.test(String(schluessel))
|
||
? `${schluessel} = ${roh ? "(gesetzt, " + roh.length + " Zeichen)" : "(geleert)"}`
|
||
: `${schluessel} = ${wert}`;
|
||
protokolliere("einstellung_geaendert", {
|
||
personId: akteur?.id ?? null, rolle: akteur?.rolle ?? null, ip: akteur?.ip ?? null,
|
||
detail: detail.slice(0, 120),
|
||
});
|
||
}
|
||
|
||
export function personAnlegen(name, rolle, akteur = null) {
|
||
if (!ROLLEN.has(rolle)) throw new Error(`Unbekannte Rolle: ${rolle}`);
|
||
const code = codeErzeugen();
|
||
const salt = randomBytes(16).toString("hex");
|
||
const hash = hashe(code, salt, SCRYPT.N);
|
||
const { lastInsertRowid } = db().prepare(
|
||
"INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
|
||
+ " VALUES (?,?,?,?,?,?,1,?)"
|
||
).run(name, rolle, hash, salt, SCRYPT.N, codeKennung(code), jetzt());
|
||
protokolliere("person_angelegt", {
|
||
personId: akteur?.id ?? null, rolle: akteur?.rolle ?? null,
|
||
ip: akteur?.ip ?? null, detail: `${name} (${rolle})`,
|
||
});
|
||
return { id: Number(lastInsertRowid), name, rolle, code };
|
||
}
|
||
|
||
/** Das Sitzungs-Merkmal aus der Anfrage -- roh, wie es im Keks steht.
|
||
* Wird gebraucht, um GENAU DIESE eine Sitzung am Leben zu lassen. */
|
||
export const sitzungToken = (req) => req?.cookies?.[COOKIE] || null;
|
||
|
||
/**
|
||
* Neuen Code setzen.
|
||
*
|
||
* @param behalteToken Die eine Sitzung, die NICHT beendet wird.
|
||
* Gedacht für den Fall "ich erneuere meinen eigenen Code": Wer das tut,
|
||
* ist gerade angemeldet und hält diese Sitzung in der Hand.
|
||
*/
|
||
export function codeNeu(id, akteur = null, behalteToken = null) {
|
||
const person = db().prepare("SELECT id, name, rolle FROM personen WHERE id = ?").get(id);
|
||
if (!person) throw new Error(`Keine Person mit Nummer ${id}`);
|
||
const code = codeErzeugen();
|
||
const salt = randomBytes(16).toString("hex");
|
||
db().prepare(
|
||
"UPDATE personen SET code_hash = ?, code_salt = ?, code_n = ?, code_kennung = ? WHERE id = ?")
|
||
.run(hashe(code, salt, SCRYPT.N), salt, SCRYPT.N, codeKennung(code), id);
|
||
|
||
/* Offene Sitzungen beenden -- ein neuer Code soll den alten Zugang
|
||
wirklich beenden, nicht nur die nächste Anmeldung betreffen.
|
||
|
||
MIT EINER AUSNAHME, seit dem 02.09.2026: die Sitzung, aus der heraus
|
||
der Code gerade erneuert wird.
|
||
|
||
Vorher flog man beim eigenen Code sofort hinaus -- und zwar in
|
||
derselben Sekunde, in der der neue Code auf dem Bildschirm erschien.
|
||
Der nächste Aufruf der Seite lief in ein "nicht angemeldet" und
|
||
leitete zur Anmeldung um; der Code war weg, bevor man ihn kopieren
|
||
konnte. Genau so ist es Filipe am 02.09.2026 passiert, und danach kam
|
||
er nur noch über einen Eingriff auf dem Server wieder hinein.
|
||
|
||
Sicherheitlich kostet die Ausnahme nichts: Wer den Code erneuert, hat
|
||
sich mit genau dieser Sitzung soeben ausgewiesen und hält sie in
|
||
Händen. Alle ANDEREN Geräte fliegen weiterhin hinaus -- das ist der
|
||
Sinn der Übung. */
|
||
const behalten = behalteToken ? tokenHash(behalteToken) : null;
|
||
if (behalten) {
|
||
db().prepare("DELETE FROM sitzungen WHERE person_id = ? AND token_hash <> ?")
|
||
.run(id, behalten);
|
||
} else {
|
||
db().prepare("DELETE FROM sitzungen WHERE person_id = ?").run(id);
|
||
}
|
||
protokolliere("code_erneuert", {
|
||
personId: akteur?.id ?? null, rolle: akteur?.rolle ?? null,
|
||
ip: akteur?.ip ?? null, detail: `für ${person.name}`,
|
||
});
|
||
return { ...person, code };
|
||
}
|
||
|
||
export function personSperren(id, aktiv = 0, akteur = null) {
|
||
const person = db().prepare("SELECT name FROM personen WHERE id = ?").get(id);
|
||
db().prepare("UPDATE personen SET aktiv = ? WHERE id = ?").run(aktiv ? 1 : 0, id);
|
||
if (!aktiv) db().prepare("DELETE FROM sitzungen WHERE person_id = ?").run(id);
|
||
protokolliere(aktiv ? "person_entsperrt" : "person_gesperrt", {
|
||
personId: akteur?.id ?? null, rolle: akteur?.rolle ?? null,
|
||
ip: akteur?.ip ?? null, detail: person?.name ?? `#${id}`,
|
||
});
|
||
}
|
||
|
||
export function personenListe() {
|
||
return db().prepare(`
|
||
SELECT p.id, p.name, p.rolle, p.aktiv, p.erstellt, p.letzter_login,
|
||
(SELECT COUNT(*) FROM sitzungen s WHERE s.person_id = p.id) AS sitzungen
|
||
FROM personen p ORDER BY ${ROLLEN_SORTIERUNG.replace("rolle", "p.rolle")}, p.name`).all();
|
||
}
|
||
|
||
export function protokollLesen(anzahl = 20) {
|
||
return db().prepare(
|
||
"SELECT zeitpunkt, rolle, aktion, detail, ip FROM protokoll ORDER BY id DESC LIMIT ?"
|
||
).all(anzahl);
|
||
}
|
||
|
||
/* =====================================================================
|
||
DER MANTEL UM DIE SECHS LISTENFUNKTIONEN (09.09.2026)
|
||
|
||
Jede von ihnen hat mehrere Rueckgabewege -- `sichtbarePersonenIds`
|
||
allein drei. Die Verbergungsregel an jedem einzelnen anzubringen
|
||
waere achtzehn Gelegenheiten, einen zu vergessen; und wer hier einen
|
||
vergisst, merkt es nicht an einem Fehler, sondern daran, dass jemand
|
||
etwas sieht, das er nicht sehen soll.
|
||
|
||
Deshalb bleiben die Funktionen unveraendert (jetzt mit `Roh` im
|
||
Namen) und werden EINMAL ummantelt. Der Mantel ist die einzige
|
||
Stelle, an der die Regel steht -- und er kann keinen Rueckgabeweg
|
||
uebersehen, weil er hinter allen sitzt.
|
||
|
||
Die Namen nach aussen bleiben gleich: Kein Aufrufer muss geaendert
|
||
werden, und niemand kann versehentlich die ungefilterte Fassung
|
||
benutzen -- die `Roh`-Funktionen werden nicht exportiert.
|
||
===================================================================== */
|
||
/* =====================================================================
|
||
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.
|
||
===================================================================== */
|
||
/* WER AUF DER TEAM-ADRESSE UEBERHAUPT VORKOMMT.
|
||
*
|
||
* Die Community steht seit dem 11.09.2026 mit drin (Filipe: "das soll
|
||
* keine app fuer sich sein, das soll in der crew seite adaptiert
|
||
* werden"). Der Treff wohnt jetzt hier, also muessen seine Mitglieder
|
||
* hier auch verwaltet werden koennen -- sonst legt DogFather einen
|
||
* Zugang an und er verschwindet im selben Moment aus der Liste.
|
||
*
|
||
* DAS MACHT SIE NICHT ZU KOLLEGEN. Diese Menge beantwortet nur "wer
|
||
* kommt auf dieser Adresse vor". Ob jemand in eine Einladung, eine
|
||
* Aufgabe oder einen Chat darf, beantwortet ohneAussen() weiter unten
|
||
* -- 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. */
|
||
/* 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) {
|
||
const haus = person?.haus;
|
||
if (haus !== "crew" && haus !== "agentur") return ids;
|
||
try {
|
||
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
|
||
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") {
|
||
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
|
||
ein `null` ("sieht alles") zu einer echten Liste aus, sobald es etwas
|
||
zu verbergen gibt -- und zwar zur Liste ALLER Personen, weil sie
|
||
nicht wissen kann, wovon "alles" gerade handelt. Fuer DogFather
|
||
greift das nie (er verbirgt nichts vor sich selbst), fuer Spicy
|
||
Media schon: Bei ihr kam die Liste mit Admins, Managern und Scouts
|
||
darin zurueck.
|
||
|
||
GEMESSEN, NICHT VERMUTET: Am 11.09.2026 bekam Spicy Media auf der
|
||
Zahlen-Seite "Agentur, Filipe, Luna, Max, NeuerCreator,
|
||
NeuerManager" zur Auswahl -- DogFather dagegen nur "Luna,
|
||
NeuerCreator". Aufgefallen ist es, weil eine Pruefung die beiden
|
||
Listen VERGLICHEN hat, statt bei jeder einzeln "ist nicht leer" zu
|
||
sagen.
|
||
|
||
WARUM DIE REPARATUR HIER STEHT UND NICHT BEIM AUFRUFER: Es gibt
|
||
zehn Aufrufer. Jeder von ihnen baut daraus ein `IN (...)`, und jeder
|
||
haette die Rolle selbst danebenschreiben muessen -- neun Stellen
|
||
richtig und eine vergessen sieht man nie. Der Name der Funktion ist
|
||
das Versprechen; es gehoert dorthin eingeloest, wo der Name steht. */
|
||
const nurCreatorIds = (ids) => {
|
||
if (ids === null || !ids.length) return ids;
|
||
try {
|
||
return db().prepare(
|
||
`SELECT id FROM personen WHERE rolle = 'creator' AND id IN (${ids.map(() => "?").join(",")})`)
|
||
.all(...ids).map((z) => z.id);
|
||
} catch { return ids; }
|
||
};
|
||
|
||
/* WER VON AUSSEN KOMMT, SIEHT NIEMANDEN (11.09.2026).
|
||
|
||
Filipe: "egal welche rolle oder person hinzugefuegt wird soll immer
|
||
nur das sehen wass ich erlaube."
|
||
|
||
GEFUNDEN, NICHT UEBERLEGT. Eine Zeile in pruef-treff behauptete
|
||
"Personen: nichts" und wurde rot. Nachgemessen bekam ein Gast:
|
||
|
||
{"personen":[{"id":1,"name":"Filipe","rolle":"admin", ...},
|
||
{"id":5,"name":"Kessi","rolle":"gast", ...}]}
|
||
|
||
Also DogFathers Namen und seine Rolle. Nicht dramatisch -- ein
|
||
Streamer ist kein Geheimnis -- aber es war eine Auskunft, die
|
||
niemand erlaubt hatte, und genau das ist der Punkt. Die Liste kam
|
||
zustande, weil `sichtbarePersonenIdsRoh` fuer jede Rolle DogFather
|
||
mitgibt (er ist das Gegenueber von allem) und die vier Umhuellungen
|
||
danach nichts daran aendern.
|
||
|
||
`nurEigene` steht deshalb GANZ AUSSEN -- nach allen anderen. Wer
|
||
innen ansetzt, muss darauf vertrauen, dass keine spaetere Huelle
|
||
wieder etwas hinzufuegt; aussen gilt es unabhaengig davon, was
|
||
innen passiert. Und es gilt fuer alle vier Listen gleich, statt
|
||
viermal einzeln.
|
||
|
||
`null` heisst bei diesen Funktionen "alle" -- deshalb wird es hier
|
||
ausdruecklich zu einer Liste mit genau einem Eintrag. Ein `null`
|
||
durchzureichen hiesse fuer eine Aussenrolle das Gegenteil von dem,
|
||
was hier steht. */
|
||
function nurEigene(ids, person) {
|
||
if (!AUSSEN_ROLLEN.has(person?.rolle)) return ids;
|
||
return person?.id ? [person.id] : [];
|
||
}
|
||
|
||
/* =====================================================================
|
||
WER VON AUSSEN IST, STEHT IN KEINER ARBEITSLISTE (11.09.2026)
|
||
=====================================================================
|
||
|
||
nurEigene() beantwortet "was sieht ein Mitglied der Community?".
|
||
Diese Huelle beantwortet die andere Richtung: "steht ein Mitglied der
|
||
Community in der Liste, die jemand ANDERS bekommt?"
|
||
|
||
DER ANLASS: Seit es die Rolle gibt, taucht sie in jeder Liste auf,
|
||
die "alle" bedeutet -- und fuer DogFather bedeutet fast jede Liste
|
||
"alle" (siehtAlles). Ein Mitglied der Community stand damit in der
|
||
Auswahl, wen man zu einem Termin einlaedt, wem man eine Aufgabe gibt
|
||
und mit wem man einen Chat anfaengt. Nichts davon ist vorgesehen, es
|
||
gab dafuer nie eine Entscheidung, und aufgefallen waere es zum ersten
|
||
Mal, wenn jemand versehentlich einen Zuschauer zu einer internen
|
||
Besprechung einlaedt.
|
||
|
||
WARUM ALS HUELLE UND NICHT IN DEN VIER FUNKTIONEN: Weil sie dann auch
|
||
fuer die Listen gilt, die es noch nicht gibt. Dieselbe Ueberlegung
|
||
steht schon bei nurHaus() und bei der Modi-Huelle in
|
||
workspace-bereiche.js -- und beide Male war der erste Anlauf ein
|
||
einzelner geflickter Fall, der den naechsten offen liess.
|
||
|
||
NULL HEISST "ALLE" -- und sobald hier etwas herausfaellt, darf es das
|
||
nicht mehr heissen. Die Liste wird deshalb AUSGESCHRIEBEN, so wie
|
||
ohneVerborgene() und nurHaus() es auch tun.
|
||
|
||
EIN FEHLSCHLAG SCHLIESST. Bei einer Datenbankstoerung eine
|
||
ungefilterte Liste zurueckzugeben hiesse, die Community genau dann in
|
||
die Einladungsauswahl zu setzen, wenn ohnehin gerade etwas nicht
|
||
stimmt. Eine leere Liste ist sichtbar falsch, eine zu volle nicht.
|
||
|
||
AUSDRUECKLICH NICHT ANGEWENDET auf sichtbarePersonenIds(): Das ist
|
||
die Verwaltungsliste, in der DogFather Zugaenge anlegt und Codes neu
|
||
setzt. Dort MUSS die Community stehen. Die Einladungsliste leitet
|
||
sich zwar davon ab -- aber sie bekommt diese Huelle aussen herum,
|
||
und damit ist die Verwaltung offen und die Zusammenarbeit zu. */
|
||
function ohneAussen(ids) {
|
||
const rollen = [...AUSSEN_ROLLEN];
|
||
if (!rollen.length) return ids;
|
||
try {
|
||
const platz = rollen.map(() => "?").join(", ");
|
||
const aussen = db().prepare(
|
||
`SELECT id FROM personen WHERE rolle IN (${platz})`).all(...rollen).map((z) => z.id);
|
||
if (!aussen.length) return ids;
|
||
if (ids === null) {
|
||
return db().prepare(
|
||
`SELECT id FROM personen WHERE rolle NOT IN (${platz})`).all(...rollen).map((z) => z.id);
|
||
}
|
||
const menge = new Set(aussen);
|
||
return ids.filter((i) => !menge.has(i));
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
export const sichtbareCreatorIds = (person) => ohneAussen(nurEigene(nurCreatorIds(nurHaus(ohneVerborgene(sichtbareCreatorIdsRoh(person), person), person)), person));
|
||
export const sichtbarePersonenIds = (person) => nurEigene(nurHaus(ohneVerborgene(sichtbarePersonenIdsRoh(person), person), person), person);
|
||
export const betreuerIds = (person) => ohneAussen(nurEigene(nurHaus(ohneVerborgene(betreuerIdsRoh(person), person), person), person));
|
||
export const einladbareIds = (person) => ohneAussen(nurEigene(nurHaus(ohneVerborgene(einladbareIdsRoh(person), person), person), person));
|
||
/* SICH SELBST NICHT -- UND ZWAR AUCH DANN NICHT, WENN DIE LISTE
|
||
AUSGESCHRIEBEN WIRD (10.09.2026).
|
||
|
||
schreibbareIdsRoh() endet mit `basis.delete(person.id)`. Das greift
|
||
aber nur, solange eine Liste gebaut wird. Wer `siehtAlles` hat,
|
||
bekam `null` = jeder, und die Route setzte dafuer `id <> ?` ein --
|
||
auch richtig.
|
||
|
||
Dazwischen liegt der Fall, den beide nicht abdecken: Sobald es
|
||
jemanden zu VERBERGEN gibt, schreibt ohneVerborgene() aus `null`
|
||
eine echte Liste -- und die enthaelt alle ausser den Verborgenen,
|
||
also auch die fragende Person selbst. Spicy Media stand dadurch in
|
||
der eigenen Auswahl, und darfSchreibenMit() haette ein Gespraech mit
|
||
sich selbst durchgelassen.
|
||
|
||
Aufgefallen ist das nicht beim Lesen des Codes, sondern an einer
|
||
Zeile Messausgabe: "Spicy Media hat es NICHT (VanVan, Filipe,
|
||
Cigdem)" -- der erste Name war ihr eigener. Der Fall entsteht erst,
|
||
seit es ueberhaupt jemanden zu verbergen gibt; vorher konnte es ihn
|
||
nicht geben. */
|
||
export const schreibbareIds = (person) => {
|
||
/* ==== DIE COMMUNITY SCHREIBT NIEMANDEM EINZELN (19.09.2026) ========
|
||
|
||
GEFUNDEN VON pruef-treffchat, NICHT BEIM LESEN. Ein Gast konnte
|
||
ein Zweier-Gespraech mit DogFather aufmachen -- die Route
|
||
antwortete mit 200 und legte den Raum an.
|
||
|
||
WARUM `ohneAussen` DAS NICHT VERHINDERT HAT: Diese Huelle nimmt
|
||
die Community aus den Listen ANDERER heraus. Sie sorgt dafuer,
|
||
dass ein Creator keinen Zuschauer anschreiben kann. Die
|
||
Gegenrichtung -- ein Zuschauer, der IN die Liste schaut -- kam nie
|
||
vor, weil die Chatseite fuer 'gast' gar nicht offen war.
|
||
|
||
SEIT HEUTE IST SIE OFFEN, und damit war die Erlaubnis da. Nicht,
|
||
weil jemand sie erteilt haette, sondern weil eine Aenderung an
|
||
einer ganz anderen Stelle sie freigelegt hat. Genau die Sorte
|
||
Erlaubnis, vor der ich im Anruf-Riegel zwei Dateien weiter
|
||
ausdruecklich warne -- und hier waere ich selbst hineingelaufen.
|
||
|
||
WAS DARAN SCHLIMM IST: Ein Zweier-Gespraech ist kein Kanal. Es hat
|
||
keine Nachtruhe (die gilt fuer den Treff), kein Modi liest mit,
|
||
und niemand koennte moderieren. Aus "die Community bekommt einen
|
||
Chat" waere ungewollt "jeder Zuschauer bekommt eine Standleitung
|
||
zu DogFather" geworden.
|
||
|
||
WAS SIE STATTDESSEN HAT: den Treff -- einen Raum, in dem Modis
|
||
dabei sind -- und den vertraulichen Meldeweg fuer alles, was
|
||
niemanden sonst angeht. Beides ist besser als eine Privatleitung:
|
||
Das eine wird moderiert, das andere hat eine Zustaendigkeit. */
|
||
if (AUSSEN_ROLLEN.has(person?.rolle)) return [];
|
||
|
||
const ids = ohneAussen(nurHaus(ohneVerborgene(schreibbareIdsRoh(person), person), person));
|
||
return ids === null ? null : ids.filter((i) => i !== person?.id);
|
||
};
|
||
export const pipelineIds = (person) => ohneAussen(nurHaus(ohneVerborgene(pipelineIdsRoh(person), person), person));
|