Siebter Durchgang durch die Team-Dogi-Seiten aus Benutzersicht. Alles am
Code belegt, die Groessenangaben nachgerechnet.
=== DIE ANMELDUNG ===
WOHER BEKOMMT MAN EINEN CODE? Stand nirgends. Vier Kacheln, ein Feld,
ein Knopf -- wer keinen Code hatte, fand keinen einzigen Satz dazu. Fuer
die Community besonders teuer: Sie ist die einzige Rolle, die von
aussen kommt und niemanden im Haus kennt.
"BITTE KURZ WARTEN" SIND BIS ZU ZEHN MINUTEN (VERSUCHE_FENSTER_MIN).
Wer nach zwanzig Sekunden nachsieht und dieselbe Meldung liest, haelt
die Seite fuer kaputt. Jetzt steht die Zahl da.
EIN RICHTIGER CODE AN DER FALSCHEN KACHEL fiel in dieselbe Absage wie
ein erfundener -- der Server sucht nur unter der gewaehlten Rolle. Wer
aus der Community kommt und auf "Modi" tippt (das klingt ja danach),
tippt seinen richtigen Code dreimal Buchstabe fuer Buchstabe. Ab dem
zweiten Fehlversuch kommt jetzt ein Hinweis auf die Auswahl -- ohne zu
sagen, welche die richtige waere.
=== DER ANRUF: VIER FEHLER, EINE ERFAHRUNG ===
DAS FREIZEICHEN LIEF UNENDLICH. Es gab keinen Zeitgeber, der aufhoert.
Wer niemanden erreichte, sass vor einem piependen Kasten mit "02:47"
darauf, als telefoniere er laengst. Der Server schickt den Verfallszeit-
punkt sogar mit (`laeuft_bis`) -- benutzt hat ihn nie jemand.
DER KLINGELKASTEN VERSCHWAND NACH 46 SEKUNDEN, waehrend der Server 120
gibt. Wer beim Klingeln das Handy aus der Tasche holt und in Sekunde 50
hinsieht, fand nichts mehr -- kein Kasten, keine Knoepfe -- waehrend der
Anrufer noch siebzig Sekunden Freizeichen hoerte. Die 120 Sekunden sind
im Server ausdruecklich damit begruendet, dass man Zeit zum Entsperren
braucht; eine Zeile im Browser hat diese Entscheidung aufgehoben.
DAS FREIZEICHEN PIEPTE WEITER, waehrend daneben "Keine Verbindung."
stand. Ton sagte "es klingelt noch", Text sagte "vorbei".
DIE UHR ZAEHLTE AB DEM WAEHLEN. Wer 30 Sekunden klingeln liess und 12
Sekunden sprach, las im Kasten "00:42" und danach im Chat "Anruf . 12
Sekunden". Der Server rechnet es richtig; der Kasten hat es wieder
kaputtgemacht.
Dazu: Die Mikrofon-Absage verwies auf "das Schloss-Symbol neben der
Adresse" -- in der installierten App gibt es weder Adresszeile noch
Schloss. Das Haus kennt diesen Fehler und hat ihn beim Weg zur
Anruf-Probe schon behoben; hier stand er noch.
Und die Anruf-Probe: Ihr einziger Punkt ohne Handlungsanweisung war
"Antwort 503." -- auf einer Seite, die ausdruecklich fuer jemanden
gebaut ist, der nicht technisch ist. Ihr Rueckweg fuehrte ausserdem
immer in den Chat, auch wenn man von der Startseite kam.
=== DER TREFF ===
EIN EINZEILER MACHTE SECHS ERKLAERTEXTE UNSICHTBAR. bewerben.js las
`g.unter`, der Server schickt `g.text` -- also immer undefined. Damit
fehlten ALLE SECHS Gruppeneinleitungen des Fragebogens ("Kein Test mit
richtigen Antworten"), und die CSS-Regel dafuer lief ins Leere. Uebrig
blieben sechs nackte Ueberschriften ueber fuenfzehn Fragen.
DIE EINZIGE "SO LAEUFT DAS HIER"-SEITE WAR FAST UNERREICHBAR. Die
Kachel "Regeln & Hilfe" zeigte auf ein Brett, das am ersten Tag leer
ist. Die Regeln, die es wirklich gibt (Begruessung, fuenf Regeln, drei
Stufen, Mindestalter), stehen auf treff-regeln.html -- erreichbar ueber
genau einen kleinen Textlink auf einer Brettseite. Die Kachel zeigt
jetzt dorthin.
FIEL /api/treff/lage AUS, versprach die Seite einem Zuschauer
Schreibrechte, die der Server ablehnt: Die Pruefung fiel bei `lage ===
null` auf `true` durch -- von "darf auf keinem Brett" auf "darf
ueberall". Formular auf, getippt, 403.
Dazu: hilfe.js hatte eine eigene kleine Fehlertabelle fuer zwei
Kennungen und endete sonst bei "Ging nicht." -- ausgerechnet die Seite,
auf der jemand mit einem Problem sitzt, hatte die inhaltsleerste Absage
im Haus. Und der gesperrte "Neue Meldung"-Knopf erklaerte sich nur im
Maus-Tooltip; die Community kommt von TikTok, also vom Handy.
=== DIE STARTSEITE ===
DIE COMMUNITY BEKAM EIN WORT. Unter dem Titel steht bei jeder Rolle ein
Satz -- beim Community-Mitglied stand "Community". Woertlich derselbe
Mangel, den der Kommentar bei MODI_ROLLENTEXT beschreibt und fuer den
Modi behebt.
DER RING MELDETE 100 %, bevor man irgendetwas getan hat. Am ersten Tag,
ohne eine einzige Aufgabe, stand das groesste Element der Seite auf
"100 % -- HEUTE NICHTS OFFEN". Gelesen wird zuerst die Zahl, und 100 %
heisst fuer jeden "fertig", nicht "leer".
UND BEI EINER STOERUNG BLIEB "wird geladen" FUER IMMER STEHEN -- der
fruehe `return` liess den Platzhalter unberuehrt.
=== DIE ZUGANGSVERWALTUNG ===
Der Code-Kasten sagte "jetzt weitergeben" und gab die Haelfte nicht
mit, die man weitergeben muss: WO sich die Person anmeldet. DogFather
ruft den Code durchs Zimmer, der andere fragt "und wo?". Die Adresse
kommt jetzt vom Server (anmeldeAdresseFuer, abgeleitet aus derselben
Weiche), dazu ein Knopf "Code und Adresse kopieren".
Er verschwieg ausserdem den Rettungsweg: "danach nicht mehr abrufbar"
stimmt fuer DIESEN Code -- man kann jederzeit einen neuen vergeben. Wer
das nicht weiss, glaubt, er habe einen Menschen angelegt, der nie
hereinkommt. Und das Fenster liess sich wortlos schliessen, waehrend
der Code offen stand.
=== OPTIK: NACHGERECHNET, NICHT VERMUTET ===
DIE GLOCKE STAND NICHT IN DER LISTE. Sie ist ein echtes <button> und
wurde von der 44-px-Regel erfasst, waehrend ihre vier Nachbarn durch
einen staerkeren Selektor auf 36 px gehen. Zwischen 401 und 560 px --
also bei 412 px (haeufigste Android-Breite) und 430 px (iPhone Pro Max)
-- stand eine 44 px hohe runde Pille zwischen fuenf 36-px-Quadraten.
Der 400er-Block listet sie korrekt; genau dieses Band fiel durch beide
Rechnungen. Dasselbe Muster wie am 06.09.
DIE 9,6-px-REPARATUR IM KALENDER WAR SEIT WOCHEN WIRKUNGSLOS.
kalender.css wird NACH start.css geladen, beide Selektoren sind gleich
stark -- also gewann `.6rem` auf jeder Breite unter 760 px. Genau der
Wert, den start.css als behobenen Fehler protokolliert.
Und die Hebung selbst unterschritt ihre eigene Grenze: Die Regel, die
zu kleine Schrift hochziehen soll, zog `.k-pille` auf 11,2 px -- 0,3 px
unter die 11,5, die 33 Zeilen darueber aufgestellt werden.
Weitere nachgerechnete Groessen: Spaltenkopf und KW-Spalte im Kalender
(10,56 px), Kalenderwoche am Balken (9,92 px), Wecker-Knopf (30 px --
der kleinste im Haus, 14 unter der Vorgabe), Chat-Zurueck auf dem
Tablet (34x34, 40 % weniger Flaeche als noetig, und der einzige Weg
zurueck in die Liste), Emoji-Knoepfe (34 px bei 2 px Abstand -- wer
danebentippt, verschickt ein anderes Zeichen, und das ist sofort
abgeschickt).
IM WINDOWS-KONTRASTMODUS HATTE DIE STARTSEITE KEINE UEBERSCHRIFT.
`.ztitel` und der Team-Dogi-Schriftzug sind mit `background-clip: text`
gesetzt; faellt der Verlauf weg, bleibt durchsichtiger Text. Beide
haben jetzt den Rueckfall, den gate.css schon lange hat.
UND MEIN EIGENES STREIFENRASTER rechnete die Spaltenzahl aus der Breite
statt aus den Daten (`auto-fit`): Bei 390 px passten sechs, alles
darueber fiel in eine zweite, unbeschriftete Zeile. Das `overflow-x`
daneben konnte nie greifen -- ein auto-fit-Raster wird nie breiter als
sein Kasten. Der Kommentar beschrieb ein Verhalten, das es nicht gab.
GEMESSEN: pruef-meldungen 8/0, pruef-css-klassen ALLES IN ORDNUNG,
pruef-rechtetafel 19/0, alle Module laden, keine doppelten
Kachelnamen/-ziele/-farben in allen vier Rollen.
Co-Authored-By: Claude Opus 5 <[email protected]>
6404 lines
305 KiB
JavaScript
6404 lines
305 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 { sitzungPasstZurAdresse, istOhneModiAdresse, istCrewAdresse, istPruefAdresse, AUSSEN_ROLLEN,
|
||
TEAM_DOGI_ROLLEN } 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 };
|
||
|
||
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";
|
||
const SITZUNG_STUNDEN = 12;
|
||
const VERSUCHE_MAX = 8; // pro IP
|
||
const VERSUCHE_FENSTER_MIN = 10;
|
||
const ROLLEN = new Set(["spicy", "admin", "manager", "scout", "creator", "hand", "modi", "gast"]);
|
||
|
||
/* 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", "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. */
|
||
const CREW_KACHEL = new Set(["admin", "hand", "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 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"],
|
||
};
|
||
|
||
/** 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. */
|
||
if (person.haus !== "crew") return alle;
|
||
return alle.filter((r) => HAUS_TEAM_ROLLEN.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.
|
||
*/
|
||
export const darfRollenWechseln = (person) =>
|
||
!!person && (person.rolle === "admin" || person.rolle === "spicy");
|
||
|
||
/* 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" || person.rolle === "hand");
|
||
|
||
/* =======================================================================
|
||
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. */
|
||
const AGENTUR_ROLLEN = new Set(["spicy", "manager", "scout", "creator"]);
|
||
|
||
/* Die gemeinsame Bauweise beider Filter. Sie stand bis zum 10.09.2026
|
||
nur einmal da, in ohneTeamDogi -- und beim zweiten Haus waere sie
|
||
abgeschrieben worden. Eine Abschrift ist hier besonders teuer: In ihr
|
||
steckt die NULL-Falle (siehe unten), und die sieht man einer Kopie
|
||
nicht an. */
|
||
function ohneRollen(praefix, spalten, rollen) {
|
||
const liste = [...rollen].map((r) => `'${r}'`).join(", ");
|
||
return spalten
|
||
.map((sp) => `(${praefix}.${sp} IS NULL OR ${praefix}.${sp} NOT IN `
|
||
+ `(SELECT id FROM personen WHERE rolle IN (${liste})))`)
|
||
.join(" AND ");
|
||
}
|
||
|
||
/* =====================================================================
|
||
NUR DAS HAUS VON TEAM DOGI (10.09.2026)
|
||
|
||
Das Gegenstueck zu ohneTeamDogi -- und ABSICHTLICH nach demselben
|
||
Muster gebaut: "keine Spalte zeigt auf jemanden aus dem anderen
|
||
Haus".
|
||
|
||
WARUM NICHT "MINDESTENS EINE SPALTE ZEIGT AUF TEAM DOGI": Das waere
|
||
die naheliegende Formulierung und sie waere falsch. Eine Aufgabe, die
|
||
DogFather fuer einen Creator anlegt, haette dann ueber `erstellt_von`
|
||
(er selbst gehoert zum Haus) trotzdem gepasst -- und stuende auf der
|
||
Team-Seite, obwohl sie die Agentur betrifft. Andersherum stimmt es:
|
||
Sobald IRGENDEINE Spalte auf einen Creator, Scout, Manager oder
|
||
Spicy Media zeigt, gehoert die Zeile ins andere Haus.
|
||
|
||
Zeilen ohne jede Zuordnung (alle Spalten NULL) bleiben sichtbar. Das
|
||
ist richtig: Sie gehoeren dem, der sie geschrieben hat, und ueber die
|
||
Regel darueber steht ohnehin schon, wer das sein darf.
|
||
===================================================================== */
|
||
export function ohneAgentur(praefix, spalten = ["creator_id", "erstellt_von"]) {
|
||
return ohneRollen(praefix, spalten, AGENTUR_ROLLEN);
|
||
}
|
||
|
||
export function ohneTeamDogi(praefix, spalten = ["creator_id", "erstellt_von"]) {
|
||
/* Die Rollenliste kommt aus TEAM_DOGI_ROLLEN und steht nicht hier als
|
||
Text. Sie hiess bis zum 10.09.2026 fest `rolle = 'modi'` -- mit dem
|
||
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);
|
||
}
|
||
|
||
/** Darf diese Person ueberhaupt etwas sehen, das einem Modi gehoert?
|
||
* Nur die DogFather-Rolle und die Modis selbst. */
|
||
export const siehtModis = (person) => !!person
|
||
&& (person.rolle === "admin" || TEAM_DOGI_ROLLEN.has(person.rolle));
|
||
|
||
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",
|
||
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",
|
||
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, Review", zeichen: "aufgaben",
|
||
ton: 13, 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" },
|
||
{ gruppe: "Für dich", gruppeUnter: "Deine Seite im Team",
|
||
name: "Wissen", unter: "Nachschlagen statt nachfragen", zeichen: "wissen",
|
||
ton: 9, ziel: "wissen.html", szene: "arena" },
|
||
];
|
||
|
||
/**
|
||
* Die 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: "Der Treff \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, "Der Treff" 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]
|
||
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 = [
|
||
{ ...TREFF_GRUPPE, name: "Der Treff", unter: "Hallo sagen, fragen, loben",
|
||
zeichen: "community", ton: 34, gross: true, ziel: "bereich.html?b=treff", 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: "Treff-Chat", unter: "Miteinander reden – nachts schläft er",
|
||
zeichen: "chat", ton: 41, ziel: "chat.html?raum=treff", szene: "lounge" },
|
||
{ ...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" },
|
||
{ ...TREFF_GRUPPE, name: "Highlights", unter: "Eure Clips und Bilder",
|
||
zeichen: "live", ton: 29, ziel: "bereich.html?b=highlight", szene: "arena" },
|
||
{ ...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. */
|
||
zeichen: "schutz", ton: 28, ziel: "treff-regeln.html", szene: "studio" },
|
||
{ ...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, "Der Treff" 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: "Unsere Seiten", unter: "Website, Shop & dein Rabatt",
|
||
zeichen: "hinaus", ton: 38, ziel: "unsere-seiten.html", szene: "portal" },
|
||
];
|
||
|
||
/* 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 Treff \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 = TREFF_BEREICHE
|
||
.map((k) => new URL("http://x/" + k.ziel).searchParams.get("b"))
|
||
.filter(Boolean);
|
||
|
||
/* 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", "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. */
|
||
const HILFE_KACHEL = {
|
||
gruppe: "Für dich", gruppeUnter: "Deine eigene Seite",
|
||
name: "Vertraulich melden", unter: "Nur DogFather und die rechte Hand",
|
||
zeichen: "schutz", ton: 39, 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",
|
||
};
|
||
|
||
const MODI_MIT_TREFF = [...MODI_BEREICHE, BEFINDEN_KACHEL,
|
||
MEINE_AUFGABEN_KACHEL, MEINE_ENTWICKLUNG_KACHEL, ...TREFF_BEREICHE,
|
||
TREFF_MODERATION_KACHEL];
|
||
const HAND_MIT_TREFF = [...HAND_BEREICHE, ...TREFF_BEREICHE,
|
||
TREFF_MODERATION_KACHEL, HILFE_KACHEL];
|
||
|
||
/* 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) => {
|
||
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];
|
||
|
||
export function bereicheFuer(person) {
|
||
if (person?.rolle === "modi") return MODI_MIT_TREFF;
|
||
if (person?.rolle === "hand") 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));
|
||
|
||
/** 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";
|
||
|
||
/* =====================================================================
|
||
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.
|
||
===================================================================== */
|
||
export const KANAELE = [
|
||
{ wert: "dogfather", name: "DogFather", handle: "@dogfather0804",
|
||
text: "Der Hauptkanal. Die Livestreams und alles, was unmittelbar "
|
||
+ "daran hängt." },
|
||
{ wert: "hasidog", name: "HasiDog", handle: "@hasidog00804",
|
||
text: "Der zweite Account — eigene Zuschauer, eigener Ton. Was hier "
|
||
+ "läuft, ist nicht einfach dasselbe noch einmal." },
|
||
{ wert: "clips", name: "DogFather Clips", handle: "@dogis.modi.gang",
|
||
text: "Nur Ausschnitte. Hier wird geschnitten, beschriftet und "
|
||
+ "hochgeladen, nicht gesendet." },
|
||
];
|
||
|
||
/** 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) {
|
||
if (!person) return [];
|
||
return TEAM_DOGI_ROLLEN.has(person.rolle) || person.rolle === "admin"
|
||
? KANAELE : [];
|
||
}
|
||
|
||
export function kategorienFuer(person) {
|
||
if (!person) return [];
|
||
return TEAM_DOGI_ROLLEN.has(person.rolle) || person.rolle === "admin"
|
||
? 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" && person.rolle !== "hand") 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";
|
||
|
||
export function rollentextFuer(person) {
|
||
if (person?.rolle === "modi") return MODI_ROLLENTEXT;
|
||
if (person?.rolle === "hand") return HAND_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 Treff";
|
||
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;
|
||
}
|
||
|
||
/* =====================================================================
|
||
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",
|
||
};
|
||
|
||
const ZUSATZ_BEREICHE = {
|
||
admin: [
|
||
{ ...TEAM_GRUPPE, ...EINGANG_KACHEL, gruppe: TEAM_GRUPPE.gruppe,
|
||
gruppeUnter: TEAM_GRUPPE.gruppeUnter },
|
||
{
|
||
...TEAM_GRUPPE,
|
||
name: "Ideen-Board", unter: "Gesammelt und nach Dringlichkeit sortiert",
|
||
zeichen: "content", ton: 23, ziel: "bereich.html?b=ideen", szene: "portal",
|
||
},
|
||
/* DIESELBE Kachel wie im Team, nur in seiner Gruppe. Name, Ton und
|
||
Ziel sind Wort fuer Wort dieselben -- wer sie hier anders nennt,
|
||
hat zwei Namen fuer eine Sache, und das Team liest den einen,
|
||
DogFather den anderen. */
|
||
{
|
||
...TEAM_GRUPPE,
|
||
name: "Rückmeldung", unter: "Was gut läuft, was hakt – in beide Richtungen",
|
||
zeichen: "reports", ton: 25, ziel: "bereich.html?b=rueckmeldung", szene: "lounge",
|
||
},
|
||
/* Eine eigene Gruppe, direkt hinter Team Dogi: Was ueber Menschen
|
||
aufgeschrieben wird, steht nicht zwischen den taeglichen
|
||
Werkzeugen. */
|
||
/* WER SIEHT WAS (11.09.2026).
|
||
|
||
Filipe: "egal welche Rolle oder Person hinzugefuegt wird, soll
|
||
immer nur das sehen, was ich erlaube."
|
||
|
||
Dazu gehoert, dass er es nachsehen kann. Die Seite rechnet die
|
||
Uebersicht aus derselben Tafel, aus der die Schranke ihre
|
||
Entscheidung holt (rechte.js) -- eine von Hand gepflegte
|
||
Uebersicht waere eine zweite Wahrheit, und ausgerechnet die, der
|
||
man glaubt.
|
||
|
||
IN "Team & System" UND NICHT BEI DEN PERSONEN: Es geht nicht um
|
||
Menschen, sondern um das Haus. */
|
||
{
|
||
...TEAM_GRUPPE, ...RECHTE_KACHEL,
|
||
gruppe: TEAM_GRUPPE.gruppe, gruppeUnter: TEAM_GRUPPE.gruppeUnter,
|
||
},
|
||
{ ...ENTWICKLUNG_KACHEL, gruppeNach: TEAM_GRUPPE.gruppe },
|
||
{ ...WERDEGANG_KACHEL, gruppeNach: TEAM_GRUPPE.gruppe },
|
||
{ ...TALENTE_KACHEL, gruppeNach: TEAM_GRUPPE.gruppe },
|
||
/* DER TREFF -- GANZ UNTEN, UND ZWAR ABSICHTLICH.
|
||
|
||
Filipe zu den beiden Gruppen darueber: "das sind private
|
||
informationen von meiner community." Dasselbe gilt hier, nur
|
||
noch deutlicher: Im Treff stehen Namen und Saetze von Leuten,
|
||
die nichts mit der Agentur zu tun haben.
|
||
|
||
Ohne `gruppeNach` haengt eine Gruppe hinten an -- genau richtig.
|
||
Wer sich den Bildschirm teilt oder jemandem ueber die Schulter
|
||
schauen laesst, hat sie dann nicht als Erstes im Blick. */
|
||
...TREFF_BEREICHE,
|
||
TREFF_MODERATION_KACHEL,
|
||
],
|
||
};
|
||
|
||
/* NICHT AUF DER TEAM-ADRESSE (10.09.2026). Dort liefert bereicheFuer()
|
||
ohnehin schon die Kacheln des Teams -- Eingang, Ideen-Board und
|
||
Rueckmeldung stuenden sonst zweimal auf der Seite, einmal in
|
||
"Taeglich" und einmal in "Team Dogi". Zwei gleiche Kacheln sind
|
||
schlimmer als eine fehlende: Man klickt die falsche und sucht den
|
||
Unterschied. */
|
||
export function zusatzBereicheFuer(person) {
|
||
if (person?.haus === "crew") return [];
|
||
return ZUSATZ_BEREICHE[person?.rolle] || [];
|
||
}
|
||
|
||
/* 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.
|
||
===================================================================== */
|
||
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");
|
||
}
|
||
}
|
||
|
||
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 ----
|
||
|
||
"Der Treff -- 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"],
|
||
|
||
/* ---- 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". */
|
||
["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"],
|
||
|
||
/* 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"],
|
||
|
||
/* 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"],
|
||
|
||
/* 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"],
|
||
|
||
/* 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"],
|
||
]) {
|
||
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);
|
||
}
|
||
}
|
||
|
||
/* 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);
|
||
}
|
||
}
|
||
|
||
/* 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. */
|
||
const aufgabenPlan = d.prepare(
|
||
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'aufgaben'").get()?.sql || "";
|
||
if (aufgabenPlan && !aufgabenPlan.includes("'abgebrochen'")) {
|
||
const sicherung = `${DB_PFAD}.vor-abbruch-${jetztStempel}`;
|
||
try {
|
||
d.exec(`VACUUM INTO '${sicherung.replace(/'/g, "''")}'`);
|
||
console.log("[workspace] Sicherung vor der Umstellung:", sicherung);
|
||
} catch (fehler) {
|
||
console.error("[workspace] Sicherung fehlgeschlagen, Umstellung abgebrochen:", fehler?.message);
|
||
return;
|
||
}
|
||
|
||
d.exec("PRAGMA foreign_keys = OFF");
|
||
try {
|
||
/* Die Spalten stehen hier vollstaendig, weil die Schleife oben
|
||
sie zu diesem Zeitpunkt schon ergaenzt hat. Wer hier eine
|
||
vergisst, verliert ihren Inhalt still -- deshalb wird nach dem
|
||
Tausch die Zeilenzahl verglichen. */
|
||
const vorher = d.prepare("SELECT COUNT(*) AS n FROM aufgaben").get().n;
|
||
d.exec("BEGIN");
|
||
d.exec(`
|
||
CREATE TABLE aufgaben_neu (
|
||
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,
|
||
creator_extern TEXT,
|
||
verantwortlich_extern TEXT,
|
||
abbruch_grund TEXT,
|
||
abgebrochen_am TEXT,
|
||
abbruch_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
status_vorher TEXT
|
||
);
|
||
INSERT INTO aufgaben_neu
|
||
(id, titel, beschreibung, status, prioritaet, creator_id, verantwortlich_id,
|
||
frist, erstellt, erstellt_von, geaendert, erledigt_am,
|
||
creator_extern, verantwortlich_extern,
|
||
abbruch_grund, abgebrochen_am, abbruch_von, status_vorher)
|
||
SELECT id, titel, beschreibung, status, prioritaet, creator_id, verantwortlich_id,
|
||
frist, erstellt, erstellt_von, geaendert, erledigt_am,
|
||
creator_extern, verantwortlich_extern,
|
||
abbruch_grund, abgebrochen_am, abbruch_von, status_vorher
|
||
FROM aufgaben;
|
||
DROP TABLE aufgaben;
|
||
ALTER TABLE aufgaben_neu RENAME TO aufgaben;
|
||
CREATE INDEX IF NOT EXISTS idx_aufgaben_status ON aufgaben (status);
|
||
CREATE INDEX IF NOT EXISTS idx_aufgaben_creator ON aufgaben (creator_id);
|
||
`);
|
||
/* Nach dem RENAME heisst die neue Tabelle wieder "aufgaben" --
|
||
gezaehlt wird also unter dem alten Namen, und der Vergleich
|
||
laeuft noch INNERHALB der Transaktion. Stimmt er nicht, ist
|
||
ein ROLLBACK noch moeglich. */
|
||
const nachher = d.prepare("SELECT COUNT(*) AS n FROM aufgaben").get().n;
|
||
/* Stimmt die Zahl nicht, wird NICHT bestaetigt. Lieber laeuft das
|
||
Abbrechen noch nicht, als dass eine Aufgabe verschwindet. */
|
||
if (nachher !== vorher) {
|
||
d.exec("ROLLBACK");
|
||
console.error(`[workspace] Umstellung abgebrochen: ${vorher} Aufgaben vorher, `
|
||
+ `${nachher} nachher. Sicherung: ${sicherung}`);
|
||
} else {
|
||
d.exec("COMMIT");
|
||
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] Status 'abgebrochen' freigeschaltet, ${vorher} Aufgaben, Verweise geprueft.`);
|
||
}
|
||
}
|
||
} catch (fehler) {
|
||
try { d.exec("ROLLBACK"); } catch { /* schon zurueckgerollt */ }
|
||
console.error("[workspace] Umstellung 'abgebrochen' fehlgeschlagen:", fehler?.message);
|
||
} finally {
|
||
d.exec("PRAGMA foreign_keys = ON");
|
||
}
|
||
}
|
||
|
||
/* ---- 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 ---- */
|
||
const bauplan = d.prepare(
|
||
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'personen'").get()?.sql || "";
|
||
if (!bauplan.includes("'manager'")) {
|
||
const sicherung = `${DB_PFAD}.vor-manager-${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 der Manager
|
||
noch nicht, als dass Daten ohne Netz angefasst werden. */
|
||
console.error("[workspace] Sicherung fehlgeschlagen, Umstellung abgebrochen:", fehler?.message);
|
||
return;
|
||
}
|
||
|
||
/* 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 {
|
||
d.exec("BEGIN");
|
||
d.exec(`
|
||
CREATE TABLE personen_neu (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name TEXT NOT NULL,
|
||
rolle TEXT NOT NULL CHECK (rolle IN ('admin','manager','scout','creator')),
|
||
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
|
||
);
|
||
INSERT INTO personen_neu
|
||
(id, name, rolle, code_hash, code_salt, code_n, aktiv, erstellt, letzter_login)
|
||
SELECT id, name, rolle, code_hash, code_salt, code_n, aktiv, erstellt, letzter_login
|
||
FROM personen;
|
||
DROP TABLE personen;
|
||
ALTER TABLE personen_neu RENAME TO personen;
|
||
`);
|
||
d.exec("COMMIT");
|
||
|
||
/* Nach dem Tausch pruefen, ob die Verweise noch stimmen. Findet
|
||
sich etwas, wird das laut gemeldet -- stillschweigend kaputte
|
||
Verweise waeren das Schlimmste an dieser Stelle. */
|
||
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] Rolle 'manager' freigeschaltet, Verweise geprueft.");
|
||
}
|
||
} catch (fehler) {
|
||
try { d.exec("ROLLBACK"); } catch { /* schon zurueckgerollt */ }
|
||
console.error("[workspace] Umstellung fehlgeschlagen:", fehler?.message);
|
||
} finally {
|
||
d.exec("PRAGMA foreign_keys = ON");
|
||
}
|
||
}
|
||
|
||
/* =====================================================================
|
||
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);
|
||
|
||
/* 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);
|
||
|
||
/* =================================================================
|
||
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);
|
||
|
||
/* 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);
|
||
|
||
/* 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 Treffs 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);
|
||
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;
|
||
}
|
||
/* 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. */
|
||
const haus = istCrewAdresse(req.get?.("host")) ? "crew"
|
||
: "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_STUNDEN * 3600_000).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_STUNDEN * 3600_000,
|
||
});
|
||
}
|
||
|
||
/* ---------- 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. */
|
||
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),
|
||
/* 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),
|
||
/* 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")),
|
||
/* 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 n Tage verschoben. */
|
||
export function tagLokal(versatz) {
|
||
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. */
|
||
if (person.haus === "crew" && person.rolle !== "admin") {
|
||
return {
|
||
wo: `(${eigen}) AND ${ohneAgentur(p, ["creator_id", "teilnehmer_id", "erstellt_von"])}`,
|
||
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;
|
||
}
|
||
}
|
||
|
||
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);
|
||
protokolliere("einstellung_geaendert", {
|
||
personId: akteur?.id ?? null, rolle: akteur?.rolle ?? null, ip: akteur?.ip ?? null,
|
||
detail: `${schluessel} = ${wert}`.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. */
|
||
const HAUS_TEAM_ROLLEN = new Set(["admin", "hand", "modi", "gast"]);
|
||
|
||
function nurHaus(ids, person) {
|
||
if (person?.haus !== "crew") return ids;
|
||
try {
|
||
const liste = [...HAUS_TEAM_ROLLEN].map((r) => `'${r}'`).join(", ");
|
||
const erlaubt = db().prepare(
|
||
`SELECT id FROM personen WHERE rolle IN (${liste})`).all().map((z) => z.id);
|
||
/* `null` heisst bei diesen Funktionen "alle". Sobald das Haus
|
||
einschraenkt, darf es das nicht mehr heissen -- die Liste wird
|
||
deshalb AUSGESCHRIEBEN, genau wie ohneVerborgene() es tut. */
|
||
if (ids === null) return erlaubt;
|
||
const menge = new Set(erlaubt);
|
||
return ids.filter((i) => menge.has(i));
|
||
} catch {
|
||
/* EIN FEHLSCHLAG SCHLIESST, ER OEFFNET NICHT. Gaebe es hier `ids`
|
||
zurueck, waere bei einer Datenbankstoerung ploetzlich die ganze
|
||
Agentur auf der Team-Adresse zu sehen -- und niemand haette eine
|
||
Fehlermeldung gesehen. Eine leere Liste ist sichtbar falsch, eine
|
||
zu volle nicht. */
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/* DASSELBE ALS SQL-SCHNIPSEL -- fuer die zwei Abfragen im Haus, die
|
||
bewusst NICHT ueber die Listenfunktionen gehen.
|
||
|
||
Das ist der Ring in der Zentrale (er holt sich das ganze Haus selbst;
|
||
im Kommentar dort steht ausdruecklich, dass er die einzige solche
|
||
Stelle ist) und die Hinweiszeile darunter. Beide zaehlen Menschen,
|
||
und beide standen auf der Team-Adresse mit Zahlen aus der Agentur da
|
||
-- "9 IM TEAM", obwohl das Team drei Leute hat. Filipe hat genau
|
||
darauf gezeigt.
|
||
|
||
EINE FUNKTION UND KEINE ZWEITE ROLLENLISTE: Wer den Ausdruck
|
||
abschreibt, hat beim naechsten Rollenwechsel zwei Wahrheiten. */
|
||
export function hausBedingung(person, spalte = "rolle") {
|
||
if (person?.haus !== "crew") return "";
|
||
const liste = [...HAUS_TEAM_ROLLEN].map((r) => `'${r}'`).join(", ");
|
||
return ` AND ${spalte} IN (${liste})`;
|
||
}
|
||
|
||
/* 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));
|