Filipe: „die rechte hand soll das auch sehen. und die selben rechte
da haben wie dogfather. das einzige was sie nicht kann ist die
dogfather rolle oder leute anfassen. also da kann sie nichts
verändern."
ZUERST EIN IRRTUM VON MIR. Ich hatte den Bildschirmfoto-Ausschnitt
fuer die Rechtetafel gehalten und dort gebaut. „Vertritt dich im
Alltag und koordiniert das Team" steht aber in workspace-personen.js:
Gemeint war die PERSONENSEITE. Die Arbeit an der Rechtetafel ist
trotzdem drin (siehe unten) -- sie loeste dasselbe Problem an einer
zweiten Stelle.
=== DIE PERSONENSEITE ===
DIE OBERFLAECHE WAR STRENGER ALS DER SERVER. In personen.js stand
`if (ich.rolle === 'hand') { keine Knoepfe }` mit der Begruendung
„Der Server antwortet ihr auf jeden davon mit 404". Am 22.09. stimmte
das. Seither wurde der Server ZWEIMAL erweitert -- sie durfte
Personen anlegen, Codes neu erzeugen und Rollen aendern -- und diese
Zeile blieb stehen. Sie hatte drei Rechte und sah keinen einzigen
Knopf. Ein Rollenvergleich im Browser ist genau die zweite Wahrheit,
die still veraltet.
Jetzt fragt die Oberflaeche den Server (`darf_zugaenge_verwalten`,
`darf_personen_loeschen`, `rollen_anfassbar`). Dazu kommen SPERREN
und LOESCHEN, die bis heute ausdruecklich bei DogFather lagen --
Filipes „das einzige" ist juenger und eindeutig.
Die Knoepfe „Neuer Code" und „Sperren" standen inline im
DogFather-Zweig; sie sind jetzt Funktionen und werden von beiden
Stellen benutzt. Eine zweite Abschrift waere die geworden, die beim
naechsten Umbau nur halb nachgezogen wird.
ZWEI ECHTE LOECHER FAND DIE NEUE PRUEFUNG:
* `PUT /personen/:id/rolle` hatte `nurDogFatherBeiLeitung` NICHT.
Bis heute folgenlos; mit den neuen Rechten konnte die rechte Hand
darueber die Rolle eines MANAGERS aendern -- waehrend derselbe
Manager fuer DogFather auf crew. gar nicht in der Liste steht.
Zwei Wege, zwei Antworten, und der laxere galt fuer die Rolle mit
weniger Rechten.
* Die Loesch-Route hatte dieselbe Middleware ebenfalls nicht. Das
war harmlos, solange die Route selbst nur DogFather durchliess --
seit die rechte Hand loescht, ist es die Stelle, an der DogFather
geschuetzt wird.
=== DIE RECHTETAFEL (nicht bestellt, aber dasselbe Problem) ===
Dort durfte sie sehen, aber nichts umstellen. Jetzt umstellen wie
DogFather -- ausser der Spalte „DogFather" und der Zeile „Personen &
Zugaenge" (wer die freischaltet, hat Zugaenge vergeben, ohne einen
anzulegen). Zuruecksetzen bleibt bei DogFather: Der Knopf naehme
genau diese zwei Sperren mit, und eine Sperre, die ein zweiter Knopf
daneben aufhebt, ist keine.
Sie kann sich auch selbst nicht aussperren -- das hat er nicht
gesagt, aber eine Sperre, aus der man sich aussperren kann, ist eine
Falle. Ein festes Feld ist jetzt gar kein Knopf mehr und nennt den
Grund, der fuer DIESE Person gilt.
=== DREI MESSFEHLER VON MIR ===
Ein Manager ist auf crew. fuer die LISTE unsichtbar, fuer DogFathers
direkten Zugriff aber nicht -- ich hielt das eine fuer das andere und
erwartete, dass beide abgewiesen werden. Die Loesch-Vorschau (GET)
fehlte in meinem Waechter. Und `rollen_anfassbar` beantwortet eine
andere Frage als „wen sehe ich".
Gemessen: pruef-hand-personen, 40 Pruefungen, 0 Fehler -- jeder der
fuenf Wege einzeln gegen DogFather und die linke Hand, plus die
Gegenprobe, dass DogFather es kann. pruef-rechte-umstellen von 46 auf
56 Pruefungen. Gruen: pruef-personen-liste, pruef-personen-loeschen,
pruef-personen-kachel, pruef-rollen-anlegen, pruef-rechtetafel.
Co-Authored-By: Claude Opus 5 <[email protected]>
7709 lines
370 KiB
JavaScript
7709 lines
370 KiB
JavaScript
/* =====================================================================
|
||
workspace.js — Anmeldung und Sitzungen für den Creator Workspace
|
||
(/workspace). Eigenständiger Router, nach dem Muster von gate.js und
|
||
webdesign-gate.js.
|
||
|
||
BEWUSST OHNE NEUE ABHÄNGIGKEITEN. Node 24 bringt `node:sqlite` mit, das
|
||
Hashen und die Zufallswerte kommen aus `node:crypto`. Damit gibt es
|
||
nichts zu kompilieren, nichts zu aktualisieren und keine fremde
|
||
Lieferkette in einem Bereich, in dem es um Zugangsdaten geht.
|
||
|
||
WICHTIG — dieses Modul darf die Website niemals mitreißen. Es läuft im
|
||
selben Prozess wie dogfather-universe.com. Deshalb:
|
||
* beim Laden wird KEINE Datenbank geöffnet (siehe `db()` weiter unten),
|
||
* jede Route fängt ihre Fehler selbst ab,
|
||
* schlägt die Datenbank fehl, antwortet nur /workspace/api/* mit 503 —
|
||
die restliche Seite merkt davon nichts.
|
||
|
||
Sicherheitsentscheidungen und ihre Gründe stehen jeweils direkt an der
|
||
betreffenden Stelle.
|
||
===================================================================== */
|
||
|
||
import express from "express";
|
||
import {
|
||
randomBytes, scryptSync, timingSafeEqual, createHash, createHmac,
|
||
} from "node:crypto";
|
||
import { dirname, join } from "node:path";
|
||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||
import { mkdirSync } from "node:fs";
|
||
import { createRequire } from "node:module";
|
||
import { darfSeite, OHNE_ANMELDUNG, seitenFuer } from "./rechte.js";
|
||
/* Ein Modul ohne eigene Importe -- deshalb entsteht hier kein Kreis,
|
||
obwohl workspace-treff.js seinerseits aus dieser Datei importiert.
|
||
Begruendung im Kopf von treff-tabellen.js. */
|
||
import { treffTabellen, treffSperreLesen, MINDESTALTER } from "./treff-tabellen.js";
|
||
import { hilfeTabellen } from "./hilfe-tabellen.js";
|
||
import { supportTabellen } from "./support-tabellen.js";
|
||
import { sitzungPasstZurAdresse, istOhneModiAdresse, istCrewAdresse, istPruefAdresse, AUSSEN_ROLLEN,
|
||
TEAM_DOGI_ROLLEN, CREW_ADRESSE } 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";
|
||
/* ===== WIE LANGE GILT EINE ANMELDUNG? (20.09.2026) ==================
|
||
|
||
Filipe: "kannst du bitte machen dass die leute sich nur einmal
|
||
anmelden muessen und dan nur noch abgemeldet werden wenn sie sich
|
||
selbst abmelden. damit sie auch die benarichtigungen sofort kriegen
|
||
... und auch sofort die anrufe annehmen koennen."
|
||
|
||
VORHER: zwoelf Stunden, feste Frist ab dem Anmelden. Wer morgens um
|
||
acht anfing, flog abends um acht raus -- mitten im Betrieb. Und wer
|
||
abgemeldet ist, hat die Seite nicht offen; ein Anruf erreicht ihn
|
||
dann nur noch ueber die Benachrichtigung, und bis er sich wieder
|
||
angemeldet hat, ist das Klingeln vorbei.
|
||
|
||
JETZT: ein GLEITENDES Fenster. Wer die Seite benutzt, bleibt
|
||
angemeldet -- ohne Ende, ohne Nachfrage. Abgemeldet wird nur, wer
|
||
sich selbst abmeldet.
|
||
|
||
WARUM NICHT "unendlich": Ein Zugang, der nie ablaeuft, ist auf einem
|
||
verlorenen Handy fuer immer offen -- und in diesem Haus liegen
|
||
vertrauliche Meldungen ueber Menschen. Wer ein halbes Jahr nicht da
|
||
war, meldet sich neu an; das ist niemandem zuzumuten zu viel und
|
||
schliesst das verlorene Geraet.
|
||
|
||
WARUM NICHT BEI JEDEM AUFRUF NEU SCHREIBEN: Das waere ein
|
||
Schreibzugriff auf die Datenbank bei jedem Bild, jeder Abfrage,
|
||
jedem Herzschlag des Ereignisstroms. Verlaengert wird erst, wenn
|
||
weniger als die Haelfte des Fensters uebrig ist -- also hoechstens
|
||
einmal alle drei Monate je Geraet.
|
||
==================================================================== */
|
||
const SITZUNG_TAGE = 180;
|
||
const SITZUNG_MS = SITZUNG_TAGE * 86400_000;
|
||
const VERSUCHE_MAX = 8; // pro IP
|
||
const VERSUCHE_FENSTER_MIN = 10;
|
||
const ROLLEN = new Set(["spicy", "admin", "manager", "scout", "creator",
|
||
"hand", "linke", "modi", "gast"]);
|
||
|
||
/* ===== DIE LINKE HAND (21.09.2026) ==================================
|
||
|
||
Filipe, aus dem Auftrag: *"Die linke Hand erhaelt grundsaetzlich
|
||
dieselben Rechte wie die rechte Hand in allen Bereichen, die die
|
||
Modis, Aufgaben, Aufgabenzuweisungen, Uebersichten, Kommunikation
|
||
und Auswertungen betreffen. Die linke Hand darf keine neuen Personen
|
||
oder Accounts hinzufuegen. Die linke Hand darf keine privaten bzw.
|
||
persoenlichen Bereiche oder Informationen von Dogfather sehen."*
|
||
|
||
DESHALB EINE MENGE UND NICHT DREISSIG ABSCHRIFTEN. Die Rolle "hand"
|
||
steht an 35 Stellen im Servercode. Haette ich ueberall ein "linke"
|
||
danebengeschrieben, waere die naechste Stelle, die jemand
|
||
hinzufuegt, die erste, die es vergisst -- und eine vergessene Stelle
|
||
heisst hier: Die linke Hand sieht etwas nicht, ohne dass jemand
|
||
merkt, warum. Genau diese Falle hat das Haus am 11.09. schon einmal
|
||
Datenspalten gekostet.
|
||
|
||
WAS AUSDRUECKLICH NICHT DAZUGEHOERT -- und warum jedes einzeln:
|
||
|
||
- Personen anlegen (ANLEGBAR) ausdruecklich im Auftrag
|
||
- Bewerbungen lesen das ist der Weg, auf dem
|
||
Personen hinzukommen
|
||
- Rollen aendern Kontoverwaltung, nicht
|
||
Aufgabenverwaltung
|
||
- der vertrauliche Meldeweg das Empfindlichste im Haus;
|
||
im Zweifel das kleinere Recht.
|
||
Ein Wort von Filipe kehrt das
|
||
um -- eine versehentlich
|
||
gelesene Meldung nicht.
|
||
|
||
Die Menge steht hier oben, VOR allem, was sie benutzt. */
|
||
export const WIE_RECHTE_HAND = new Set(["hand", "linke"]);
|
||
|
||
/** Hat diese Person die Team-Rechte einer Hand (rechts oder links)? */
|
||
export const istHand = (person) => !!person && WIE_RECHTE_HAND.has(person.rolle);
|
||
|
||
/* DIE EINE REIHENFOLGE, IN DER ROLLEN UEBERALL ERSCHEINEN.
|
||
|
||
Sie steht hier einmal, damit keine Liste eine eigene erfindet.
|
||
|
||
DIE RECHTE HAND STEHT AN ZWEITER STELLE (Filipe, 11.09.2026:
|
||
"rechte hand soll immer als erstes sein. ueberall. ... ausser bei
|
||
personen und zugaengen soll sie ueber jedem aber unter dogfather
|
||
sein. ich will dass auch ueberall immer nach der hirarchie
|
||
gearbeitet wird.").
|
||
|
||
EINE ZAHL STATT ZWEIER REGELN: Er hat zwei Saetze gesagt, aber es
|
||
braucht nur eine Reihenfolge. In den Team-Listen kommt DogFather
|
||
gar nicht vor (er beurteilt, er wird nicht beurteilt) -- dort steht
|
||
sie damit automatisch ganz vorn. In "Personen & Zugaenge" steht er
|
||
drin, also steht sie dort hinter ihm. Zwei Sonderfaelle waeren zwei
|
||
Stellen, an denen es auseinanderlaufen kann.
|
||
|
||
SPICY MEDIA BLEIBT DAVOR -- seine ausdrueckliche Entscheidung auf
|
||
Nachfrage am 11.09.2026. Die Agentur ist keine Stufe in seinem Team,
|
||
sondern steht daneben; an Rechten aendert die Reihenfolge ohnehin
|
||
nichts, sie sortiert nur Listen.
|
||
|
||
'gast' (Community) steht mit Absicht NICHT in der Liste: Sie faellt
|
||
dadurch ans Ende (siehe ELSE unten), und das ist richtig -- die
|
||
Community ist keine Stufe im Team. */
|
||
export const ROLLEN_REIHE = ["spicy", "admin", "hand", "linke", "manager",
|
||
"scout", "creator", "modi"];
|
||
|
||
/* DIE ROLLEN MIT EINER KACHEL AUF DER ANMELDESEITE.
|
||
|
||
'modi' steht hier NICHT drin, und das ist der Kern des verborgenen
|
||
Zugangs: Es gibt keine sechste Kachel, und es laesst sich auch keine
|
||
erzwingen. Wer von aussen `rolle: "modi"` schickt, bekommt genau
|
||
dieselbe Antwort wie bei einer erfundenen Rolle -- er erfaehrt also
|
||
nicht einmal, dass es sie gibt.
|
||
|
||
Zwei Listen statt einer, weil es zwei verschiedene Fragen sind:
|
||
ROLLEN sagt, welche Rollen es GIBT (die Datenbank laesst nur diese
|
||
zu). Diese hier sagt, mit welchen man sich ANMELDEN kann, indem man
|
||
sie anklickt. Waere es eine Liste, haette 'modi' entweder eine Kachel
|
||
-- oder es gaebe die Rolle gar nicht. */
|
||
const ROLLEN_KACHEL = new Set(["spicy", "admin", "manager", "scout", "creator"]);
|
||
|
||
|
||
/** Die Rollen mit einer Kachel auf der Zugangswand von crew.
|
||
*
|
||
* DREI, seit Filipe am 10.09.2026 sagte: "3 rollen. dogfather. rechte
|
||
* hand und modis." Auf dieser Adresse gibt es also wieder etwas zu
|
||
* waehlen -- anders als in der Fassung davor, wo nur Modis
|
||
* hereinkamen und eine Kachelreihe mit einem Eintrag eine Huerde
|
||
* gewesen waere.
|
||
*
|
||
* DogFather steht auf BEIDEN Waenden. Er ist die einzige Person, die
|
||
* in beiden Welten arbeitet; das ist keine Ausnahme von der Trennung,
|
||
* sondern ihr Sinn.
|
||
*
|
||
* VIER, SEIT DER TREFF HIER WOHNT (Entscheidung Filipe, 11.09.2026:
|
||
* "das soll keine app fuer sich sein, das soll in der crew seite
|
||
* adaptiert werden" -- und auf die Frage, wie die Community durch
|
||
* diese Wand kommt: "vierte Kachel Community").
|
||
*
|
||
* WAS DAS KOSTET, und es gehoert hierhin und nicht in eine Fussnote:
|
||
* Ein Mitglied der Community liest vor seiner Anmeldung die Namen der
|
||
* drei Team-Rollen. Die Alternative waere gewesen, die Kachelreihe
|
||
* ganz abzuschaffen und allein den Code entscheiden zu lassen -- dann
|
||
* haette niemand eine Rolle gesehen, aber Filipes Entscheidung vom
|
||
* 10.09. waere wieder umgeworfen worden. Er hat sich fuer die vierte
|
||
* Kachel entschieden; der Preis ist damit bewusst bezahlt und nicht
|
||
* uebersehen. */
|
||
/* Die linke Hand meldet sich ueber dieselbe Wand an wie die rechte. */
|
||
const CREW_KACHEL = new Set(["admin", "hand", "linke", "modi", "gast"]);
|
||
|
||
/** Die Rollen mit einer Kachel auf der Zugangswand von treff.
|
||
*
|
||
* EINE, und deshalb zeigt die Wand dort gar keine Kachelreihe: Eine
|
||
* Auswahl mit einem einzigen Eintrag ist keine Auswahl, sondern eine
|
||
* Huerde. Dieselbe Ueberlegung wie bei der ersten Fassung der
|
||
* Crew-Wand -- nur ist sie hier dauerhaft richtig, weil es im Treff
|
||
* auch kuenftig nur eine Rolle gibt.
|
||
*
|
||
* Die Menge steht trotzdem hier: Die Anmeldung prueft gegen sie, und
|
||
* eine Wand ohne Kacheln darf nicht heissen, dass jede Rolle
|
||
* durchkommt. */
|
||
|
||
|
||
/* Als SQL-Ausdruck fuer ORDER BY. "ORDER BY rolle" waere alphabetisch
|
||
(admin, creator, manager, scout, spicy) -- also fast genau falsch
|
||
herum.
|
||
|
||
ABGELEITET UND NICHT ABGESCHRIEBEN (11.09.2026). Bis heute stand die
|
||
Reihenfolge zweimal da: einmal als Liste, einmal als CASE mit
|
||
Zahlen. Beim Hochziehen der rechten Hand haetten beide geaendert
|
||
werden muessen -- und eine gepflegte Liste, die mit einer zweiten
|
||
uebereinstimmen muss, ist genau die Bauart, an der im Projekt schon
|
||
dreimal etwas verlorengegangen ist (zuletzt drei Spalten beim
|
||
Tabellenumbau). Eine Liste, die niemand pflegt, kann nicht veralten.
|
||
|
||
Das Wort "rolle" kommt hier genau EINMAL vor -- in "CASE rolle".
|
||
Darauf verlassen sich die Aufrufer, die es per .replace() auf
|
||
"p.rolle" umstellen; kein Rollenname enthaelt die Zeichenfolge. */
|
||
export const ROLLEN_SORTIERUNG = "CASE rolle "
|
||
+ ROLLEN_REIHE.map((r, i) => `WHEN '${r}' THEN ${i}`).join(" ")
|
||
+ ` ELSE ${ROLLEN_REIHE.length} END`;
|
||
|
||
/* LEITUNG = DogFather und Manager. Ein Manager darf alles, was
|
||
DogFather darf -- mit genau zwei Ausnahmen, die in
|
||
workspace-personen.js stehen: Er kann keine Leitung anlegen und keine
|
||
Leitung veraendern. Sonst koennte er sich selbst zum DogFather machen
|
||
oder den echten aussperren. "Nur DogFather hat alle endgueltigen
|
||
Rechte" heisst genau das. */
|
||
const LEITUNG = new Set(["spicy", "admin", "manager"]);
|
||
export const istLeitung = (person) => !!person && LEITUNG.has(person.rolle);
|
||
export const istDogFather = (person) => !!person && person.rolle === "admin";
|
||
|
||
/* ===== WER DARF AUFGABEN AN ANDERE VERTEILEN? (20.09.2026) ==========
|
||
|
||
Filipe: "jeder soll sich selber aufgabe vergeben koennen aber nur die
|
||
rechte hand und dogfather aufgaben an andere verteilen."
|
||
|
||
WARUM EINE EIGENE REGEL UND NICHT `hand` IN DIE LEITUNG:
|
||
`istLeitung` wird an 57 Stellen in 19 Dateien gefragt -- unter
|
||
anderem im vertraulichen Meldeweg, in der Personenverwaltung und in
|
||
den Auswertungen ueber Menschen. Die rechte Hand dort ueberall
|
||
mitzunehmen waere eine Entscheidung ueber Einsicht in fremde
|
||
Meldungen, und die hat niemand getroffen.
|
||
|
||
Verteilen ist eine Frage fuer sich. Sie bekommt deshalb ihren
|
||
eigenen Namen: Man sieht an der Aufrufstelle, worum es geht, und
|
||
wer sie spaeter aendert, aendert genau diese eine Sache.
|
||
|
||
spicy und manager bleiben drin: Auf der Agenturseite verteilen sie
|
||
seit jeher Aufgaben, und daran soll sich nichts aendern. */
|
||
/** Wer eine Aufgabe ANLEGEN darf.
|
||
*
|
||
* IM TEAM DOGI KOMMEN AUFGABEN VON DER LEITUNG (22.09.2026). Filipe:
|
||
* "sie sich nicht selber aufgaben geben" und "die modis sollen nur
|
||
* diese aufgaben wo sie von den drauf markiert werden annehmen
|
||
* koennen".
|
||
*
|
||
* Das gilt AUSDRUECKLICH NUR im Team Dogi. In der Agentur legt
|
||
* weiterhin jeder fuer sich selbst an -- dort ist eine Aufgabe eine
|
||
* Notiz an sich selbst, kein Auftrag von jemandem. Eine Regel, die
|
||
* beide Haeuser ueber einen Kamm schert, waere hier falsch. */
|
||
export const darfAufgabenAnlegen = (person) => {
|
||
if (!person) return false;
|
||
if (TEAM_DOGI_ROLLEN.has(person.rolle)) return darfAufgabenVerteilen(person);
|
||
return true;
|
||
};
|
||
|
||
export const darfAufgabenVerteilen = (person) =>
|
||
istLeitung(person) || istHand(person);
|
||
|
||
/* =====================================================================
|
||
WER ENTSCHEIDET, WER EINE AUFGABE MACHT (22.09.2026)
|
||
=====================================================================
|
||
Filipe: „die modis und linke hand sollen da nichts uebernehmen
|
||
koennen von aufgaben, ueberhaupt ueberall sollen die keine aufgaben
|
||
selber uebernehmen die ihnen nicht zugetragen sind. ich will dass die
|
||
sich fuer aufgaben bewerben koennen aber die rechte hand oder
|
||
dogfather muessen annehmen oder ablehnen koennen und das mit einem
|
||
kommentar als moeglichkeit sogar noch zum hinzufuegen."
|
||
|
||
DAS IST EINE ANDERE FRAGE ALS `darfAufgabenVerteilen`, und die zwei
|
||
auseinanderzuhalten ist der ganze Punkt:
|
||
|
||
verteilen -> eine Aufgabe an ANDERE geben
|
||
entscheiden -> bestimmen, wer sie am Ende macht
|
||
|
||
Die linke Hand darf verteilen (sein Wort vom selben Tag: „rechte hand
|
||
linke hand und dogfather ... koennen alle verteilen"), aber NICHT
|
||
entscheiden, ob sie selbst eine bekommt. Sie bewirbt sich wie ein
|
||
Modi, und die rechte Hand oder DogFather sagen ja oder nein.
|
||
|
||
GEMESSEN, WARUM ES NOETIG WAR: `istHand()` fasst die rechte UND die
|
||
linke Hand -- die linke konnte sich damit bisher Aufgaben aus dem
|
||
Pool selbst nehmen und sich beim Verteilen selbst eintragen.
|
||
|
||
Rolle istHand darfVerteilen (vorher)
|
||
admin false true
|
||
hand true true
|
||
linke true true <-- genau das soll weg
|
||
modi false false
|
||
|
||
DREI STELLEN HAENGEN DARAN, und alle drei fragen ab hier dieselbe
|
||
Funktion:
|
||
1. aus dem Pool uebernehmen
|
||
2. sich beim Verteilen selbst eintragen
|
||
3. ueber eine Bewerbung entscheiden
|
||
|
||
Eine Aufzaehlung der Rollen an drei Stellen waere die, bei der die
|
||
dritte beim naechsten Umbau vergessen wird. */
|
||
export const entscheidetUeberAufgaben = (person) =>
|
||
istLeitung(person) || person?.rolle === "hand";
|
||
|
||
/* ===== WER DARF EINE ROLLE AENDERN -- UND ZU WELCHER? (20.09.2026) ==
|
||
|
||
Filipe: "ich will dass ich da auch die rollen der leute wechseln kann
|
||
ohne dass ich denen einen neuen account machen muss mit einem neuen
|
||
code. einfach rolle wechseln und gut ist. perfektionier das fuer die
|
||
rolle dogfather und rechte hand. nur die sollen das machen koennen."
|
||
|
||
Das Wechseln selbst gab es schon -- der Zugangscode bleibt dabei
|
||
derselbe, das stand auch in der Rueckfrage. Es konnte nur DogFather.
|
||
|
||
DREI GRENZEN, UND JEDE HAT EINEN GRUND:
|
||
|
||
(1) NIEMAND WIRD ZU "admin". Das gilt seit dem 11.09.2026 fuer alle,
|
||
auch fuer DogFather selbst -- siehe ANLEGBAR. Hier aendert sich
|
||
daran nichts.
|
||
|
||
(2) DIE RECHTE HAND FASST KEINE ROLLE AN, DIE UEBER ODER NEBEN IHR
|
||
STEHT. Sie kann also einen Modi zur Community machen und
|
||
umgekehrt -- aber keinen DogFather antasten und keine zweite
|
||
rechte Hand ernennen. Wer jemanden auf die eigene Ebene hebt,
|
||
vergibt Vertrauen, das ihm nicht gehoert; das bleibt bei
|
||
DogFather. Und wer eine Rolle UEBER sich aendern koennte,
|
||
koennte sich selbst befoerdern, indem er zuerst den anderen
|
||
herabstuft.
|
||
|
||
(3) NIEMAND AENDERT DIE EIGENE ROLLE. Sonst waere jede Grenze oben
|
||
nur ein Umweg: erst sich selbst hochstufen, dann alles duerfen.
|
||
|
||
Was sie danach vergeben darf, steht in `rollenZumAendern` -- an
|
||
EINER Stelle, damit die Oberflaeche keine zweite Liste fuehrt. Genau
|
||
daran ist es am 10.09.2026 schon einmal gescheitert. */
|
||
const ROLLEN_ZUM_AENDERN = {
|
||
/* DogFather: alles, was er auch anlegen darf. */
|
||
admin: null, // null = dieselbe Liste wie ANLEGBAR
|
||
/* SPICY MEDIA STAND HIER SCHON, BEVOR ES DIESE TABELLE GAB -- sie
|
||
durfte Rollen wechseln, seit es den Weg gibt. Sie muss deshalb
|
||
drinstehen, sonst nimmt die neue Regel ihr etwas weg, das niemand
|
||
ihr wegnehmen wollte. `null` heisst: dasselbe wie beim Anlegen,
|
||
also Agenturrollen. */
|
||
spicy: null,
|
||
hand: ["modi", "gast"],
|
||
};
|
||
|
||
/** Welche Rollen darf diese Person an ANDERE vergeben? */
|
||
export function rollenZumAendern(person) {
|
||
if (!person) return [];
|
||
const eigen = ROLLEN_ZUM_AENDERN[person.rolle];
|
||
if (eigen === undefined) return [];
|
||
return eigen === null ? darfAnlegen(person) : [...eigen];
|
||
}
|
||
|
||
/** Darf diese Person die Rolle DIESES Menschen anfassen? */
|
||
export function darfRolleAendern(person, ziel) {
|
||
if (!person || !ziel) return false;
|
||
if (person.id === ziel.id) return false; // (3)
|
||
if (!rollenZumAendern(person).length) return false;
|
||
/* DOGFATHER DARF JEDEN ANFASSEN -- auch einen zweiten DogFather.
|
||
Hier stand `return ziel.rolle !== "admin"`, und das war zu scharf:
|
||
Es machte aus "DogFather ist nicht mehr VERGEBBAR" ein "an einem
|
||
DogFather ist nichts mehr zu aendern". Gemessen hat es
|
||
pruef-haus-trennung -- "solange es zwei gibt, darf einer wechseln"
|
||
bekam 403 statt 200. Ein zweiter Zugang liess sich damit nicht mehr
|
||
zurueckstufen; er waere fuer immer DogFather geblieben.
|
||
|
||
Die beiden echten Gefahren haengen woanders und bleiben: Niemand
|
||
KANN 'admin' vergeben (steht nicht in rollenZumAendern), und der
|
||
LETZTE DogFather laesst sich nicht herabstufen (Sicherung in der
|
||
Route). Die eigene Zeile ist drei Zeilen darueber schon
|
||
ausgeschlossen. */
|
||
if (person.rolle === "admin") return true;
|
||
/* Spicy Media wie bisher: das ganze Haus ausser DogFather. */
|
||
if (person.rolle === "spicy") return ziel.rolle !== "admin";
|
||
if (person.rolle === "hand") {
|
||
/* (2): nur unter sich -- nicht admin, nicht eine andere hand. */
|
||
return ziel.rolle !== "admin" && ziel.rolle !== "hand";
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/* =======================================================================
|
||
WER DARF WEN ANLEGEN — die einzige Liste dazu (10.09.2026)
|
||
|
||
Filipe, mit Bildschirmfoto der Zugaenge-Seite: "die spicy rolle soll
|
||
auch manager und scouts hinzufuegen koennen."
|
||
|
||
Sie konnte es nicht. Der Grund war NICHT ein fehlendes Recht, sondern
|
||
ZWEI Listen, die einander widersprachen — in derselben Funktion, drei
|
||
Zeilen auseinander (personen.js):
|
||
|
||
const darf = ... : ich.rolle === 'spicy' ? ['manager', 'creator']
|
||
...
|
||
if ((r.wert === 'admin' || r.wert === 'manager') && ich.rolle !== 'admin') continue;
|
||
|
||
Die erste Zeile erlaubt Spicy Media einen Manager, die zweite nimmt
|
||
ihn wieder weg. Uebrig blieb genau ein Knopf: Creator. Serverseitig
|
||
war der Weg fuer den Manager die ganze Zeit offen — es gab nur keinen
|
||
Knopf dafuer.
|
||
|
||
Das ist im Haus die immer gleiche Sorte Fehler: zwei Stellen fuer
|
||
dieselbe Aussage, und die spaetere gewinnt still. Deshalb steht die
|
||
Antwort ab jetzt EINMAL hier, und alle fragen sie:
|
||
|
||
- die drei Anlege-Wege in workspace-personen.js
|
||
- die Oberflaeche, ueber `darf_anlegen` in /workspace/api/ich
|
||
|
||
Die Oberflaeche hat damit gar keine eigene Liste mehr. Sie kann
|
||
deshalb auch nicht mehr von der des Servers abweichen — weder zu
|
||
streng (ein Recht, das niemand findet) noch zu grosszuegig (ein Knopf,
|
||
der eine Absage bringt).
|
||
|
||
WARUM SPICY MEDIA SCOUTS ANLEGEN DARF, ABER KEINE LEITUNG:
|
||
Ein Scout und ein Manager arbeiten unter Spicy Media — sie
|
||
einzustellen ist genau die Aufgabe dieser Rolle. Einen DogFather
|
||
anzulegen ist es nicht: Das waere ein zweiter Zugang mit allen
|
||
endgueltigen Rechten, und den vergibt nur DogFather selbst.
|
||
======================================================================= */
|
||
const ANLEGBAR = {
|
||
/* DogFather: alles ausser der eigenen Rolle.
|
||
|
||
DIE ROLLE "admin" IST SEIT DEM 11.09.2026 NICHT MEHR VERGEBBAR --
|
||
von niemandem, auch nicht von DogFather selbst. Filipe: "dogfather
|
||
soll man nicht auswaehlen koennen. das ist die einzige die man
|
||
nicht auswaehlen kann bitte."
|
||
|
||
WAS DAS BEDEUTET, damit es niemand spaeter sucht: Es laesst sich
|
||
kein zweiter DogFather-Zugang mehr anlegen, und niemand laesst
|
||
sich zu einem befoerdern. Der bestehende bleibt unberuehrt und ist
|
||
durch Sicherung 3 (nie den letzten DogFather herabstufen)
|
||
geschuetzt -- er kann also nicht versehentlich verschwinden.
|
||
Zurueckdrehen laesst sich das nur hier in dieser Liste.
|
||
|
||
Nebenwirkung, und sie ist erwuenscht: Damit gibt es keinen Weg
|
||
mehr, sich ueber die Oberflaeche zur hoechsten Rolle zu machen. */
|
||
/* DASS AUCH "hand", "modi" UND "gast" DARIN STEHEN, IST ENTSCHIEDEN --
|
||
nicht uebersehen (11.09.2026, ausdrueckliche Nachfrage, Antwort
|
||
"so lassen").
|
||
|
||
Sie gehoeren zu Team Dogi. Wer damit auf workspace. jemanden
|
||
umstellt, schiebt ihn ins andere Haus: Er faellt aus der
|
||
Agenturliste und kommt an dieser Adresse nicht mehr herein. Das
|
||
ist kein Fehler, das ist der Preis dafuer, dass DogFather es von
|
||
hier aus auch kann.
|
||
|
||
WER DAS SPAETER "AUFRAEUMEN" WILL: Die symmetrische Regel waere
|
||
ein Filter wie unten fuer das Crew-Haus, nur andersherum. Sie
|
||
wurde angeboten und abgelehnt. Zwei Pruefungen halten den Stand
|
||
fest (pruef-personen-formular, pruef-creator-anlegen) -- wer ihn
|
||
aendert, macht dort rot, und das soll er auch. */
|
||
admin: [...ROLLEN].filter((r) => r !== "admin"),
|
||
/* Spicy Media: das ganze Team -- und seit dem 11.09.2026 auch die
|
||
eigene Rolle.
|
||
|
||
Filipe: "ich will dass die rolle spicy und dogfather, auch die
|
||
rollen wechseln koennen wenn die personen schon drin sind. von
|
||
alle kategorien, creator, scouts, manager spicy."
|
||
|
||
Dass jemand seine eigene Rolle weitergeben kann, ist eine
|
||
Entscheidung und kein Versehen: Spicy Media fuehrt die Agentur.
|
||
DogFather bleibt trotzdem ausserhalb der Liste -- niemand hebt
|
||
sich ueber die Rolle, die ihn eingesetzt hat. */
|
||
spicy: ["spicy", "manager", "scout", "creator"],
|
||
/* Ein Manager stellt Creator ein, die er dann auch betreut. */
|
||
manager: ["creator"],
|
||
/* DIE RECHTE HAND LEGT PERSONEN AN (22.09.2026).
|
||
|
||
Filipe: "dan will ich dass die rechte hand auch neue personen
|
||
hinzufuegen kann. also neue erstellen kann und die codes genau so
|
||
sieht wie dogfather, damit sie das auch machen kann wenn er live
|
||
ist."
|
||
|
||
DIESELBE LISTE, DIE SIE AUCH VERGEBEN DARF -- abgeleitet aus
|
||
ROLLEN_ZUM_AENDERN, nicht danebengeschrieben. Zwei Listen mit
|
||
denselben zwei Rollen waeren zwei Gelegenheiten, eine davon zu
|
||
aendern und die andere zu vergessen; dann duerfte sie eine Rolle
|
||
anlegen, aber nicht vergeben, und niemand wuesste warum.
|
||
|
||
WAS DAMIT AUSDRUECKLICH NICHT GEHT: eine zweite rechte Hand, eine
|
||
linke Hand oder einen zweiten DogFather. Sie kann sich also auch
|
||
ueber den Umweg "neuen Zugang anlegen" keine hoeheren Rechte
|
||
verschaffen. */
|
||
hand: ROLLEN_ZUM_AENDERN.hand,
|
||
/* DIE LINKE HAND LEGT NIEMANDEN AN (21.09.2026) -- ausdruecklich im
|
||
Auftrag. Sie stuende auch ohne diese Zeile auf [] (darfAnlegen
|
||
nimmt `?? []`), aber dann saehe es aus wie vergessen. Eine
|
||
ausdrueckliche leere Liste ist eine Entscheidung, eine fehlende
|
||
Zeile ist eine Luecke. */
|
||
linke: [],
|
||
};
|
||
|
||
/** Welche Rollen darf diese Person anlegen? Immer ein Feld, nie null —
|
||
* wer nichts darf, bekommt eine leere Liste und keinen Sonderfall. */
|
||
export function darfAnlegen(person) {
|
||
if (!person) return [];
|
||
const alle = [...(ANLEGBAR[person.rolle] ?? [])];
|
||
/* AUF DER TEAM-ADRESSE NUR TEAM-ROLLEN (10.09.2026).
|
||
|
||
Filipe: "bitte nur basiert auf diese seite." Wer dort jemanden
|
||
anlegt, meint jemanden aus dem Team -- einen Creator dort
|
||
einzutragen waere ein Versehen, das man erst auf der anderen Seite
|
||
bemerkt.
|
||
|
||
DIESELBE AUSKUNFT BAUT AUCH DIE KNOEPFE. `darfAnlegen` ist die
|
||
Quelle sowohl fuer die Pruefung in der Route als auch fuer die
|
||
Auswahl in der Oberflaeche -- deshalb verschwinden die anderen
|
||
Rollen dort von selbst, statt eine Absage zu bringen. */
|
||
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.
|
||
*/
|
||
/* WER DARF DEN WEG "Rolle aendern" UEBERHAUPT BETRETEN?
|
||
*
|
||
* DIE RECHTE HAND KAM AM 20.09.2026 DAZU. Filipe: "perfektionier das
|
||
* fuer die rolle dogfather und rechte hand. nur die sollen das machen
|
||
* koennen."
|
||
*
|
||
* DIESE ZEILE OEFFNET NUR DIE TUER. Wen sie anfassen darf und wozu,
|
||
* entscheidet darfRolleAendern() / rollenZumAendern() weiter oben --
|
||
* dort steht auch, warum die rechte Hand keine zweite rechte Hand
|
||
* ernennen kann. Zwei getrennte Fragen, zwei getrennte Regeln: Wer
|
||
* hier etwas aendert, oeffnet keine Grenze versehentlich mit. */
|
||
export const darfRollenWechseln = (person) =>
|
||
!!person && (person.rolle === "admin" || person.rolle === "spicy"
|
||
|| person.rolle === "hand");
|
||
/* OHNE "linke" -- UND DAS IST ABSICHT, KEIN VERGESSEN (21.09.2026).
|
||
Eine Rolle zu aendern ist Kontoverwaltung. Der Auftrag sagt zur
|
||
linken Hand ausdruecklich "keine neuen Personen oder Accounts
|
||
hinzufuegen"; wer Rollen vergeben kann, kann sich selbst und andere
|
||
zu allem machen, was es gibt. Deshalb hier die kleinere Antwort. */
|
||
|
||
/* WER FUEHRT TEAM DOGI? (10.09.2026)
|
||
*
|
||
* DogFather und seine rechte Hand -- und sonst niemand. Spicy Media und
|
||
* die Manager stehen bewusst NICHT darin: Sie fuehren die Agentur, nicht
|
||
* das Team.
|
||
*
|
||
* WARUM DAS HIER STEHT UND NICHT DORT, WO ES GEBRAUCHT WIRD: Es wurde
|
||
* schon zweimal gebraucht -- im Eingang (`darfEingang`) und jetzt bei
|
||
* den Kategorie-Kanaelen. Beim dritten Mal waere es dreimal
|
||
* hingeschrieben, und die dritte Abschrift ist die, die eine Rolle
|
||
* vergisst. Der Eingang ruft ab jetzt hierher.
|
||
*/
|
||
export const fuehrtTeamDogi = (person) =>
|
||
!!person && (person.rolle === "admin" || istHand(person));
|
||
|
||
/* =======================================================================
|
||
SPICY MEDIA (07.09.2026)
|
||
|
||
Wunsch Filipe: "eine neue rolle ... die den namen traegt, spicy media,
|
||
und soll die gleichen rechte haben wie dogfather ausser, bei
|
||
automationen sollen die nicht sehen und die kategorie personen und
|
||
zugaenge sollen die nur leute hinzufuegen koennen ... aber die sollen
|
||
meine privaten chats und so nicht sehen."
|
||
|
||
ZWEI FRAGEN, DIE MAN AUSEINANDERHALTEN MUSS -- und genau daran haette
|
||
man diesen Umbau kaputtmachen koennen:
|
||
|
||
WER SIEHT ALLES? -> siehtAlles() = DogFather ODER Spicy Media
|
||
WER ENTSCHEIDET? -> istDogFather() = nur DogFather
|
||
|
||
Die erste Frage stellt sich bei Listen, Uebersichten und Auswertungen:
|
||
Spicy Media soll alle Manager, Scouts und Creator sehen. Die zweite
|
||
bei allem Endgueltigen: loeschen, Rollen aendern, Codes neu setzen,
|
||
Sicherungen, die KI abschalten. Da bleibt DogFather allein.
|
||
|
||
Es waere viel weniger Arbeit gewesen, istDogFather() einfach um
|
||
"spicy" zu erweitern -- und genau das waere der Fehler: Spicy Media
|
||
koennte dann DogFather loeschen. Zwei Namen, zwei Bedeutungen, und an
|
||
jeder Stelle steht sichtbar, welche gemeint ist.
|
||
|
||
DER CHAT BRAUCHTE NICHTS. Er haengt ausschliesslich an der
|
||
Teilnehmerliste (chat_teilnehmer) und kennt kein "das Management sieht
|
||
alles". Spicy Media sieht fremde Gespraeche also nicht, weil es dafuer
|
||
gar keinen Weg gibt -- nicht, weil eine Abfrage es verbietet.
|
||
======================================================================= */
|
||
export const istSpicy = (person) => !!person && person.rolle === "spicy";
|
||
|
||
/* =======================================================================
|
||
SPICY MEDIA SIEHT ALLES -- AUSSER DEM, WAS DOGFATHER GEHOERT
|
||
(07.09.2026, Nachtrag am selben Tag)
|
||
|
||
Filipe: "wieso sieht die spicy rolle meine daten die ich habe und
|
||
speichere, meins sollen die nicht sehen von der rolle dogfather."
|
||
|
||
Beim ersten Anlauf bekam Spicy Media dieselbe Regel wie DogFather:
|
||
`1=1`. Damit stimmte der Ueberblick ueber Manager, Scouts und Creator
|
||
-- und nebenbei standen DogFathers eigene Termine, Aufgaben, Dateien
|
||
und Eintraege mit drin. Der Chat war ausgenommen (er haengt an der
|
||
Teilnehmerliste), alles andere nicht.
|
||
|
||
DIESE FUNKTION IST DIE EINE STELLE DAFUER. Sie baut die Bedingung
|
||
"gehoert keinem DogFather" fuer eine beliebige Tabelle. Jede
|
||
Sichtbarkeitsregel im Haus benutzt sie -- eine zweite, abgeschriebene
|
||
Fassung waere die Stelle, an der es beim naechsten Modul wieder
|
||
durchsickert.
|
||
|
||
ZWEI SPALTEN, NICHT EINE. `creator_id` sagt, UM WEN es geht;
|
||
`erstellt_von` sagt, WER es geschrieben hat. Beide muessen gepruegt
|
||
werden: Ein Termin, den DogFather fuer sich selbst anlegt, hat
|
||
moeglicherweise gar keine creator_id -- er haenge dann nur an
|
||
erstellt_von. Und eine Akte UEBER einen DogFather traegt seine
|
||
creator_id, auch wenn ein Manager sie geschrieben hat.
|
||
|
||
`IS NULL OR NOT IN` und nicht bloss `NOT IN`: In SQL ist
|
||
`NULL NOT IN (...)` weder wahr noch falsch, sondern NULL -- die Zeile
|
||
fiele stillschweigend heraus. Genau so verschwinden Daten, ohne dass
|
||
jemand einen Fehler sieht.
|
||
======================================================================= */
|
||
export function ohneDogFather(praefix, spalten = ["creator_id", "erstellt_von"]) {
|
||
return spalten
|
||
.map((sp) => `(${praefix}.${sp} IS NULL OR ${praefix}.${sp} NOT IN `
|
||
+ `(SELECT id FROM personen WHERE rolle = 'admin'))`)
|
||
.join(" AND ");
|
||
}
|
||
/* =====================================================================
|
||
WAS EINEM MODI GEHOERT, SIEHT SONST NIEMAND (10.09.2026)
|
||
|
||
Das Gegenstueck zu ohneDogFather() -- und es entstand aus einem
|
||
gemessenen Leck, nicht aus einer Ueberlegung.
|
||
|
||
WAS PASSIERT WAR: Fuer PERSONEN ist die Regel zentral
|
||
(verborgeneIds). Fuer ZEILEN gibt es sie dreimal -- in
|
||
workspace-aufgaben.js, workspace-bereiche.js und
|
||
workspace-dateien.js -- und alle drei geben Spicy Media dasselbe:
|
||
"alles ausser dem, was DogFather gehoert". Ein Modi-Eintrag gehoert
|
||
ihm aber nicht. Also sah Spicy Media die Aufgaben, die Eintraege und
|
||
die Dateien der Modis, waehrend die Modis selbst in jeder Namensliste
|
||
sauber verborgen waren. Die halbe Verborgenheit ist keine.
|
||
|
||
Aufgefallen ist es nicht beim Lesen des Codes, sondern durch eine
|
||
Pruefung, die etwas ANLEGT und danach mit fremden Augen nachsieht.
|
||
Manager, Scout und Creator waren nie betroffen -- ihre Regeln sind
|
||
ohnehin enger. Der Kalender auch nicht: termineSichtbar() gibt jedem
|
||
nur Eigenes. Nachgemessen, nicht angenommen.
|
||
|
||
SPALTEN MUESSEN BEIDE RICHTUNGEN ABDECKEN. `creator_id` sagt, UM WEN
|
||
es geht; `erstellt_von`/`hochgeladen_von`/`verantwortlich_id` sagen,
|
||
an WEM es haengt. Eine Modi-Aufgabe hat gar keine creator_id -- sie
|
||
haengt allein an verantwortlich_id. Wer nur eine Spalte prueft, hat
|
||
nichts geprueft.
|
||
|
||
`IS NULL OR NOT IN` und nicht bloss `NOT IN`: In SQL ist
|
||
`NULL NOT IN (...)` weder wahr noch falsch, sondern NULL -- die Zeile
|
||
fiele stillschweigend heraus. Genau so verschwinden Daten, ohne dass
|
||
jemand einen Fehler sieht. (Dieselbe Falle steht schon im Kommentar
|
||
von ohneDogFather; sie ist es wert, zweimal dazustehen.)
|
||
===================================================================== */
|
||
/* DIE ROLLEN DER AGENTUR -- das andere Haus.
|
||
|
||
Sie stehen als eigene Menge da und nicht als "alles ausser Team
|
||
Dogi": Kaeme morgen eine sechste Rolle dazu, waere sie mit einem
|
||
`ausser` automatisch in der Agentur, ohne dass jemand darueber
|
||
nachgedacht hat. Eine Aufzaehlung zwingt zur Entscheidung -- dieselbe
|
||
Ueberlegung wie bei OHNE_MODI_HOSTS in crew-adresse.js. */
|
||
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);
|
||
}
|
||
|
||
/** Gehoert dieser Mensch zu Team Dogi -- DogFather eingerechnet?
|
||
*
|
||
* DAS SIND DIE VIER, DIE FILIPE IMMER WIEDER NENNT: „nur die modis
|
||
* rechte linke hand und dogfather". Genau diese Aufzaehlung stand
|
||
* hier dreimal wortgleich als Ausdruck da (siehtModis, kanaeleFuer,
|
||
* kategorienFuer) und waere mit dem naechsten Wunsch ein viertes Mal
|
||
* dazugekommen. Drei gleiche Ausdruecke sind drei Gelegenheiten, dass
|
||
* einer beim naechsten Rollenzuschnitt nicht mitgeht.
|
||
*
|
||
* ABGELEITET, NICHT ABGESCHRIEBEN: TEAM_DOGI_ROLLEN (hand, linke,
|
||
* modi) ist die eine Liste des Hauses; DogFather kommt dazu, weil er
|
||
* dazugehoert -- er steht nicht darueber, er ist dabei. Kaeme morgen
|
||
* eine weitere Team-Rolle, waere sie hier von selbst dabei.
|
||
*/
|
||
export const istTeamDogi = (person) => !!person
|
||
&& (person.rolle === "admin" || TEAM_DOGI_ROLLEN.has(person.rolle));
|
||
|
||
/** Darf diese Person ueberhaupt etwas sehen, das einem Modi gehoert?
|
||
* Nur die DogFather-Rolle und die Modis selbst. */
|
||
export const siehtModis = istTeamDogi;
|
||
|
||
export const siehtAlles = (person) => !!person
|
||
&& (person.rolle === "admin" || person.rolle === "spicy");
|
||
|
||
/* Der Rollenschluessel bleibt "admin" -- er steckt in der CHECK-Regel der
|
||
Datenbank, in jeder Sitzung und in jeder Rechteabfrage. Umbenannt wird
|
||
nur, was man LIEST. Diese Zuordnung ist die einzige Stelle dafuer:
|
||
Stand der Anzeigename in elf Dateien, waere er beim naechsten Mal in
|
||
zehn davon geaendert.
|
||
|
||
Hinweis fuer spaeter: In den Kommentaren der Fachmodule heisst die
|
||
Rolle "admin" weiterhin "das Management". Gemeint ist dieselbe Rolle,
|
||
die in der Oberflaeche "DogFather" heisst. */
|
||
export const ROLLEN_NAME = {
|
||
spicy: "Spicy Media",
|
||
admin: "DogFather",
|
||
manager: "Manager",
|
||
scout: "Scout",
|
||
creator: "Creator",
|
||
hand: "Rechte Hand",
|
||
/* DIE LINKE HAND (21.09.2026). Ohne diesen Eintrag heisst sie
|
||
ueberall dort, wo der Rollenname steht -- im Treff, an einem
|
||
Beitrag, in der Personenliste -- schlicht "linke". Gefunden hat
|
||
das pruef-rechtetafel, weil sie diese Tabelle gegen ROLLEN haelt
|
||
und rot wird, sobald eine Rolle nur in einer der beiden steht. */
|
||
linke: "Linke Hand",
|
||
modi: "Modi",
|
||
/* DIE COMMUNITY (11.09.2026). Der Name steht hier und NICHT in einer
|
||
Datei, die jeder herunterlaedt -- ausgeliefert wird er nur an den,
|
||
der ihn selbst traegt, und an die, die ihn sehen duerfen.
|
||
|
||
Filipe hat ausdruecklich gewaehlt, dass im Treff der volle
|
||
Rollenname neben einem Beitrag steht ("Modi", "Rechte Hand",
|
||
"DogFather"). Das bleibt damit vereinbar: Der Name kommt als DATEN
|
||
mit der Antwort, nicht als Konstante im Browser. */
|
||
gast: "Community",
|
||
};
|
||
|
||
/* DIE UEBERSCHRIFT UEBER EINER PERSONENLISTE (10.09.2026).
|
||
*
|
||
* Sie ist NICHT dasselbe wie das Namensschild: Ueber einer Liste steht
|
||
* die Mehrzahl ("Scouts"), am einzelnen Menschen die Einzahl ("Scout").
|
||
* Der Browser hat diese Zuordnung in bereiche.js ebenfalls -- aber nur
|
||
* fuer die fuenf Rollen, die dort stehen duerfen.
|
||
*
|
||
* WARUM SIE HIER GEBRAUCHT WIRD: In der Personenauswahl des Chats
|
||
* wurde nach `ROLLENFOLGE` aus bereiche.js gezeichnet. Wer dort nicht
|
||
* vorkommt, wurde nicht gezeichnet -- ohne Fehler, ohne Luecke, ohne
|
||
* Hinweis. Team Dogi kommt dort nicht vor und darf es auch nicht (der
|
||
* Rollenname gehoert in keine Datei, die jeder herunterlaedt). Folge:
|
||
* DogFather konnte ueber die Auswahl niemandem aus seinem Team
|
||
* schreiben und keinen in einen Kanal setzen. Aufgefallen ist es nicht
|
||
* beim Lesen, sondern beim Hinsehen: Der Kanal hatte hinterher zwei
|
||
* Leute statt vier.
|
||
*
|
||
* Jetzt schickt der Server die Ueberschrift mit. Der Browser braucht
|
||
* dafuer keinen Rollennamen zu kennen -- er bekommt einen Text.
|
||
*/
|
||
export const ROLLEN_GRUPPE = {
|
||
...ROLLEN_NAME,
|
||
scout: "Scouts",
|
||
/* Beide unter EINER Ueberschrift: In der Auswahl sucht man einen
|
||
Menschen, keine Rangordnung -- und zwei Ueberschriften mit je
|
||
einem Namen darunter waeren mehr Gliederung als Inhalt. */
|
||
hand: "Team Dogi",
|
||
/* DIE LINKE HAND GEHOERT DAZU (23.09.2026). Ohne diesen Eintrag
|
||
faellt sie auf ROLLEN_NAME zurueck und bekommt in der Auswahl eine
|
||
eigene Ueberschrift „Linke Hand" mit genau einem Namen darunter --
|
||
also dieselbe Rangordnung, die zwei Zeilen hoeher ausdruecklich
|
||
vermieden werden sollte. Aufgefallen beim Nachgehen des Befundes
|
||
zum Steckbrief; dort fehlte sie ebenfalls. */
|
||
linke: "Team Dogi",
|
||
modi: "Team Dogi",
|
||
};
|
||
|
||
/* =====================================================================
|
||
DIE KACHELN EINES MODIS (10.09.2026)
|
||
|
||
Wo die anderen fuenf Rollen ihre Kacheln aus assets/js/bereiche.js
|
||
bekommen, kommen sie fuer einen Modi von hier. Der Grund ist derselbe
|
||
wie beim Anzeigenamen und bei der Rollenauswahl: bereiche.js wird an
|
||
JEDEN ausgeliefert, der die Seite oeffnet. Stuende dort auch nur
|
||
`rollen: [... , 'modi']`, waere die Rolle mit einem Blick in den
|
||
Quelltext gefunden -- und damit alles verraten.
|
||
|
||
NICHT NUR EINE LISTE VON ZIELEN, sondern die ganze Kachel. Wer nur
|
||
die Ziele schickte und die Beschriftung aus bereiche.js nehmen liesse,
|
||
bekaeme unter "Dashboard" den Untertitel "Alle Creator auf einen
|
||
Blick" -- fuer jemanden, der keine Creator betreut. Die Worte gehoeren
|
||
zum Empfaenger, nicht zum Ziel.
|
||
|
||
ZEICHEN, TON UND SZENE SIND SCHLUESSEL AUS bereiche.js, keine neuen.
|
||
Sie sagen nichts ueber Modis aus (jede andere Kachel benutzt dieselben)
|
||
und muessen deshalb nicht verborgen werden. Neue zu erfinden waere
|
||
genau falsch herum: Sie muessten in bereiche.js stehen, und dort
|
||
faellt jede unbenutzte Zeile irgendwann jemandem auf. Die Toene sind
|
||
dieselben wie an den entsprechenden Kacheln der anderen Rollen -- ein
|
||
Ton kennzeichnet die Kachel, nicht ihren Platz.
|
||
|
||
WAS NICHT DABEI IST, und warum: Uebersicht, Calls, Content, Reports,
|
||
Scouting, Personen und Automationen. Sie drehen sich um betreute
|
||
Creator, um die Agentur oder um Rechte -- fuer einen Modi waeren sie
|
||
leer oder nicht seine Sache. Der Rollen-Rundgang (pruef-rollen) zeigt,
|
||
dass sie ihn nicht abstuerzen lassen; das ist der Grund, sie
|
||
wegzulassen, und nicht der Grund, sie mitzunehmen.
|
||
|
||
ES IST EINE ROLLENFRAGE, KEINE PERSONENFRAGE: Alle Modis bekommen
|
||
dasselbe. Sollte das je auseinandergehen, gehoert die Auswahl an die
|
||
Person und nicht hierher -- dann aber bewusst und mit einer Stelle,
|
||
an der sie gepflegt wird.
|
||
===================================================================== */
|
||
const MODI_BEREICHE = [
|
||
{ gruppe: "Täglich", gruppeUnter: "Was du sowieso jeden Tag aufmachst",
|
||
name: "Aufgaben", unter: "Offen, in Arbeit, zur Freigabe", zeichen: "aufgaben",
|
||
/* SILBER (23.09.2026). Filipe: „ich will dass diese kachel einen
|
||
richtig geilen silber hat als farbe bitte. mach es richtig geil,
|
||
ein glitzer silber."
|
||
|
||
ALS EIGENES MERKMAL, NICHT ALS TON. Silber ist keine Farbe aus
|
||
den siebzehn -- es ist eine OBERFLAECHE: ein kuehler
|
||
Metallverlauf mit Glanzkante und feinem Funkeln. Als Ton
|
||
eingetragen haette es eine Buntheit nahe null, und
|
||
pruef-kachelfarben verlangt zu Recht mindestens 0,12.
|
||
|
||
`ton: 13` bleibt deshalb stehen und ist der Rueckfall: Wer die
|
||
Kachel ohne die neue Stilvorlage sieht (alter Zwischenspeicher,
|
||
Kontrastmodus), bekommt weiterhin ihr Gruen. Genau so macht es
|
||
die Willkommenskachel mit `regenbogen` seit dem 22.09. */
|
||
ton: 13, silber: true, gross: true, ziel: "aufgaben.html", szene: "garage" },
|
||
{ gruppe: "Täglich", gruppeUnter: "Was du sowieso jeden Tag aufmachst",
|
||
name: "Chat", unter: "Nachrichten mit dem Team", zeichen: "chat",
|
||
ton: 4, ziel: "chat.html", szene: "lounge" },
|
||
{ gruppe: "Täglich", gruppeUnter: "Was du sowieso jeden Tag aufmachst",
|
||
name: "Kalender", unter: "Live-Zeiten und Termine", zeichen: "kalender",
|
||
ton: 8, ziel: "kalender.html", szene: "skyline" },
|
||
{ gruppe: "Täglich", gruppeUnter: "Was du sowieso jeden Tag aufmachst",
|
||
name: "Dateien", unter: "Ablage und Freigaben", zeichen: "dateien",
|
||
ton: 19, ziel: "dateien.html", szene: "arena" },
|
||
|
||
{ gruppe: "Täglich", gruppeUnter: "Was du sowieso jeden Tag aufmachst",
|
||
name: "Ideen-Board", unter: "Gesammelt und nach Dringlichkeit sortiert",
|
||
zeichen: "content", ton: 23, ziel: "bereich.html?b=ideen", szene: "portal" },
|
||
{ gruppe: "Täglich", gruppeUnter: "Was du sowieso jeden Tag aufmachst",
|
||
name: "Angebote", unter: "Vorschläge, über die DogFather entscheidet",
|
||
zeichen: "agentur", ton: 17, ziel: "bereich.html?b=angebote", szene: "halle" },
|
||
|
||
/* RUECKMELDUNG -- IN BEIDE RICHTUNGEN (10.09.2026, Kapitel 5 und 6
|
||
des Pflichtenhefts).
|
||
|
||
Filipe: "Nicht nur ich soll meine Modis bewerten oder ihnen
|
||
Feedback geben koennen. Auch die Modis sollen mir Feedback geben
|
||
koennen." Und: "Es soll vor allem dabei helfen, als Team besser zu
|
||
werden."
|
||
|
||
Deshalb steht die Kachel bei ALLEN im Team an derselben Stelle und
|
||
heisst fuer alle gleich. "Feedback an DogFather" beim einen und
|
||
"Bewertung des Teams" beim anderen waeren zwei Einbahnstrassen --
|
||
und die Richtung stuende im Namen. */
|
||
{ gruppe: "Täglich", gruppeUnter: "Was du sowieso jeden Tag aufmachst",
|
||
name: "Rückmeldung", unter: "Was gut läuft, was hakt – in beide Richtungen",
|
||
zeichen: "reports", ton: 25, ziel: "bereich.html?b=rueckmeldung", szene: "lounge" },
|
||
|
||
{ gruppe: "Rund ums Live", gruppeUnter: "Vor, während und nach der Sendung",
|
||
name: "Live-Ablauf", unter: "Checkliste für vorher, mittendrin und danach",
|
||
zeichen: "live", ton: 3, ziel: "bereich.html?b=live", szene: "portal" },
|
||
{ gruppe: "Rund ums Live", gruppeUnter: "Vor, während und nach der Sendung",
|
||
name: "Community", unter: "Begrüßen, vermitteln, wiedererkennen",
|
||
zeichen: "community", ton: 6, ziel: "bereich.html?b=community", szene: "lounge" },
|
||
{ gruppe: "Rund ums Live", gruppeUnter: "Vor, während und nach der Sendung",
|
||
name: "Technik", unter: "Ton, Bild, Verbindung", zeichen: "technik",
|
||
ton: 14, ziel: "bereich.html?b=technik", szene: "garage" },
|
||
|
||
{ gruppe: "Für dich", gruppeUnter: "Deine Seite im Team",
|
||
name: "Mein Steckbrief", unter: "Dein Bild und deine Kanäle", zeichen: "steckbrief",
|
||
ton: 10, ziel: "steckbrief.html", szene: "halle" },
|
||
{ 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: "Das Rudel \u2013 der Raum f\u00fcr die Zuschauer",
|
||
};
|
||
|
||
/* DIE BREITE KACHEL STEHT VORNE (17.09.2026).
|
||
|
||
Filipe, mit einem Bildschirmfoto: "das wie es jetzt ist ist es
|
||
einfach total scheisse ... gerade einfach nur dahin geknallt".
|
||
|
||
Er hatte recht, und die Ursache stand genau hier. Das Raster hat
|
||
DREI Spalten, "Das Rudel" ist doppelt breit (`grid-column: span 2`)
|
||
-- stand aber an DRITTER Stelle. Nach zwei normalen Kacheln war noch
|
||
EINE Spalte frei, er passte nicht hinein und rutschte eine Reihe
|
||
tiefer. Zurueck blieb ein Loch mitten im Raster:
|
||
|
||
Reihe 1 [Anschlagbrett] [Was ansteht] ....... LUECKE
|
||
Reihe 2 [ Der Treff (2) ] [Wunschliste]
|
||
Reihe 3 [Highlights] [Regeln] [Mitmachen]
|
||
Reihe 4 [Meldungen] ...... LUECKE ...... LUECKE
|
||
|
||
DIE REGEL, und sie gilt fuer jedes Raster im Haus: Eine breite
|
||
Kachel gehoert an den ANFANG. Steht sie hinten, faellt die Luecke in
|
||
die MITTE -- und eine Luecke in der Mitte sieht kaputt aus, eine am
|
||
Ende sieht grosszuegig aus.
|
||
|
||
Jetzt fuellen Treff (2) plus Anschlagbrett die erste Reihe, die
|
||
naechsten drei die zweite, der Rest die dritte. Fuer das Team mit
|
||
der achten Kachel geht es genau auf: drei mal drei.
|
||
|
||
UND ES IST AUCH INHALTLICH RICHTIG. Der Treff ist das Herz dieses
|
||
Bereichs -- dass er vorne steht, ist keine Notloesung fuer das
|
||
Raster, sondern die Reihenfolge, die ohnehin stimmt. */
|
||
const TREFF_BEREICHE = [
|
||
/* AUS DEM TREFF-BRETT WIRD DIE WILLKOMMENSSEITE (21.09.2026, Auftrag
|
||
Abschnitt 7). Gemessen davor: Das Brett "treff" hatte NULL Eintraege,
|
||
der Treff-Chat daneben 29 Nachrichten -- geredet wird im Chat, das
|
||
Brett war eine Kachel ohne Inhalt. Sie zeigt jetzt auf die Seite,
|
||
die erklaert, was es hier ueberhaupt gibt. */
|
||
/* ALLE FARBEN DES HAUSES IN EINER KACHEL (22.09.2026).
|
||
|
||
Filipe: „die farbe dieser kachel soll alle farben haben die es auf
|
||
der ganze seite gibt ... nicht gemischt ... soll komplett farbig
|
||
sein und ultra geil aussehen."
|
||
|
||
`ton: 34` bleibt stehen und ist kein Widerspruch: Er faerbt Rand,
|
||
Zeichen und Pfeil und ist der Rueckfall, falls das Stilblatt
|
||
einmal nicht laedt. Die Streifen kommen aus einer eigenen Regel,
|
||
die `tools/kachel-regenbogen.mjs` rechnet -- aus den benutzten
|
||
Toenen, nicht aus einer Liste hier. */
|
||
{ ...TREFF_GRUPPE, name: "Willkommen", unter: "Was es hier gibt, kurz erklaert",
|
||
zeichen: "community", ton: 34, gross: true, regenbogen: true,
|
||
ziel: "willkommen.html", szene: "lounge" },
|
||
/* DER CHAT (19.09.2026). Filipe: "eine geile mischung von punkt 2
|
||
und drei ... nach mitternacht bis morgens 6 uhr keiner schreiben
|
||
kann damit auch die privatsphaere beruecksichtig wird ueber nacht
|
||
... und die sollen nicht anrufen koennen. nur schreiben."
|
||
|
||
GLEICH AN ZWEITER STELLE, direkt hinter dem grossen Treff-Brett.
|
||
Er ist das Lebendigste, was die Community hier hat -- weiter unten
|
||
waere er der Knopf, den man erst findet, wenn einem jemand sagt,
|
||
dass es ihn gibt.
|
||
|
||
ZIEL IST chat.html, dieselbe Seite wie beim Team. Kein zweiter
|
||
Chat: Zwei Oberflaechen fuer dasselbe waeren zwei Stellen, an
|
||
denen ein Fehler behoben werden muss, und die zweite waere die,
|
||
die man vergisst. Was jemand dort SIEHT, entscheidet die
|
||
Raummitgliedschaft -- ein Gast hat genau einen Raum.
|
||
|
||
TON 41: gezaehlt in start.css, nicht in dieser Datei. 40 war die
|
||
hoechste (siehe die Warnung an WERDEGANG_KACHEL, dort ist genau
|
||
dieser Fehler heute schon einmal passiert). */
|
||
/* „TREFF-CHAT“ UND NICHT NUR „CHAT“ (nachgeschärft 19.09.2026).
|
||
|
||
Gefunden beim Durchgehen aus Benutzersicht: Ein Modi und die
|
||
rechte Hand sahen ZWEI Kacheln mit demselben Namen „Chat“, beide
|
||
mit demselben Ziel `chat.html` -- eine unter „Täglich“ (das Team),
|
||
eine hier unter „Community“. Sie taten auch dasselbe: Man landete
|
||
in der Gesprächsliste und musste den richtigen Raum suchen.
|
||
|
||
Zwei gleich benannte Wege zum selben Ort sind schlimmer als ein
|
||
fehlender -- man klickt den falschen und sucht den Unterschied.
|
||
|
||
Jetzt trägt sie ihren eigenen Namen UND ihr eigenes Ziel:
|
||
`?raum=treff` öffnet den Raum der Community direkt. Für die
|
||
Community selbst ändert sich am Namen nichts Wesentliches -- sie
|
||
hat nur diese eine. */
|
||
{ ...TREFF_GRUPPE, name: "Rudel-Chat", unter: "Miteinander reden – nachts schläft er",
|
||
zeichen: "chat", ton: 41, ziel: "chat.html?raum=treff", szene: "lounge" },
|
||
/* HIGHLIGHTS AN DIE SPITZE (22.09.2026).
|
||
|
||
Filipe: „da wo anschlagbrett ist soll highlights sein und dan den
|
||
rest nochmal weiter fuehren."
|
||
|
||
Kein Tausch, sondern ein Aufruecken: Highlights nimmt den Platz,
|
||
alle anderen wandern eine Stelle weiter. Inhaltlich stimmt das
|
||
auch -- was die Leute selbst gemacht haben, steht vor dem, was
|
||
ihnen angesagt wird. */
|
||
{ ...TREFF_GRUPPE, name: "Highlights", unter: "Eure Clips und Bilder",
|
||
zeichen: "live", ton: 29, ziel: "bereich.html?b=highlight", szene: "arena" },
|
||
{ ...TREFF_GRUPPE, name: "Anschlagbrett", unter: "Was gerade gilt",
|
||
zeichen: "reports", ton: 33, ziel: "bereich.html?b=anschlag", szene: "halle" },
|
||
{ ...TREFF_GRUPPE, name: "Was ansteht", unter: "Die n\u00e4chsten Streams und Events",
|
||
zeichen: "kalender", ton: 31, ziel: "bereich.html?b=ansteht", szene: "skyline" },
|
||
{ ...TREFF_GRUPPE, name: "Wunschliste", unter: "Was ihr euch w\u00fcnscht \u2013 und wer mitwill",
|
||
zeichen: "content", ton: 30, ziel: "bereich.html?b=wunsch", szene: "portal" },
|
||
/* HIESS BIS ZUM 21.09.2026 "Unsere Seiten" (Ziel und Datei bleiben
|
||
gleich, nur der Name auf der Kachel aendert sich).
|
||
|
||
Dahinter stehen jetzt drei Dinge statt zwei Adressen: ob gerade
|
||
gestreamt wird, wo man Dogi sonst findet, und die beiden Seiten mit
|
||
dem Rabatt. "Unsere Seiten" haette davon nur noch den letzten
|
||
Drittel benannt -- und wer wegen des Live-Status wiederkommt,
|
||
findet eine Kachel nicht, die nach etwas anderem klingt.
|
||
|
||
DER NAME STEHT AB JETZT NUR EINMAL IM HAUS: pruef-wege-nach-draussen
|
||
liest die Ueberschrift der Seite und verlangt, dass die Kachel
|
||
genauso heisst. Zwei Namen fuer dieselbe Sache waren schon einmal
|
||
ein echter Fehler (die zwei Kacheln "Chat" am 19.09.) -- man klickt
|
||
den falschen und sucht den Unterschied. */
|
||
/* MATERIAL (22.09.2026).
|
||
|
||
Filipe: "ich brauch auch noch eine neue kachel im bereich
|
||
community, wo wir als team ... bilder reinschicken koennen, mit
|
||
text und wo die leute sei es community oder modis sich die bilder
|
||
nehmen koennen zum posten ... sobald ein bild oder ein video ...
|
||
runtergeladen wurde, soll direkt blockiert werden."
|
||
|
||
SIE STEHT VOR "DRAUSSEN": Wer hier etwas holt, geht danach
|
||
hinaus und postet es. Die Reihenfolge ist der Weg.
|
||
|
||
Ton 42 ist GERECHNET, nicht gegriffen -- tools/kachel-farbe-
|
||
einzeln.mjs, Abstand 0,0823 zum naechsten Nachbarn (1,3-fach
|
||
besser als der erste Vorschlag), Kontrast 6,08:1. */
|
||
{ ...TREFF_GRUPPE, name: "Dogi-Media", unter: "Bilder und Videos zum Posten – jedes nur einmal",
|
||
zeichen: "galerie", ton: 42, ziel: "material.html", szene: "studio" },
|
||
/* DIE FARBE, DIE "REGELN & HILFE" HATTE (22.09.2026).
|
||
Filipes screen5: "die farbe dieser kachel soll die farbe von der
|
||
kachel: regeln & hilfe werden". Regeln & Hilfe bekommt dafuer
|
||
eine eigene -- zwei Kacheln mit derselben Farbe in derselben
|
||
Ansicht waeren genau das, was screen4 abschafft.
|
||
Ton 39 ist GESETZT. */
|
||
{ ...TREFF_GRUPPE, name: "Draußen", unter: "Ist er live? Kanäle, Shop & dein Rabatt",
|
||
zeichen: "hinaus", ton: 39, ziel: "unsere-seiten.html", szene: "portal" },
|
||
{ ...TREFF_GRUPPE, name: "Mitmachen", unter: "Wie du dazugeh\u00f6ren kannst",
|
||
zeichen: "personen", ton: 32, ziel: "bereich.html?b=mitmachen", szene: "garage" },
|
||
/* UNSERE SEITEN (19.09.2026) -- die einzige Kachel, die HINAUSFUEHRT.
|
||
|
||
Filipe: "ich brauch im community bereich noch eine kachel, die wenn
|
||
man drauf dr\u00fcckt dan kacheln hat die auf die website von dogfather
|
||
universe geht ... und die shop site von vanvan."
|
||
|
||
SIE IST KEIN BRETT, sondern eine Seite -- deshalb ein Dateiname als
|
||
Ziel und kein "bereich.html?b=...". Die Ableitung von TREFF_BRETTER
|
||
weiter unten nimmt sie dadurch von selbst nicht mit, genau wie die
|
||
Moderationskachel. Eine zweite Liste, die man pflegen muesste,
|
||
entsteht nicht.
|
||
|
||
UND SIE STEHT AM ENDE. Die sieben davor fuehren nach innen: lesen,
|
||
schreiben, mitmachen. Diese eine verlaesst den Treff -- das ist der
|
||
letzte Schritt, nicht der erste.
|
||
|
||
RASTERRECHNUNG (drei Spalten, "Das Rudel" belegt zwei): Mit dieser
|
||
Kachel sind es fuer die Community 7 + 1 Kacheln = 9 Plaetze, also
|
||
genau drei volle Reihen. Vorher waren es acht Plaetze und damit
|
||
eine Luecke am Ende. Sie passt also nicht nur inhaltlich. */
|
||
{ ...TREFF_GRUPPE, name: "Regeln & Hilfe", unter: "Wie es hier l\u00e4uft",
|
||
/* ZEIGT JETZT AUF DIE SEITE, DIE DIE ANTWORT HAT (19.09.2026).
|
||
|
||
Sie fuehrte auf `bereich.html?b=regeln` -- ein Brett, auf das die
|
||
Leitung Regeln eintragen KANN und das am ersten Tag leer ist
|
||
("Hier stehen die Regeln des Treffs -- sobald sie eingetragen
|
||
sind.").
|
||
|
||
Die Regeln, die es WIRKLICH gibt, stehen auf `treff-regeln.html`:
|
||
Begruessung, die fuenf Regeln, die drei Stufen, das Mindestalter,
|
||
was bei Aerger zu tun ist. Diese Seite war von genau EINER Stelle
|
||
aus erreichbar -- einem kleinen Textlink unter der Unterzeile
|
||
einer Brettseite. Keine der elf Kacheln fuehrte hin.
|
||
|
||
Damit war die einzige "So laeuft das hier"-Seite des Hauses fuer
|
||
einen neuen Zuschauer praktisch unsichtbar, waehrend die Kachel,
|
||
die ihren Namen trug, auf eine leere Liste zeigte.
|
||
|
||
Das Brett gibt es weiter; treff-regeln.html verlinkt darauf. Ein
|
||
Ort fuer die Frage, nicht zwei. */
|
||
/* EIGENER TON (22.09.2026). "Draussen" traegt jetzt die Farbe,
|
||
die hier stand -- ausdruecklicher Wunsch. Zwei Kacheln mit
|
||
derselben Farbe in derselben Ansicht waeren genau das, was
|
||
screen4 abschafft. */
|
||
zeichen: "schutz", ton: 28, ziel: "treff-regeln.html", szene: "studio" },
|
||
];
|
||
|
||
/* MELDUNGEN & MASSNAHMEN -- FUER DAS TEAM, NICHT FUER DIE COMMUNITY
|
||
(11.09.2026).
|
||
|
||
Diese Kachel steht ausdruecklich NICHT in TREFF_BEREICHE. Die Liste
|
||
dort ist das, was ein Mitglied sieht; sie wird dem Team nur
|
||
angehaengt. Eine achte Kachel darin waere auch bei der Community
|
||
gelandet -- und zwar lautlos, denn sie saehe dort aus wie die
|
||
anderen sieben. Erst beim Klicken haette der Server 404 gesagt, und
|
||
das sieht aus wie ein kaputtes Programm.
|
||
|
||
SIE GEHOERT AUCH NICHT IN TREFF_BRETTER: Sie ist kein Brett mit
|
||
Eintraegen, sondern eine Seite. Das Ziel ist deshalb ein
|
||
Dateiname und kein "bereich.html?b=...", und die Ableitung weiter
|
||
unten nimmt sie von selbst nicht mit. */
|
||
/* EIGENE GRUPPE, NICHT DIE DER COMMUNITY (19.09.2026).
|
||
|
||
Filipe: "ich will das oben die sachen fuer dogfather sind, dan die
|
||
sachen fuer modis und dan community bereich."
|
||
|
||
Bis heute trug diese Kachel `...TREFF_GRUPPE` -- sie landete damit
|
||
IN der Gruppe \u201eCommunity" und dort, weil sie zuletzt angehaengt
|
||
wird, hinter allen sieben Brettern. Wer moderiert, musste also an
|
||
der ganzen Community vorbeiscrollen, um an seine Arbeit zu kommen.
|
||
|
||
Getrennt ist es auch inhaltlich richtig: Die sieben Bretter sind der
|
||
Raum der Zuschauer. Diese Kachel ist Arbeit AN diesem Raum -- und
|
||
die sieht nur, wer moderiert. */
|
||
const MODERATION_GRUPPE = {
|
||
gruppe: "Moderation",
|
||
gruppeUnter: "Die Arbeit am Rudel \u2013 sieht nur, wer moderiert",
|
||
};
|
||
|
||
const TREFF_MODERATION_KACHEL = {
|
||
...MODERATION_GRUPPE,
|
||
name: "Meldungen & Ma\u00dfnahmen", unter: "Was gemeldet wurde \u2013 und was daraus wurde",
|
||
/* EIGENES ZEICHEN, NICHT DASSELBE WIE "REGELN & HILFE" (17.09.2026).
|
||
|
||
Hier stand zweimal `schutz` -- zwei Kacheln nebeneinander mit
|
||
demselben Schild. Auf dem Bildschirmfoto sind sie nicht zu
|
||
unterscheiden, und genau daran erkennt man eine Sammlung, die
|
||
niemand als Ganzes angesehen hat.
|
||
|
||
`startcheck` ist eine abgehakte Liste und sagt den Unterschied:
|
||
Bei "Regeln & Hilfe" steht, was GILT. Hier steht, was daraus
|
||
WURDE. */
|
||
zeichen: "startcheck", ton: 36, ziel: "treff-moderation.html", szene: "studio",
|
||
};
|
||
|
||
/* Welche Bretter zum Treff gehoeren -- ABGELEITET aus den Kacheln, nicht
|
||
danebengeschrieben. Eine zweite Liste waere die, die beim naechsten
|
||
neuen Brett vergessen wird. */
|
||
export const TREFF_BRETTER = [...new Set([
|
||
...TREFF_BEREICHE
|
||
.map((k) => new URL("http://x/" + k.ziel).searchParams.get("b"))
|
||
.filter(Boolean),
|
||
/* EIN BRETT OHNE KACHEL (berichtigt 20.09.2026).
|
||
----------------------------------------------------------------
|
||
Die Liste wird aus den Kachelzielen abgeleitet, und das ist
|
||
richtig: Was niemand anklicken kann, muss auch nicht offenstehen.
|
||
|
||
Am 19.09.2026 bekam die Kachel „Regeln & Hilfe" ein neues Ziel --
|
||
`treff-regeln.html` statt `bereich.html?b=regeln`, weil die
|
||
Kachel vorher auf ein leeres Brett zeigte. Damit fiel `regeln`
|
||
aus dieser Ableitung heraus, und das BRETT war ab da fuer die
|
||
Community gesperrt (404).
|
||
|
||
Nur verweist `treff-regeln.html` weiterhin darauf: „Haeufige
|
||
Fragen und Antworten stehen auf dem Brett Regeln & Hilfe." Ein
|
||
Verweis, der seit einem Tag ins Leere fuehrt -- und zwar auf der
|
||
einen Seite, die einem neuen Mitglied alles erklaeren soll.
|
||
|
||
Gefunden hat es `pruef-treff` (4 Fehler, seit dem 19.09.), nicht
|
||
ein Mensch. Deshalb steht es hier ausdruecklich und mit Grund:
|
||
Ein Brett kann erreichbar sein muessen, ohne eine eigene Kachel
|
||
zu haben. Wer die Ableitung liest und diese Zeile nicht sieht,
|
||
haelt die Liste fuer vollstaendig. */
|
||
"regeln",
|
||
|
||
/* UND DASSELBE EIN DRITTES MAL (21.09.2026) -- deshalb steht unten
|
||
jetzt eine Pruefung, die es laut macht.
|
||
----------------------------------------------------------------
|
||
Am 21.09. wurde die Kachel "Das Rudel" zur Willkommensseite
|
||
(Auftrag Abschnitt 7). Damit fiel `treff` aus dieser Ableitung.
|
||
|
||
WAS DAS ANRICHTETE, ist schlimmer als beim letzten Mal: Die
|
||
Schranke in workspace-bereiche.js faengt Aussenrollen und
|
||
Team-Dogi-Rollen ab, laesst eine AGENTURROLLE aber unbesehen
|
||
durch ("if (!TEAM_DOGI_ROLLEN.has(rolle)) return next()"). Solange
|
||
`treff` hier stand, fing der Zweig darueber sie ab -- danach nicht
|
||
mehr. Eine Creatorin konnte das Brett des Treffs lesen.
|
||
|
||
Gefunden hat es wieder pruef-treff, nicht das Nachdenken. Und
|
||
wieder war es kein neuer Fehler, sondern derselbe: Ein Brett
|
||
gehoert zum Treff, WEIL es dazugehoert -- nicht, weil gerade
|
||
jemand eine Kachel darauf zeigen laesst. */
|
||
"treff",
|
||
])];
|
||
|
||
/* Wer den Treff ueberhaupt sehen darf: die Community selbst und das
|
||
Team von Dogi. Die Agentur ausdruecklich nicht -- Creator, Scouts und
|
||
Spicy Media haben mit den Zuschauern dieses Streams nichts zu tun. */
|
||
export const TREFF_ROLLEN = new Set(["gast", "modi", "hand", "linke", "admin"]);
|
||
|
||
/* DER VERTRAULICHE MELDEWEG (19.09.2026).
|
||
|
||
Eigene Gruppe, nicht bei den Brettern: Auf den sieben Brettern
|
||
schreibt man oeffentlich. Hier schreibt man an genau zwei Menschen.
|
||
Zwischen den anderen Kacheln wuerde das verschwimmen -- und wer ein
|
||
Problem hat, sucht nicht lange.
|
||
|
||
SIE STEHT VOR "FUER DICH": Ein Problem ist dringender als der eigene
|
||
Steckbrief. Die Reihenfolge innerhalb der Gruppe "Fuer dich" ergibt
|
||
sich aus der Liste, die Sortierung in start.js fasst beide ans Ende. */
|
||
/* DER SUPPORT (24.09.2026).
|
||
|
||
Filipe: „da fehlt auch eine support kachel. die jeder sieht. jeder
|
||
benutzen kann. jeder kann da probleme von der seite melden und
|
||
hinweisen. fotos mit schicken mit einem text."
|
||
|
||
SIE STEHT NICHT IN EINER ROLLENLISTE, sondern wird in
|
||
`bereicheFuer()` an JEDE angehaengt -- siehe dort. Sie in fuenf
|
||
Listen einzutragen waere fuenf Gelegenheiten, sie bei der sechsten
|
||
Rolle zu vergessen; und genau so ist heute der
|
||
Benachrichtigungs-Knopf auf 14 Seiten verschwunden.
|
||
|
||
TON 44: gezaehlt in start.css, wo die Farben wirklich stehen (bis
|
||
43 vergeben). Danach laeuft `node tools/kachel-farben.mjs
|
||
--schreiben`, das alle Toene neu gegeneinander rechnet.
|
||
|
||
ZEICHEN "schutz" wie beim vertraulichen Meldeweg -- beides sind
|
||
Wege, auf denen man sich meldet, wenn etwas nicht stimmt. Ein
|
||
eigenes Zeichen zu erfinden hiesse, zwei Dinge zu unterscheiden,
|
||
die sich fuer den Benutzer gleich anfuehlen. */
|
||
const SUPPORT_KACHEL = {
|
||
name: "Support", unter: "Etwas kaputt? Schreib es mit Bild",
|
||
zeichen: "schutz", ton: 44, ziel: "support.html", szene: "studio",
|
||
};
|
||
|
||
const HILFE_KACHEL = {
|
||
gruppe: "Für dich", gruppeUnter: "Deine eigene Seite",
|
||
name: "Vertraulich melden", unter: "Nur DogFather und die rechte Hand",
|
||
/* KNALLROT -- AUSDRUECKLICH DIESE KACHEL (berichtigt 22.09.2026).
|
||
==================================================================
|
||
Filipe: "nicht die kachel draussen soll rot sein sondern die
|
||
kachel vertraulich melden nur die soll knall rot sein!!!"
|
||
|
||
Ich hatte screen5 und screen6 vertauscht und das Rot auf
|
||
"Draussen" gelegt. Hier sitzt es richtig: Ein vertraulicher
|
||
Meldeweg ist das, was jemand im schlimmsten Moment sucht -- und
|
||
dann zaehlt, dass man ihn auf der Wand sofort findet, ohne zu
|
||
lesen. Rot ist hier die Aussage, nicht die Dekoration.
|
||
|
||
#ff1a1a, voll gesaettigt, Kontrast 4,84:1 gegen den Grund.
|
||
Ton 38 ist GESETZT: pruef-kachelfarben wird rot, wenn ein
|
||
Farblauf ihn anfasst. */
|
||
zeichen: "schutz", ton: 38, ziel: "hilfe.html", szene: "studio",
|
||
};
|
||
|
||
/* DAS TEAM SIEHT DEN TREFF MIT -- als eigene Gruppe unter seinen
|
||
uebrigen Kacheln.
|
||
|
||
Filipe: "die modis rechte hand und ich also dogfather sehen die aber
|
||
auch. die community aber nur die dann."
|
||
|
||
Zusammengesetzt und nicht in MODI_BEREICHE hineingeschrieben, weil
|
||
diese Liste weiter oben steht als die Kacheln des Treffs. Eine Kopie
|
||
dort waere die Stelle, an der beim naechsten neuen Brett eines fehlt. */
|
||
/* =====================================================================
|
||
WAS DER MODI UEBER SICH SELBST SIEHT (19.09.2026)
|
||
|
||
GEFUNDEN BEIM DURCHGEHEN DER SEITE AUS BENUTZERSICHT, und es war
|
||
mein eigener Fehler vom Vortag: `entwicklung.html` und
|
||
`werdegang.html` haben BEIDE eine ausgearbeitete Eigensicht --
|
||
entwicklung.js zeigt die eigene Karte, werdegang.js die eigene
|
||
Bewegung. Die Rechtetafel laesst den Modi auf beide Seiten
|
||
(rechte.js).
|
||
|
||
Nur: In seiner Kachelliste stand keine von beiden. Der Weg dorthin
|
||
existierte fuer ihn nicht -- er haette die Adresse von Hand tippen
|
||
muessen. Die ganze Eigensicht war damit toter Code, ausgerechnet
|
||
fuer den Menschen, fuer den sie geschrieben wurde.
|
||
|
||
ZWEI KACHELN UND NICHT DIE DER LEITUNG. Dieselben Ziele, aber die
|
||
Leitungstexte passen nicht: "Wie sich jemand ueber die Zeit bewegt"
|
||
liest sich, als ginge es um andere Leute. Aus seiner Sicht geht es
|
||
um ihn.
|
||
|
||
UND SIE LOESEN EINE NAMENSKOLLISION. Bis heute setzten BEIDE Seiten
|
||
fuer ihn die Ueberschrift "Deine Entwicklung" -- wer aus der Glocke
|
||
kam, konnte nicht sagen, auf welcher er stand. Jetzt heisst der
|
||
Katalog "Deine Aufgaben" (passend zur Kachel "Eure Aufgaben") und
|
||
die Auswertung "Deine Entwicklung". Ein Name, eine Sache.
|
||
|
||
ABGELEITET, NICHT ABGESCHRIEBEN: Ziel, Zeichen und Ton kommen aus
|
||
den Kacheln der Leitung. Wer dort das Ziel aendert, aendert es hier
|
||
mit -- zwei Kachelobjekte mit demselben Ziel waeren sonst zwei
|
||
Gelegenheiten, eines davon zu vergessen. */
|
||
const FUER_DICH = {
|
||
gruppe: "Für dich", gruppeUnter: "Deine Seite im Team",
|
||
};
|
||
const MEINE_AUFGABEN_KACHEL = {
|
||
...FUER_DICH,
|
||
zeichen: ENTWICKLUNG_KACHEL.zeichen, ton: ENTWICKLUNG_KACHEL.ton,
|
||
ziel: ENTWICKLUNG_KACHEL.ziel, szene: ENTWICKLUNG_KACHEL.szene,
|
||
name: "Deine Aufgaben", unter: "Was du kannst – und was als Nächstes kommt",
|
||
};
|
||
const MEINE_ENTWICKLUNG_KACHEL = {
|
||
...FUER_DICH,
|
||
zeichen: WERDEGANG_KACHEL.zeichen, ton: WERDEGANG_KACHEL.ton,
|
||
ziel: WERDEGANG_KACHEL.ziel, szene: WERDEGANG_KACHEL.szene,
|
||
name: "Deine Entwicklung", unter: "Was sich bei dir bewegt hat – ohne Note",
|
||
};
|
||
|
||
/* DER MODI BEKOMMT DENSELBEN BEREICH WIE DIE LEITUNG (22.09.2026).
|
||
|
||
Filipe zum Bildschirmfoto der Gruppe "Entwicklung & Nachwuchs":
|
||
"die modis sollen diesen bereich und diese zwei kacheln auch sehen,
|
||
die sollen aber nur ihre aufgaben und daten sehen."
|
||
|
||
DIESELBEN ZWEI KACHELN, NICHT EIGENE MIT ANDEREM NAMEN. Bis heute
|
||
standen hier MEINE_AUFGABEN_KACHEL und MEINE_ENTWICKLUNG_KACHEL --
|
||
zwei Kacheln mit denselben Zielen und anderen Namen. Zwei Namen fuer
|
||
dieselbe Sache ist genau der Fehler, der am 19.09. schon einmal zwei
|
||
Kacheln "Chat" ergeben hat: Man klickt den falschen und sucht den
|
||
Unterschied.
|
||
|
||
"TALENTE" BLEIBT DRAUSSEN. Dort stehen Notizen ueber Zuschauer, die
|
||
nichts davon wissen -- das ist etwas anderes als die eigene
|
||
Entwicklung. Der Riegel dafuer sitzt ohnehin in rechte.js; diese
|
||
Zeile sorgt nur dafuer, dass gar nicht erst eine Kachel dorthin
|
||
zeigt.
|
||
|
||
DASS ER DORT NUR SICH SELBST SIEHT, entscheiden die Seiten: In
|
||
workspace-werdegang.js steht `leitung ? alle : []`, in
|
||
workspace-entwicklungs-punkte.js dasselbe. Die Kachel oeffnet die
|
||
Seite -- was darauf steht, entscheidet der Server. */
|
||
/* =====================================================================
|
||
CALLS & PROTOKOLLE GEHOEREN NEBEN JEDEN KALENDER (22.09.2026)
|
||
=====================================================================
|
||
Filipe: „ich will diese kachel vom workspace auch im team dogi seite.
|
||
ich will dass jeder immer nur seine eigenen sachen von seinem
|
||
kalender sieht mehr nicht. perfektioniere das bitte. jeder der einen
|
||
kalender hat soll auch sowas haben danke."
|
||
|
||
GEMESSEN, BEVOR GEBAUT WURDE:
|
||
|
||
Rolle Kacheln Kalender Calls
|
||
admin 31 ja NEIN
|
||
hand 31 ja NEIN
|
||
linke 30 ja NEIN
|
||
modi 26 ja NEIN
|
||
gast 12 nein nein
|
||
|
||
Vier Listen mit Kalender, keine mit Calls -- und die Community hat
|
||
keinen Kalender und braucht deshalb auch keine Calls. Sein Satz
|
||
„jeder der einen kalender hat" beschreibt die Lage also genau.
|
||
|
||
DESHALB IST ES EINE REGEL UND KEINE VIERFACHE EINTRAGUNG. Vier
|
||
Stellen von Hand zu pflegen waere die Stelle, an der es beim
|
||
naechsten neuen Rollenzuschnitt auseinanderlaeuft: Einer bekommt
|
||
einen Kalender, und die Calls vergisst man.
|
||
|
||
DIE GRUPPE WIRD MITGENOMMEN, nicht danebengeschrieben: Die neue
|
||
Kachel erbt `gruppe` und `gruppeUnter` vom Kalender, neben dem sie
|
||
steht. Zieht der Kalender eines Tages in eine andere Gruppe, zieht
|
||
sie mit.
|
||
|
||
DER TON IST GERECHNET, NICHT UEBERNOMMEN -- und der erste Versuch
|
||
war falsch.
|
||
|
||
Naheliegend war Ton 15, derselbe wie im anderen Haus: dieselbe
|
||
Sache, dieselbe Farbe. Als Zahl war er frei. Auf dem BILDSCHIRM
|
||
nicht: pruef-kachelfarben misst den Abstand zwischen je zwei
|
||
Kacheln an echten Bildpunkten, und 15 lag bei 0,0139 zu „Was
|
||
ansteht" -- unter der Grenze von 0,02. Zwei Kacheln, die gleich
|
||
aussehen, sind eine Kachel zu viel.
|
||
|
||
Also ausgerechnet, welcher freie Ton am weitesten von ALLEN
|
||
benutzten wegliegt (OKLab-Abstand):
|
||
|
||
Ton 22 #fee0b2 0,1135 aber Buntheit nur 0,068 -> zu blass
|
||
Ton 7 #098f71 0,1067 Buntheit 0,112 -> zu blass
|
||
Ton 16 #19b2c2 0,0854 Abstand unter 0,090 -> zu nah
|
||
Ton 15 #01d7e3 0,0279 (der erste Versuch)
|
||
|
||
KEINER DER FREIEN TOENE ERFUELLTE ALLE HAUSREGELN. Die Palette
|
||
kannte 42 Toene, 31 davon im Einsatz -- und unter den elf uebrigen
|
||
war keiner, der gleichzeitig weit genug weg (>= 0,090) und bunt
|
||
genug (>= 0,12) ist. Eine 32. Kachel braucht einen 32. Ton.
|
||
|
||
Also einen NEUEN gerechnet statt einen vorhandenen umzufaerben:
|
||
Ton 16 gehoert im anderen Haus zu „Start-Check" -- ihn zu aendern
|
||
haette dort eine Kachel umgefaerbt, nach der niemand gefragt hat.
|
||
`tools/kachel-farbe-einzeln.mjs` haelt alle vorhandenen fest und
|
||
sucht fuer EINEN die Farbe, die am weitesten von allen wegliegt:
|
||
|
||
Ton 43 #027afb Abstand 0,0925 Buntheit 0,212 Kontrast 4,63:1
|
||
|
||
Alle drei Regeln erfuellt, und es bleibt ein Blau -- die Kachel ist
|
||
damit wiedererkennbar als dieselbe wie im anderen Haus.
|
||
===================================================================== */
|
||
const CALLS_KACHEL = {
|
||
name: "Calls & Protokolle",
|
||
unter: "Gespräche mit To-dos",
|
||
zeichen: "calls",
|
||
ton: 43,
|
||
ziel: "calls.html",
|
||
szene: "lounge",
|
||
};
|
||
|
||
/** Hinter jeden Kalender einer Liste eine Calls-Kachel setzen. */
|
||
function mitCalls(liste) {
|
||
const raus = [];
|
||
for (const k of liste) {
|
||
raus.push(k);
|
||
/* Am ZIEL erkannt, nicht am Namen: Der Name einer Kachel ist im
|
||
Haus schon mehrfach gewandert, das Ziel nicht. */
|
||
if (String(k.ziel || "").startsWith("kalender.html")) {
|
||
raus.push({ gruppe: k.gruppe, gruppeUnter: k.gruppeUnter, ...CALLS_KACHEL });
|
||
}
|
||
}
|
||
return raus;
|
||
}
|
||
|
||
const MODI_MIT_TREFF = mitCalls([...MODI_BEREICHE, BEFINDEN_KACHEL,
|
||
ENTWICKLUNG_KACHEL, WERDEGANG_KACHEL, ...TREFF_BEREICHE,
|
||
TREFF_MODERATION_KACHEL]);
|
||
const HAND_MIT_TREFF = mitCalls([...HAND_BEREICHE, ...TREFF_BEREICHE,
|
||
TREFF_MODERATION_KACHEL, HILFE_KACHEL]);
|
||
|
||
/* DIE LINKE HAND SIEHT "DEIN TEAM" NICHT (22.09.2026).
|
||
|
||
Filipe: "die rolle linke hand soll die kachel dein team auch nicht
|
||
sehen bitte danke."
|
||
|
||
ABGELEITET, NICHT NACHGEBAUT: Ihre Liste ist die der rechten Hand
|
||
MINUS dieser einen Kachel. Eine zweite, eigene Liste waere die, die
|
||
beim naechsten neuen Bereich auseinanderlaeuft -- dann haette sie
|
||
ihn, und die linke Hand nicht, und niemand wuesste warum.
|
||
|
||
Erkannt wird die Kachel an ihrem ZIEL, nicht an ihrem Namen: Der
|
||
Name ist am 17.09. schon einmal gewandert ("Eingang" -> "Dein
|
||
Team"), das Ziel nicht. */
|
||
const LINKE_MIT_TREFF = HAND_MIT_TREFF.filter((k) => k.ziel !== EINGANG_KACHEL.ziel);
|
||
|
||
/* AUSSEN_ROLLEN kommt aus crew-adresse.js und wird hier nur
|
||
weitergereicht -- die Adressregel braucht sie zuerst, und diese Datei
|
||
importiert von dort. Eine zweite Menge waere die, die auseinanderlaeuft. */
|
||
export { AUSSEN_ROLLEN };
|
||
|
||
/** Darf diese Person telefonieren?
|
||
*
|
||
* HIER UND NICHT AUS workspace-treffchat.js IMPORTIERT: Jene Datei
|
||
* importiert aus dieser. Ein Import zurueck waere ein Ring, und Ringe
|
||
* halten in JavaScript genau so lange, bis jemand eine Konstante auf
|
||
* oberster Ebene benutzt. `workspace-treffchat.js` reicht diese
|
||
* Funktion deshalb nur WEITER -- gerechnet wird sie an einer Stelle,
|
||
* naemlich hier. */
|
||
export function darfTelefonieren(person) {
|
||
return !AUSSEN_ROLLEN.has(person?.rolle);
|
||
}
|
||
|
||
/* =====================================================================
|
||
KACHEL UND TUER GEHOEREN ZUSAMMEN (11.09.2026)
|
||
=====================================================================
|
||
|
||
Seit DogFather die Rechtetafel von Hand umstellen kann, kann eine
|
||
Seite zugehen, ohne dass ihre Kachel verschwindet. Gefunden hat das
|
||
pruef-rechte-umstellen mit einer einzigen Zeile: Er nahm einem Modi
|
||
chat.html weg -- die Schranke griff sofort, und die Kachel stand
|
||
weiterhin da. Ein Knopf, der auf die Startseite zurueckwirft.
|
||
|
||
Der Satz "Kachel und Tuer gehoeren zusammen" steht seit dem
|
||
06.09.2026 in assets/js/bereiche.js. Bis heute war er eine Bitte an
|
||
den, der beides pflegt. Jetzt ist er eine Rechnung: Die Kachelliste
|
||
wird gegen dieselbe Tafel gefiltert, aus der die Schranke ihre
|
||
Entscheidung holt.
|
||
|
||
NACH DEM PFAD UND NICHT NACH DEM GANZEN ZIEL: "bereich.html?b=live"
|
||
ist die Seite bereich.html. Wer die Frage am Fragezeichen nicht
|
||
abschneidet, filtert jede Brett-Kachel weg -- lautlos, denn eine
|
||
fehlende Kachel sieht aus wie eine Rolle, die sie eben nicht hat.
|
||
|
||
WAS KEIN ZIEL HAT, BLEIBT. Eine Kachel ohne "ziel" ist kein Weg zu
|
||
einer Seite, und "ich kenne die Seite nicht" darf nicht "also weg
|
||
damit" heissen. */
|
||
function nurOffeneKacheln(liste, person) {
|
||
if (!Array.isArray(liste) || !person) return liste;
|
||
return liste.filter((k) => {
|
||
/* EINE KACHEL, DIE HINAUSFUEHRT, HAT HIER KEINE SEITE (21.09.2026).
|
||
|
||
Unten wird aus dem Ziel ein Pfad gebaut ("/workspace/" + ziel)
|
||
und die Rechtetafel gefragt. Bei einer vollstaendigen Adresse --
|
||
der Tuer zu Team Dogi -- kaeme dabei
|
||
"/workspace/https://crew..." heraus, das steht in keiner Tafel,
|
||
und die Kachel waere lautlos verschwunden. Lautlos ist das
|
||
Schlimme daran: Man sucht den Fehler dann bei der Rolle. */
|
||
if (k?.aussen) return true;
|
||
const ziel = String(k?.ziel || "");
|
||
if (!ziel) return true;
|
||
const pfad = "/workspace/" + ziel.split("?")[0];
|
||
return darfSeite(person, pfad);
|
||
});
|
||
}
|
||
|
||
/* JEDER HAT EINEN STECKBRIEF -- AUCH DIE COMMUNITY (19.09.2026).
|
||
|
||
Filipe: "jeder soll ein steckbrief haben, jeder der ein account hat
|
||
... jeder soll genau wie ich foto und so hinzufuegen koennen."
|
||
|
||
Das Team hatte die Kachel laengst (sie steht in MODI_BEREICHE unter
|
||
„Für dich"), die Community nicht -- und damit die groesste Gruppe
|
||
ueberhaupt. Die Seite selbst konnte es die ganze Zeit: `/mein` fragt
|
||
nach der eigenen Nummer und kennt gar keine Rolle.
|
||
|
||
EIGENE FASSUNG DES UNTERTEXTS: „Deine Seite im Team" waere fuer ein
|
||
Mitglied falsch -- es ist nicht im Team. Der Steckbrief ist fuer sie
|
||
das, was man von sich zeigt. */
|
||
const STECKBRIEF_KACHEL_TREFF = {
|
||
gruppe: "Für dich", gruppeUnter: "Deine eigene Seite",
|
||
name: "Mein Steckbrief", unter: "Dein Bild und deine Kanäle",
|
||
zeichen: "steckbrief", ton: 10, ziel: "steckbrief.html", szene: "halle",
|
||
};
|
||
|
||
/* Die Community: der Treff und ihre eigene Seite. Sonst nichts. */
|
||
const TREFF_MIT_EIGENEM = [...TREFF_BEREICHE, HILFE_KACHEL, STECKBRIEF_KACHEL_TREFF];
|
||
|
||
/* DIE SUPPORT-KACHEL BEKOMMT JEDER -- an genau einer Stelle
|
||
angehaengt (24.09.2026).
|
||
|
||
`bereicheFuer` gibt je nach Rolle eine von fuenf Listen zurueck,
|
||
dazu `null` ("nimm die Liste aus dem Browser") und `[]` ("nichts").
|
||
Die Kachel in jede Liste einzutragen waeren fuenf Aenderungen und
|
||
fuenf kuenftige Gelegenheiten, eine zu vergessen. Stattdessen
|
||
haengt diese Huelle sie an das an, was herauskommt.
|
||
|
||
`null` UND `[]` BLEIBEN UNANGETASTET, und das ist Absicht:
|
||
`null` heisst "der Browser hat die Liste" -- dort steht sie in
|
||
assets/js/bereiche.js und wird dort ergaenzt. `[]` heisst "diese
|
||
Rolle bekommt gar nichts"; ihr eine einzelne Kachel zu geben waere
|
||
die Ausnahme, die spaeter niemand mehr erklaeren kann. */
|
||
export function bereicheFuer(person) {
|
||
const liste = bereicheRoh(person);
|
||
if (liste === null || !Array.isArray(liste) || !liste.length) return liste;
|
||
return liste.some((k) => k?.ziel === "support.html")
|
||
? liste : [...liste, SUPPORT_KACHEL];
|
||
}
|
||
|
||
function bereicheRoh(person) {
|
||
if (person?.rolle === "modi") return MODI_MIT_TREFF;
|
||
/* Die linke Hand vor der allgemeinen Hand-Regel -- sonst faenge
|
||
`istHand()` sie ab, und der Filter darunter liefe nie. */
|
||
if (person?.rolle === "linke") return LINKE_MIT_TREFF;
|
||
if (istHand(person)) return HAND_MIT_TREFF;
|
||
/* DIE COMMUNITY SIEHT DEN TREFF UND SONST NICHTS (11.09.2026) --
|
||
seit dem 19.09. plus den eigenen Steckbrief. */
|
||
if (person?.rolle === "gast") return TREFF_MIT_EIGENEM;
|
||
/* AUF DER ADRESSE VON TEAM DOGI SIEHT AUCH DOGFATHER DIE KACHELN DES
|
||
TEAMS (10.09.2026).
|
||
|
||
Filipe: "die hier soll ihre eigene daten haben und komplett von der
|
||
anderen getrennt sein." Fuenfundzwanzig Kacheln, von denen zwei
|
||
Drittel Creator, Scouts und Agentur betreffen, waeren dort
|
||
Fenster in ein Haus, in dem er gerade nicht ist -- und hinter
|
||
jedem stuende seit heute eine leere Liste.
|
||
|
||
ES IST DIESELBE LISTE WIE BEI DER RECHTEN HAND, nicht eine dritte.
|
||
Beide sehen auf dieser Adresse dasselbe; eine eigene Liste fuer
|
||
ihn waere die, die beim naechsten Umbau auseinanderlaeuft -- und
|
||
sie stuende ausserdem gegen die Hausregel, ihn nicht ueber sein
|
||
Team zu stellen. */
|
||
/* Auf der Team-Adresse sehen DogFather und die rechte Hand dieselben
|
||
Kacheln. Eine eigene Liste fuer ihn waere die, die beim naechsten
|
||
Umbau auseinanderlaeuft -- und sie stuende gegen die Hausregel,
|
||
ihn nicht ueber sein Team zu stellen. */
|
||
if (person?.haus === "crew") return HAND_MIT_TREFF;
|
||
/* `null` HEISST "NIMM DIE LISTE AUS DEM BROWSER" -- und die enthaelt
|
||
fuenfundzwanzig Kacheln des Agenturhauses. Bis zum 11.09.2026 stand
|
||
hier nur `return null`, also bekam JEDE Rolle ohne eigenen Zweig
|
||
diese Liste angeboten. Gefiltert wurde sie danach zwar nach
|
||
`rollen:` an jeder Kachel, aber das ist eine zweite Schranke an
|
||
einer anderen Stelle -- und sie lebt in einer Datei, die jeder
|
||
herunterlaedt.
|
||
|
||
Jetzt bekommen nur die fuenf Rollen `null`, fuer die diese Liste
|
||
geschrieben wurde. Alles andere bekommt eine leere Liste: keine
|
||
Kachel, kein Weg, nichts zu filtern. */
|
||
if (AGENTUR_LISTE.has(person?.rolle)) return null;
|
||
return [];
|
||
}
|
||
|
||
/* Die fuenf, deren Kacheln in assets/js/bereiche.js stehen. Nicht zu
|
||
verwechseln mit AGENTUR_ROLLEN (dort fehlt `admin`, weil es dabei um
|
||
die Sichtbarkeit von Zeilen geht und nicht um Kacheln). */
|
||
const AGENTUR_LISTE = new Set(["spicy", "admin", "manager", "scout", "creator"]);
|
||
|
||
/* WELCHE BEREICHSSEITEN EIN MODI OEFFNEN DARF -- abgeleitet aus seinen
|
||
eigenen Kacheln und nicht danebengeschrieben.
|
||
|
||
Eine zweite Liste waere die Stelle, an der es auseinanderlaeuft: Wer
|
||
eine Kachel hinzufuegt und den Eintrag vergisst, baut einen Knopf,
|
||
der auf die Startseite zurueckwirft. Wer eine Kachel entfernt und den
|
||
Eintrag vergisst, laesst eine Tuer offen. So kann beides nicht
|
||
passieren.
|
||
|
||
Gebraucht wird sie, weil `bereich.html` seit heute fuer ihn offen ist
|
||
-- ohne diese Einschraenkung kaeme er ueber die Adresszeile auch in
|
||
die Agentur-Ablage, die ihn nichts angeht. */
|
||
/* EINE KACHEL IST NICHT MEHR DER EINZIGE WEG ZU EINEM BRETT
|
||
(11.09.2026, gefunden von pruef-entwicklung).
|
||
|
||
Die Ableitung oben liest die Bretter aus den Kachelzielen. Das war
|
||
richtig, solange jede Kachel auf ein Brett zeigte. Seit „Entwicklung"
|
||
und „Talente" auf ihre KATALOGSEITEN zeigen, fielen genau diese
|
||
beiden Bretter aus der Liste -- und die rechte Hand bekam auf dem
|
||
Notizbrett, das die Katalogseite selbst verlinkt, ein 404.
|
||
|
||
Kein Fehler im Riegel, sondern in seiner QUELLE: Die Frage "welche
|
||
Bretter darf diese Rolle oeffnen" laesst sich nicht mehr allein aus
|
||
den Kacheln beantworten, weil es jetzt Bretter gibt, die man ueber
|
||
eine Seite erreicht statt ueber eine Kachel.
|
||
|
||
Beides wird deshalb zusammengezogen -- und die zweite Liste ist
|
||
ausdruecklich KURZ und begruendet, nicht offen: Hier stehen nur die
|
||
Bretter, zu denen eine erlaubte SEITE verlinkt. Wer ein drittes
|
||
dazunimmt, ohne es einzutragen, bekommt dasselbe 404 und findet
|
||
diesen Absatz. */
|
||
/* Bretter, die kein eigenes Kachelziel haben, sondern von einer SEITE
|
||
aus verlinkt sind -- das Notizbrett neben dem Katalog.
|
||
|
||
AUS EINER LISTE WURDE EINE ZUORDNUNG (19.09.2026), und das war ein
|
||
echtes Loch, kein Schoenheitsfehler:
|
||
|
||
Der Satz darueber lautete "Wer die Seite oeffnen darf, muss auch
|
||
dorthin kommen." Der Gedanke ist richtig -- nur stand er als feste
|
||
Liste da und fragte nie, WER gerade fragt. Ergebnis: Ein Modi kam
|
||
ueber `bereich.html?b=talente` an die Notizen zu Kandidaten, obwohl
|
||
`talente.html` fuer ihn ausdruecklich gesperrt ist. Die Begruendung
|
||
der Sperre steht in rechte.js und gilt hier genauso: "Hier stehen
|
||
Namen von Menschen, die nichts davon wissen."
|
||
|
||
Schlimmer noch: In workspace-bereiche.js stand ein Kommentar, der
|
||
das Gegenteil des Codes behauptete ("entwicklung, talente bleiben
|
||
404"). Ein falscher Kommentar an einer Rechtestelle ist schlimmer
|
||
als gar keiner -- der Naechste liest ihn und prueft nicht nach.
|
||
|
||
Jetzt steht neben jedem Brett die Seite, von der es abhaengt, und
|
||
gefragt wird `darfSeite()` -- dieselbe Tafel, die auch die Seite
|
||
selbst bewacht. Eine zweite Regel kann damit nicht auseinanderlaufen. */
|
||
const BRETTER_UEBER_SEITEN = {
|
||
entwicklung: "/workspace/entwicklung.html",
|
||
talente: "/workspace/talente.html",
|
||
};
|
||
|
||
/** Bretter mit eigener Kachel. Diese darf jeder aus dem Team, der die
|
||
* Kachel sieht -- sie sind daraus abgeleitet, nicht abgeschrieben. */
|
||
export const MODI_BEREICHE_ERLAUBT = new Set([
|
||
...HAND_MIT_TREFF
|
||
.map((k) => (/bereich\.html\?b=([a-z]+)/.exec(k.ziel) || [])[1])
|
||
.filter(Boolean),
|
||
/* DIESELBE LUECKE WIE BEI TREFF_BRETTER (20.09.2026), und aus
|
||
demselben Grund: `regeln` hat seit dem 19.09. keine Kachel mehr,
|
||
faellt damit aus jeder Ableitung ueber Kachelziele -- und ist
|
||
trotzdem ein Brett, das `treff-regeln.html` verlinkt.
|
||
|
||
Zwei Ableitungen, dieselbe blinde Stelle. Das ist der Grund,
|
||
warum hier eine Zeile steht statt einer zweiten klugen Regel:
|
||
Wer eine Kachel umleitet, nimmt einem Brett damit stillschweigend
|
||
die Erreichbarkeit. Das muss sichtbar sein, nicht gerechnet. */
|
||
"regeln",
|
||
/* Und `treff` aus demselben Grund (21.09.2026): Seine Kachel zeigt
|
||
seit heute auf die Willkommensseite. Ohne diese Zeile sieht ein
|
||
Modi sechs von sieben Treff-Brettern -- und das siebte ist
|
||
ausgerechnet das, das dem Bereich den Namen gibt. */
|
||
"treff",
|
||
]);
|
||
|
||
/** Und die Bretter hinter einer Seite -- nur fuer den, der die Seite darf.
|
||
*
|
||
* DER DRITTE AUSGANG FEHLT HIER ABSICHTLICH: Ein Brett, das nicht in
|
||
* der Zuordnung steht, ist keine offene Frage, sondern schlicht nicht
|
||
* gemeint. `false` ist die richtige Antwort. */
|
||
export function brettHinterSeiteOffen(person, brett) {
|
||
const seite = BRETTER_UEBER_SEITEN[brett];
|
||
return !!seite && darfSeite(person, seite);
|
||
}
|
||
|
||
/* Der Satz unter der Begruessung. Die fuenf bekannten Rollen haben ihn
|
||
in assets/js/start.js stehen ("Creator · dein eigener Bereich"); fuer
|
||
einen Modi darf er dort nicht stehen und kommt deshalb von hier.
|
||
|
||
OHNE IHN STAND DA NUR DAS WORT "Modi" -- neben fuenf Rollen, die einen
|
||
ganzen Satz bekommen. Das sieht nicht verborgen aus, sondern
|
||
unfertig, und Filipe haette zu Recht gefragt, warum seine Seite
|
||
halbfertig wirkt. Gefunden hat das die Browserpruefung, nicht das
|
||
Nachdenken.
|
||
|
||
AUF AUGENHOEHE FORMULIERT: "dein Bereich im Team", nicht "dein
|
||
Bereich unter DogFather". Ein Modi ist Teil des Teams, kein
|
||
Untergebener -- das gilt fuer jeden Text im Haus. */
|
||
const MODI_ROLLENTEXT = "Modi · dein Bereich im Team";
|
||
|
||
/* DIE ZIERZEILE UEBER "DIE ZENTRALE".
|
||
|
||
Sie nennt den BETRIEB, unter dem gearbeitet wird -- seit dem
|
||
08.09.2026 auf Filipes Wunsch "Spicy Media" statt "Dogfather
|
||
Universe". Das steht fest in start.html, und fuer die fuenf bekannten
|
||
Rollen stimmt es auch.
|
||
|
||
FUER EINEN MODI STIMMT ES NICHT. Ein Modi gehoert nicht zur Agentur,
|
||
er moderiert die Lives -- "Team Dogi" heisst diese Runde auch auf der
|
||
oeffentlichen Seite (team-modis.html). Bis hierher stand ueber seiner
|
||
Startseite die Marke eines Betriebs, mit dem er nichts zu tun hat.
|
||
Aufgefallen ist das nicht beim Nachdenken, sondern auf dem
|
||
Bildschirmfoto der Browserpruefung.
|
||
|
||
Wie ueberall kommt der Text vom Server: Ein zweiter Markenname in
|
||
start.html waere zwar kein Verrat (er sagt nichts ueber Modis), aber
|
||
eine Zeile, die fuer fast jeden Leser tot dasteht -- und tote Zeilen
|
||
werden irgendwann falsch erklaert. */
|
||
const MODI_MARKE = "Team Dogi";
|
||
|
||
/* =====================================================================
|
||
DIE KATEGORIEN EINER MODI-AUFGABE (10.09.2026)
|
||
|
||
Es sind die des Anforderungsdokuments -- alle dreizehn, die der
|
||
Aufgabenkatalog in Teil 2 tatsaechlich benutzt, dazu "Sonstiges" aus
|
||
Kapitel 6.1.
|
||
|
||
BEIM ERSTEN ANLAUF WAREN ES ACHT, mit der Begruendung, dreizehn
|
||
Knoepfe seien auf einem Handy unbedienbar. Das war eine plausible
|
||
Erklaerung fuer etwas Falsches: Gebaut ist ein AUSWAHLFELD, keine
|
||
Knopfleiste -- vierzehn Eintraege darin sind voellig unauffaellig.
|
||
Die Zusammenfassung haette eine Uebersetzungstabelle noetig gemacht
|
||
("Branding gehoert zu Planung"), die spaeter niemand nachvollziehen
|
||
kann. Jetzt steht an jeder Aufgabe die Kategorie, die im Dokument
|
||
danebensteht.
|
||
|
||
DIE REIHENFOLGE IST DIE DER HAEUFIGKEIT, nicht die des Dokuments:
|
||
Was taeglich vorkommt, steht oben. Wer eine Kategorie sucht, soll
|
||
nicht scrollen muessen.
|
||
|
||
SIE STEHEN AUF DEM SERVER, nicht in assets/js/aufgaben.js. Nicht weil
|
||
die Woerter etwas verraten wuerden -- "Clipping" sagt nichts --,
|
||
sondern weil eine Liste, die im Browser jedes Menschen liegt und nur
|
||
bei einem einzigen benutzt wird, die Frage aufwirft, fuer wen sie da
|
||
ist. Genau diese Frage soll niemand stellen. */
|
||
/* DIE VIERZEHN KATEGORIEN -- jetzt mit einem Satz dazu (16.09.2026).
|
||
*
|
||
* Filipe: "perfektionniere die kategorien wo ich aufgaben an die modis
|
||
* verteile."
|
||
*
|
||
* Bis heute standen hier vierzehn nackte Namen. "Organisation" und
|
||
* "Planung" nebeneinander, ohne einen Satz, der sagt, was worin
|
||
* gehoert -- und dann landet dieselbe Aufgabe beim einen unter
|
||
* Planung, beim anderen unter Organisation, und die Auswertung
|
||
* darueber ist wertlos.
|
||
*
|
||
* Der Satz steht deshalb an der Kategorie und nicht in einer
|
||
* Anleitung: Er wird dort gebraucht, wo man sich entscheidet. */
|
||
export const MODI_KATEGORIEN = [
|
||
{ wert: "chat", name: "Chat-Moderation",
|
||
text: "Was im Livechat passiert, während gesendet wird. Löschen, "
|
||
+ "stummschalten, begrüßen, deeskalieren." },
|
||
{ wert: "community", name: "Community",
|
||
text: "Die Menschen außerhalb der Sendung: wer wiederkommt, wer neu "
|
||
+ "ist, wer lange nicht da war." },
|
||
{ wert: "events", name: "Events",
|
||
text: "Alles mit einem Termin: Matches, Aktionen, Geburtstage, "
|
||
+ "besondere Sendungen." },
|
||
{ wert: "clipping", name: "Clipping",
|
||
text: "Aus einer Sendung viele Videos machen — schneiden, "
|
||
+ "beschriften, hochladen." },
|
||
{ wert: "social", name: "Social Media",
|
||
text: "Alles außerhalb des Livestreams: Ankündigungen, Beiträge, "
|
||
+ "Antworten unter Videos." },
|
||
{ wert: "technik", name: "Technik",
|
||
text: "Ton, Bild, Verbindung, Geräte. Meistens unsichtbar — und "
|
||
+ "sofort sichtbar, wenn es fehlt." },
|
||
{ wert: "planung", name: "Planung",
|
||
text: "Was NOCH NICHT ist: Sendeplan, Themen, wer wann kann." },
|
||
{ wert: "organisation", name: "Organisation",
|
||
text: "Was BEREITS ist, in Ordnung halten: Ablage, Listen, Zugänge, "
|
||
+ "Übersichten." },
|
||
{ wert: "team", name: "Team",
|
||
text: "Die Zusammenarbeit selbst: Übergaben, Einarbeitung, "
|
||
+ "Absprachen, Vertretung." },
|
||
{ wert: "kommunikation", name: "Kommunikation",
|
||
text: "Was nach außen geht und beantwortet werden muss: "
|
||
+ "Nachrichten, Anfragen, Kooperationen." },
|
||
{ wert: "analyse", name: "Analyse",
|
||
text: "Nachsehen statt schätzen: Zahlen, Verläufe, was sich "
|
||
+ "verändert hat." },
|
||
{ wert: "wachstum", name: "Wachstum",
|
||
text: "Gezielt mehr werden: neue Zuschauer, neue Kanäle, neue "
|
||
+ "Zeiten ausprobieren." },
|
||
{ wert: "branding", name: "Branding",
|
||
text: "Wie es aussieht und klingt: Farben, Zeichen, Sprache, "
|
||
+ "Wiedererkennung." },
|
||
{ wert: "sonstiges", name: "Sonstiges",
|
||
text: "Was in keine der dreizehn passt. Bleibt absichtlich klein — "
|
||
+ "wird es groß, fehlt eine Kategorie." },
|
||
];
|
||
|
||
/** Die drei Stufen einer Modi-Aufgabe.
|
||
*
|
||
* DIESELBE IDEE WIE BEI DEN CREATOR-VORLAGEN, andere Worte: Dort heisst
|
||
* es Anfaenger/Fortgeschritten/Profi, und das passt fuer jemanden, der
|
||
* etwas LERNT. Hier geht es nicht um Koennen, sondern darum, wie lange
|
||
* jemand dabei ist und wie viel Vertrauen die Aufgabe braucht.
|
||
*
|
||
* Eine Aufgabe der Stufe "erfahren" an einen Neuen zu geben ist kein
|
||
* Kompliment, sondern ein Ueberfallen. */
|
||
export const MODI_STUFEN = [
|
||
{ wert: "neu", name: "Neu dabei",
|
||
text: "Die ersten Wochen. Klar umrissen, ohne Folgen, wenn etwas "
|
||
+ "schiefgeht — und jemand ist erreichbar." },
|
||
{ wert: "dabei", name: "Eingearbeitet",
|
||
text: "Kennt den Laden. Kann allein entscheiden, ohne vorher zu "
|
||
+ "fragen, und weiß, wann man doch fragt." },
|
||
{ wert: "erfahren", name: "Erfahren",
|
||
text: "Trägt Verantwortung für andere: arbeitet ein, vertritt, "
|
||
+ "entscheidet im Zweifel." },
|
||
];
|
||
/**
|
||
* Darf diese Person mit Kategorien arbeiten?
|
||
*
|
||
* Ein Modi (es sind seine Aufgaben) und die DogFather-Rolle (sie sieht
|
||
* seine Aufgaben und legt ihm welche an). Sonst niemand -- und "sonst
|
||
* niemand" heisst hier: Die Liste kommt gar nicht erst mit, statt
|
||
* mitzukommen und ausgeblendet zu werden.
|
||
*/
|
||
/* =====================================================================
|
||
DIE KANAELE (17.09.2026)
|
||
|
||
Filipe: "ich hab ja auch 2 neben account, hasidog und dogfather
|
||
clips." Bis heute wusste der Arbeitsplatz davon nichts -- es gab
|
||
genau eine Welt, und die hiess nirgends.
|
||
|
||
WARUM DAS EIN ECHTER MANGEL WAR, nicht nur eine fehlende Spalte:
|
||
"Schneide drei Ausschnitte" ist bei drei Accounts keine Aufgabe,
|
||
sondern eine Frage. Wer sie bekommt, muss nachfragen -- oder raet.
|
||
Raet er falsch, ist die Arbeit nicht halb getan, sondern am
|
||
falschen Ort gelandet, und das faellt erst auf, wenn jemand es
|
||
sieht. Genau diese Sorte Mehrdeutigkeit kostet doppelt.
|
||
|
||
WARUM EINE LISTE IM QUELLTEXT UND KEINE TABELLE.
|
||
Sonst gilt hier: Eine abgeschriebene Liste altert. Diese hier ist
|
||
keine Abschrift -- sie laesst sich aus nichts ableiten, weil sie
|
||
eine Tatsache ueber Filipes Betrieb ist und nirgends sonst steht.
|
||
Sie darf deshalb hier stehen, und einen Kanal dazuzunehmen ist eine
|
||
Zeile. Sobald sich das oefter aendert als ein paarmal im Jahr,
|
||
gehoert sie in die Verwaltung -- vorher waere das eine Oberflaeche
|
||
fuer drei Zeilen.
|
||
|
||
KEIN KANAL IST EIN GUELTIGER ZUSTAND. Vieles gilt fuer alles (eine
|
||
Absprache im Team, ein Zugang, eine Auswertung). Ein Pflichtfeld
|
||
haette dafuer einen falschen Kanal erzwungen -- und ein falscher
|
||
Eintrag ist schlechter als ein leerer.
|
||
===================================================================== */
|
||
/* ZWEI SAETZE JE KANAL, WEIL ES ZWEI LESERSCHAFTEN GIBT (21.09.2026).
|
||
|
||
`text` erklaert dem TEAM, wofuer der Kanal da ist -- es steht an
|
||
einer Aufgabe und beantwortet "wo soll das hin". `fuerDraussen`
|
||
steht auf der Community-Seite und beantwortet etwas anderes: "was
|
||
finde ich da". "Hier wird geschnitten, beschriftet und hochgeladen"
|
||
ist fuer die Zuschauerin keine Auskunft, sondern ein Blick in eine
|
||
fremde Werkstatt.
|
||
|
||
ES BLEIBT EINE LISTE. Der zweite Satz haengt am Kanal, nicht in
|
||
einer zweiten Aufzaehlung woanders -- sonst haette ein vierter
|
||
Account wieder zwei Orte, und einer davon wird vergessen. Genau so
|
||
ist am selben Tag "@hasidog00804" entstanden. */
|
||
export const KANAELE = [
|
||
{ wert: "dogfather", name: "DogFather", handle: "@dogfather0804",
|
||
text: "Der Hauptkanal. Die Livestreams und alles, was unmittelbar "
|
||
+ "daran hängt.",
|
||
fuerDraussen: "Der Hauptkanal — hier laufen die Livestreams." },
|
||
{ wert: "hasidog", name: "HasiDog", handle: "@hasidog0804",
|
||
text: "Der zweite Account — eigene Zuschauer, eigener Ton. Was hier "
|
||
+ "läuft, ist nicht einfach dasselbe noch einmal.",
|
||
fuerDraussen: "Der zweite Account. Eigener Ton, eigene Leute." },
|
||
{ wert: "clips", name: "DogFather Clips", handle: "@dogis.modi.gang",
|
||
text: "Nur Ausschnitte. Hier wird geschnitten, beschriftet und "
|
||
+ "hochgeladen, nicht gesendet.",
|
||
fuerDraussen: "Die besten Ausschnitte zum Nachschauen." },
|
||
];
|
||
|
||
/** Zu welchem Kanal gehoert dieser TikTok-Name?
|
||
*
|
||
* DIE HANDLES STEHEN AM KANAL UND NICHT IN EINER ZWEITEN LISTE
|
||
* (17.09.2026). Der Video-Plan nennt genau das als die eine Stelle,
|
||
* an der etwas auseinanderlaufen kann: Sie stehen ohnehin schon in
|
||
* der Analyse-App (`TIKTOK_ACCOUNTS_LIST`). Wer einen vierten Account
|
||
* aufmacht und nur einen der beiden Orte pflegt, bekommt Videos, die
|
||
* zu keinem Kanal gehoeren.
|
||
*
|
||
* Hier gibt es deshalb genau EINE Liste, und sie haengt an dem
|
||
* Begriff, zu dem sie gehoert.
|
||
*
|
||
* `null` heisst: gehoert uns nicht. Das ist KEIN Sonderfall, sondern
|
||
* der wichtigste Ausgang -- ein fremdes Video hat in Filipes
|
||
* Highlights nichts zu suchen. */
|
||
export function kanalVonHandle(handle) {
|
||
const h = String(handle || "").trim().toLowerCase().replace(/^@?/, "@");
|
||
return KANAELE.find((k) => k.handle.toLowerCase() === h)?.wert ?? null;
|
||
}
|
||
|
||
/** Wer darf mit Kanaelen arbeiten? Dieselbe Runde wie bei den
|
||
* Kategorien -- das Team und DogFather. Fuer alle anderen kommt die
|
||
* Liste gar nicht erst mit, statt mitzukommen und ausgeblendet zu
|
||
* werden. */
|
||
export function kanaeleFuer(person) {
|
||
return istTeamDogi(person) ? KANAELE : [];
|
||
}
|
||
|
||
/* =====================================================================
|
||
DIE ALTEN ZURUECKGENOMMENEN NACHRICHTEN GEHEN GANZ (22.09.2026)
|
||
=====================================================================
|
||
|
||
Filipe: "Geloeschte Nachrichten verschwinden vollstaendig -- keine
|
||
Spur, kein ‚wurde geloescht'-Hinweis, bei niemandem, auch nicht bei
|
||
DogFather."
|
||
|
||
Seit dem 22.09.2026 loescht der Chat wirklich (DELETE). Was davor
|
||
zurueckgenommen wurde, steht aber noch als Zeile da: Text leer,
|
||
`weg_am` gesetzt. Die Anzeige zeigt sie nicht mehr -- in der
|
||
Datenbank waere sie trotzdem. "Keine Spur" gaelte dann fuer alles,
|
||
was ab heute passiert, und nicht fuer das, was ihn zu dem Satz
|
||
gebracht hat.
|
||
|
||
ES GEHT NUR UM ZEILEN OHNE INHALT. `text = ''` ist die Bedingung,
|
||
nicht bloss `weg_am IS NOT NULL`: Der Text wurde beim Zuruecknehmen
|
||
geleert, also ist hier nichts zu verlieren. Stuende irgendwo doch
|
||
noch Text in einer zurueckgenommenen Zeile, bliebe sie liegen --
|
||
lieber ein Rest, den jemand ansehen kann, als ein stiller Verlust.
|
||
|
||
WIEDERHOLBAR: Beim zweiten Start gibt es nichts mehr zu tun, und es
|
||
passiert nichts. GESICHERT WIRD VORHER -- aber nur, wenn es wirklich
|
||
etwas zu tun gibt. Eine Sicherung bei jedem Start waere eine Datei
|
||
je Neustart und damit kein Schutz, sondern Muell.
|
||
|
||
---------------------------------------------------------------------
|
||
ALS EIGENE, EXPORTIERTE FUNKTION -- und das hat einen Grund, der an
|
||
genau dieser Stelle entstanden ist.
|
||
|
||
In der ersten Fassung stand `d.prepare("DELETE ...").changes` statt
|
||
`.run().changes`: die Eigenschaft der vorbereiteten ANWEISUNG, nicht
|
||
die des Laufs. Die Anweisung wurde vorbereitet und nie ausgefuehrt.
|
||
Nichts stuerzte ab, nichts war rot. Im Protokoll stand "undefined
|
||
zurueckgenommene Nachrichten endgueltig entfernt" -- ein Zaehler,
|
||
der `undefined` sagt, hat nicht gezaehlt, und was nicht zaehlt, hat
|
||
meist auch nicht gearbeitet. Die fuenf Zeilen blieben liegen, und
|
||
weil die Sicherung VOR dem Loeschen geschrieben wird, waere bei
|
||
jedem Neustart eine weitere Sicherungsdatei entstanden.
|
||
|
||
Eine Umstellung, die tief im Hochfahren steckt, kann keine Pruefung
|
||
aufrufen. Eine Funktion schon: pruef-loeschen.mjs gibt ihr eine
|
||
vorbereitete Datenbank und sieht nach, was sie WIRKLICH getan hat.
|
||
|
||
@returns {{entfernt:number, gesichert:string|null}}
|
||
===================================================================== */
|
||
export function loeschspurenAufraeumen(d, dbPfad = DB_PFAD) {
|
||
try {
|
||
const spalten = d.prepare("PRAGMA table_info(chat_nachrichten)").all().map((x) => x.name);
|
||
if (!spalten.includes("weg_am")) return { entfernt: 0, gesichert: null };
|
||
const offen = d.prepare(
|
||
"SELECT COUNT(*) AS n FROM chat_nachrichten WHERE weg_am IS NOT NULL AND text = ''").get().n;
|
||
if (offen === 0) return { entfernt: 0, gesichert: null };
|
||
|
||
let sicherung = null;
|
||
if (dbPfad) {
|
||
const stempel = new Date().toISOString().replace(/[-:T]/g, "").slice(0, 14);
|
||
sicherung = `${dbPfad}.vor-loeschspuren-${stempel}`;
|
||
d.exec(`VACUUM INTO '${sicherung.replace(/'/g, "''")}'`);
|
||
console.log("[workspace] Sicherung vor dem Aufraeumen:", sicherung);
|
||
}
|
||
/* Reaktionen und Erwaehnungen haengen mit ON DELETE CASCADE daran
|
||
und gehen von selbst mit -- `PRAGMA foreign_keys = ON` steht
|
||
beim Oeffnen. */
|
||
const entfernt = d.prepare(
|
||
"DELETE FROM chat_nachrichten WHERE weg_am IS NOT NULL AND text = ''").run().changes;
|
||
/* Und die Raeume rechnen ihren letzten Zeitpunkt neu -- sonst
|
||
stuende einer oben in der Liste, zu dessen Zeitpunkt es nichts
|
||
mehr gibt. */
|
||
d.exec(`UPDATE chat_raeume SET letzte_am =
|
||
(SELECT MAX(erstellt) FROM chat_nachrichten WHERE raum_id = chat_raeume.id)`);
|
||
console.log(`[workspace] ${entfernt} zurueckgenommene Nachrichten endgueltig entfernt.`);
|
||
return { entfernt, gesichert: sicherung };
|
||
} catch (fehler) {
|
||
console.error("[workspace] Aufraeumen der Loeschspuren:", fehler?.message);
|
||
return { entfernt: 0, gesichert: null };
|
||
}
|
||
}
|
||
|
||
export function kategorienFuer(person) {
|
||
return istTeamDogi(person) ? MODI_KATEGORIEN : [];
|
||
}
|
||
|
||
/**
|
||
* FUER WESSEN AUFGABEN gilt die Kategorie? (10.09.2026)
|
||
*
|
||
* null -> fuer alle eigenen; die Oberflaeche zeigt das Feld immer
|
||
* [Nummern] -> nur wenn eine dieser Personen gewaehlt ist
|
||
* [] -> nie
|
||
*
|
||
* WARUM DAS DER SERVER BEANTWORTET UND NICHT DER BROWSER: Die
|
||
* Oberflaeche muesste sonst `p.rolle === 'modi'` vergleichen -- und
|
||
* assets/js/aufgaben.js bekommt JEDER, der die Seite oeffnet. Genau so
|
||
* stand es beim ersten Anlauf da, an sechs Stellen. Mit Nummern statt
|
||
* einem Rollennamen steht im Browser nichts, woraus sich etwas
|
||
* schliessen laesst: Wer keine bekommt, sieht eine leere Liste, und
|
||
* eine leere Liste sagt nichts.
|
||
*/
|
||
export function kategoriePersonen(person) {
|
||
if (!person) return [];
|
||
/* Ein Modi arbeitet ausschliesslich an eigenen Aufgaben -- dort gilt
|
||
sie immer, unabhaengig davon, wen er eintraegt. */
|
||
if (person.rolle === "modi") return null;
|
||
if (person.rolle !== "admin" && !istHand(person)) return [];
|
||
try {
|
||
/* Auch die rechte Hand selbst: Sie traegt Aufgaben ein -- fuer
|
||
andere UND fuer sich. Waere ihre eigene Nummer nicht dabei,
|
||
verschwaende das Kategoriefeld genau dann, wenn sie sich selbst
|
||
etwas notiert. */
|
||
const liste = [...TEAM_DOGI_ROLLEN].map((r) => `'${r}'`).join(", ");
|
||
return db().prepare(
|
||
`SELECT id FROM personen WHERE rolle IN (${liste}) AND aktiv = 1`).all().map((z) => z.id);
|
||
} catch {
|
||
/* Ohne Datenbank lieber kein Feld als ein falsches. */
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/* Der Satz der rechten Hand sagt, was ihre Rolle ist, ohne sie ueber
|
||
die anderen zu stellen: Sie haelt den Ueberblick, sie steht nicht
|
||
darueber. */
|
||
const HAND_ROLLENTEXT = "Rechte Hand · du hast alles im Blick und entscheidest mit";
|
||
const LINKE_ROLLENTEXT = "Linke Hand · du fuehrst das Team durch den Tag";
|
||
|
||
export function rollentextFuer(person) {
|
||
if (person?.rolle === "modi") return MODI_ROLLENTEXT;
|
||
if (person?.rolle === "hand") return HAND_ROLLENTEXT;
|
||
if (person?.rolle === "linke") return LINKE_ROLLENTEXT;
|
||
/* DIE COMMUNITY HATTE NUR EIN WORT (nachgetragen 19.09.2026).
|
||
|
||
Bei jeder anderen Rolle steht unter dem Titel ein Satz. Beim
|
||
Community-Mitglied stand dort das nackte Wort "Community" -- und
|
||
das ist woertlich derselbe Mangel, den der Kommentar bei
|
||
MODI_ROLLENTEXT beschreibt und fuer den Modi behebt: "das sieht
|
||
nicht verborgen aus, sondern unfertig."
|
||
|
||
Fuer den Menschen, der von aussen kommt und niemanden hier kennt,
|
||
ist der erste Satz nach dem Anmelden der wichtigste im ganzen
|
||
Haus. */
|
||
if (person?.rolle === "gast") return "Community · dein Platz im Rudel";
|
||
return null;
|
||
}
|
||
|
||
/* DIE MARKE HAENGT AN ZWEI DINGEN, NICHT AN EINEM (10.09.2026).
|
||
*
|
||
* An der ROLLE: Wer zu Team Dogi gehoert, sieht ueberall den Namen
|
||
* seines Teams -- auch dann, wenn er die Seite ueber eine andere
|
||
* Adresse aufmacht.
|
||
*
|
||
* Und an der ADRESSE: Auf crew.dogfather-universe.com gibt es Spicy
|
||
* Media nicht. Dort stand ueber der Zentrale trotzdem "SPICY MEDIA" --
|
||
* gross, mitten im Bild, weil diese Funktion nur die Rolle kannte und
|
||
* DogFather nun einmal zur Agentur gehoert. Filipe: "ich will doch
|
||
* dass der eingang hier getrennt ist von der workspace seite."
|
||
*
|
||
* Zwei Fragen, EINE Antwortstelle. Wer die Adressbedingung stattdessen
|
||
* an der Aufrufstelle prueft, hat sie beim naechsten Aufruf nicht --
|
||
* und diese Funktion wird ueber kurz oder lang ein zweites Mal
|
||
* gebraucht. */
|
||
export function markeFuer(person, hostKopfzeile) {
|
||
if (istCrewAdresse(hostKopfzeile)) return MODI_MARKE;
|
||
return TEAM_DOGI_ROLLEN.has(person?.rolle) ? MODI_MARKE : null;
|
||
}
|
||
|
||
/* =====================================================================
|
||
KACHELN, DIE ZU EINER VORHANDENEN LISTE DAZUKOMMEN (10.09.2026)
|
||
|
||
bereicheFuer() ersetzt die Liste im Browser ganz -- das passt fuer
|
||
eine Rolle, die dort gar nicht vorkommt. Fuer die DogFather-Rolle
|
||
passt es nicht: Sie hat ihre 23 Kacheln, und es soll EINE dazu.
|
||
|
||
Sie steht hier und nicht in assets/js/bereiche.js, weil dort jeder
|
||
mitliest. "Team-Lage" allein verraet zwar nichts -- aber eine
|
||
Kachel, die nur bei einer einzigen Rolle erscheint, wirft die Frage
|
||
auf, fuer wen sie ist. Genau die soll niemand stellen.
|
||
===================================================================== */
|
||
/* ZWEIMAL "TEAM-LAGE" -- KORRIGIERT AM 10.09.2026.
|
||
|
||
Hier stand: Gruppe "Rund um das Team", Name "Team-Lage", Ton 22.
|
||
In bereiche.js gibt es aber SCHON eine Kachel "Team-Lage" mit Ton 22
|
||
und demselben Zeichen, die nach team.html fuehrt. DogFather hatte
|
||
damit zwei Kacheln, die gleich heissen, gleich aussehen, gleich
|
||
gefaerbt sind, nebeneinander in derselben Gruppe stehen -- und zu
|
||
verschiedenen Seiten fuehren.
|
||
|
||
Gefunden hat das nicht das Auge, sondern pruef-start-ansicht mit dem
|
||
Satz "jede Kachel hat ihre eigene Farbe (23 Farben auf 24 Kacheln)".
|
||
Ohne diese Zeile waere es stehen geblieben; zwei gleiche Kacheln
|
||
fallen niemandem auf, der weiss, welche er meint.
|
||
|
||
Jetzt drei Unterschiede statt keinem: eigener Name, eigene Gruppe,
|
||
eigene Farbe. Das Zeichen bleibt geteilt -- nachgemessen sind alle
|
||
22 Zeichen des Hauses bei DogFather bereits in Gebrauch, und ein
|
||
geteiltes Zeichen bei verschiedenem Namen, Ort und Ton ist keine
|
||
Verwechslungsgefahr (profil steht ohnehin schon zweimal).
|
||
|
||
"Eingang" und nicht "Rueckmeldungen": Die Kachel steht bei den
|
||
taeglichen Dingen, weil dort etwas WARTET -- Vorschlaege, ueber die
|
||
entschieden werden will. Das ist eine Aufgabe fuer heute, keine
|
||
Uebersicht fuer zwischendurch. */
|
||
/* =====================================================================
|
||
DIE EIGENE GRUPPE FUER DAS TEAM (10.09.2026)
|
||
|
||
Filipe zum Bildschirmfoto seiner Startseite: "ich will dass die
|
||
kacheln von den modis bei der rolle dogfather eine eigene kategorie
|
||
haben. damit ich nicht zwischen den kacheln suchen muss."
|
||
|
||
Er hatte recht, und es war schlimmer als unuebersichtlich: Der
|
||
Eingang stand unter "Taeglich" zwischen Chat und Kalender, das
|
||
Ideen-Board ebenfalls -- zwei Kacheln derselben Welt, verteilt auf
|
||
eine Reihe mit sieben anderen. Wer sie sucht, liest jedes Mal alle.
|
||
|
||
`gruppeNach` setzt die Gruppe direkt hinter "Taeglich" statt ans
|
||
Ende. Ganz unten waere zwar auch eine eigene Kategorie -- und immer
|
||
noch Scrollen.
|
||
|
||
DAS IDEEN-BOARD IST DABEI VON assets/js/bereiche.js HIERHER GEZOGEN.
|
||
Zwei Gruende, und der zweite ist der wichtigere:
|
||
1. Es gehoert in dieselbe Gruppe wie der Eingang, und die entsteht
|
||
hier.
|
||
2. Es stand mit `rollen: ['admin']` in einer Datei, die JEDER
|
||
herunterlaedt. Eine Kachel, die nur eine Rolle sieht, wirft dort
|
||
die Frage auf, wofuer sie ist -- genau die Frage, die niemand
|
||
stellen soll. Jetzt kommt sie vom Server und nur an die, die sie
|
||
bekommt.
|
||
|
||
Der Gruppenname "Team Dogi" ist unverfaenglich: Er steht so auf der
|
||
oeffentlichen Seite. */
|
||
/* GANZ NACH UNTEN (11.09.2026).
|
||
|
||
Filipe: "diese zwei kategorien sollen auf dieser seite (workspace)
|
||
ganz unten sein. die letzten kategorien bei der rolle dogfather ...
|
||
goldene regel das sind private informationen von meiner community."
|
||
|
||
Vorher stand hier `gruppeNach: "Täglich"` -- damit sassen die
|
||
privatesten Kacheln des Hauses zwischen den taeglichen Werkzeugen,
|
||
also genau dort, wo der Blick zuerst hinfaellt und wo jemand
|
||
mitliest, der einem ueber die Schulter schaut.
|
||
|
||
"Team & System" ist die letzte Gruppe der Workspace-Liste
|
||
(bereiche.js). Sie steht hier als NAME und nicht als Position: Eine
|
||
Zahl ("an vierter Stelle") waere beim naechsten Umsortieren still
|
||
falsch -- und still falsch heisst hier: privates Material rutscht
|
||
wieder nach oben. Verschiebt jemand die Gruppen, wandert Team Dogi
|
||
mit, solange "Team & System" die letzte bleibt.
|
||
|
||
pruef-start-ansicht misst die Reihenfolge nach, damit aus "solange"
|
||
keine Hoffnung wird. */
|
||
const TEAM_GRUPPE = {
|
||
gruppe: "Team Dogi",
|
||
gruppeUnter: "Was dein Team macht – und was auf dich wartet",
|
||
gruppeNach: "Team & System",
|
||
};
|
||
|
||
/* =====================================================================
|
||
KEINE MISCHUNG MEHR (21.09.2026)
|
||
|
||
Filipe: "ich will dass du im workspace alles von team dogi weg
|
||
nimmst. nur was ich im kalender eintrage soll ich im workspace sehen
|
||
und im team dogi. aber die kacheln von team dogi soll ich nur bei
|
||
team dogi sehen und die von workspace nur bei workspace bitte. ich
|
||
will keine mischung mehr von kacheln. wie gesagt nur die der kalender
|
||
soll verbunden sein von dogfather sonst nichts."
|
||
|
||
---------------------------------------------------------------------
|
||
WAS HIER BIS HEUTE STAND
|
||
|
||
Eine Liste ZUSATZ_BEREICHE mit siebzehn Kacheln, die auf der
|
||
AGENTURADRESSE an DogFathers Seite gehaengt wurden: Dein Team,
|
||
Ideen-Board, Rueckmeldung, Wer sieht was, Entwicklung, Werdegang,
|
||
Talente -- und der ganze Treff samt Moderation. Nachgemessen am
|
||
21.09.2026 bekam er dort 0 eigene Kacheln und 17 fremde; auf der
|
||
Team-Adresse 30, in sechs Gruppen, aus beiden Haeusern.
|
||
|
||
Der Gedanke dahinter war gut gemeint: Er soll ueberall an alles
|
||
herankommen. Die Wirkung war eine Seite, auf der zwei Betriebe
|
||
uebereinanderliegen -- und zwar genau der Zustand, den dieselbe
|
||
Datei am 10.09. fuer die andere Richtung schon als Fehler erkannt
|
||
hatte ("Fuenfundzwanzig Kacheln, von denen zwei Drittel Creator,
|
||
Scouts und Agentur betreffen, waeren dort Fenster in ein Haus, in
|
||
dem er gerade nicht ist").
|
||
|
||
---------------------------------------------------------------------
|
||
DIE EINE AUSNAHME IST DER KALENDER -- UND SIE BRAUCHT NICHTS
|
||
|
||
Nachgemessen: Die Terminabfragen in workspace-kalender.js filtern
|
||
NICHT nach Haus. Was er eintraegt, steht ohnehin auf beiden
|
||
Adressen. Die Verbindung, die er will, existiert also bereits; sie
|
||
musste nicht gebaut, sondern nur nicht zerschnitten werden.
|
||
|
||
---------------------------------------------------------------------
|
||
EINE TUER IST KEINE MISCHUNG
|
||
|
||
Ohne Ersatz waere von der Agenturadresse aus kein Weg mehr zu Team
|
||
Dogi sichtbar -- er muesste die Adresse tippen. Das ist die Sorte
|
||
Sackgasse, die dieses Haus nicht baut. Statt siebzehn Brettern steht
|
||
dort jetzt EINE Kachel, die hinuebergeht.
|
||
|
||
SIE VERRAET NICHTS: Sie kommt vom Server und nur fuer die, die zu
|
||
Team Dogi gehoeren. In keiner ausgelieferten Datei steht sie, und
|
||
ein Manager bekommt sie nie zu sehen -- genau wie bei den Kacheln,
|
||
die sie ersetzt.
|
||
===================================================================== */
|
||
export function zusatzBereicheFuer(person) {
|
||
/* Auf der Team-Adresse liefert bereicheFuer() ohnehin die Kacheln des
|
||
Teams. Eine Tuer zu dem Haus, in dem man schon steht, waere ein
|
||
Knopf, der nichts tut. */
|
||
if (person?.haus === "crew") return [];
|
||
|
||
/* NUR DOGFATHER UND VANVAN -- also `admin`.
|
||
|
||
Der erste Entwurf gab die Tuer auch an TEAM_DOGI_ROLLEN (die
|
||
rechte Hand und die Moderation). Das war falsch, und pruef-modi
|
||
-checkliste hat es sofort gemeldet: "Modi bekommt 1
|
||
Zusatzkachel(n), erwartet keine".
|
||
|
||
WARUM ES FALSCH IST: Wer NUR zu Team Dogi gehoert, hat auf der
|
||
Agenturadresse gar nichts zu suchen -- die Tuer waere dort seine
|
||
einzige Kachel, also eine Seite, die aus nichts als einem Ausgang
|
||
besteht. Und sie wuerde eine Adresse nennen, die auf dieser Seite
|
||
niemand nennen soll.
|
||
|
||
`admin` ist der einzige Fall, der wirklich zwei Haeuser hat. */
|
||
if (person?.rolle !== "admin") return [];
|
||
|
||
return [{
|
||
gruppe: "Team Dogi",
|
||
gruppeUnter: "Das andere Haus \u2014 eigene Seite, eigene Daten",
|
||
name: "Zu Team Dogi",
|
||
unter: "Das Rudel, das Team und die Moderation \u2014 auf ihrer eigenen Adresse",
|
||
zeichen: "hinaus",
|
||
ton: 40,
|
||
szene: "portal",
|
||
ziel: CREW_ADRESSE + "/workspace/start.html",
|
||
/* AUSSEN heisst: Diese Kachel fuehrt aus dem Haus hinaus. Ohne das
|
||
Merkmal wuerde nurOffeneKacheln() sie wegfiltern -- es baut aus
|
||
dem Ziel einen Pfad ("/workspace/" + ziel) und fragt die
|
||
Rechtetafel. Bei einer vollstaendigen Adresse kommt dabei
|
||
Unsinn heraus, und die Kachel verschwaende lautlos. */
|
||
aussen: true,
|
||
}];
|
||
}
|
||
|
||
/* scrypt-Parameter. N=2^15 braucht auf diesem Server rund 150 ms — spürbar
|
||
genug, um Rateversuche auszubremsen, und unauffällig für einen echten
|
||
Anmeldevorgang. Die Werte werden je Datensatz mitgespeichert, damit sie
|
||
später erhöht werden können, ohne alte Codes ungültig zu machen. */
|
||
const SCRYPT = { N: 32768, r: 8, p: 1, keylen: 64 };
|
||
|
||
let _db = null;
|
||
let _dbFehler = null;
|
||
|
||
/* =====================================================================
|
||
Umstellungen an bestehenden Datenbanken.
|
||
|
||
CREATE TABLE IF NOT EXISTS legt eine Tabelle nur an, wenn sie fehlt --
|
||
eine vorhandene wird NICHT angefasst. Die CHECK-Regel fuer die Rolle
|
||
stand also weiterhin auf den alten drei Werten, und ein Manager waere
|
||
an der Datenbank gescheitert, obwohl im Code alles stimmt.
|
||
|
||
SQLite kann eine CHECK-Regel nicht aendern. Der einzige saubere Weg
|
||
ist: neue Tabelle, Daten hinueber, alte weg, neue umbenennen. Das ist
|
||
der Moment, in dem eine Datenbank kaputtgehen kann -- deshalb wird
|
||
vorher eine vollstaendige Sicherung geschrieben (VACUUM INTO, von
|
||
SQLite selbst und in sich konsistent, anders als ein Dateikopie
|
||
waehrend laufender Schreibvorgaenge).
|
||
===================================================================== */
|
||
/* =====================================================================
|
||
DIE ROLLENLISTE DER DATENBANK ERWEITERN (09.09.2026)
|
||
|
||
Welche Rollen es geben darf, steht als CHECK-Regel in der Tabelle
|
||
`personen`. SQLite kann eine CHECK-Regel nicht aendern -- der einzige
|
||
saubere Weg ist: neue Tabelle, Daten hinueber, alte weg, neue
|
||
umbenennen. Das ist der Moment, in dem eine Datenbank kaputtgehen
|
||
kann. Deshalb steht der Ablauf ab jetzt EINMAL hier.
|
||
|
||
WARUM ALS FUNKTION: Er stand vorher zweimal da -- einmal fuer
|
||
'manager', einmal fuer 'spicy'. Fuer 'modi' waere es die dritte
|
||
Abschrift geworden, und jede Abschrift ist eine Gelegenheit, eine der
|
||
vier Absicherungen zu vergessen: die Sicherung VORHER, die Zaehlung
|
||
INNERHALB der Transaktion, die Spaltenliste aus der Tabelle statt aus
|
||
dem Gedaechtnis, die Pruefung auf verwaiste Verweise DANACH. Der
|
||
manager-Block darueber bleibt unangetastet: Er baut die Tabelle mit
|
||
einer fest hingeschriebenen Spaltenliste auf und ist damit ein
|
||
anderer Fall -- ihn mit einzufangen waere ein zweiter Umbau an einer
|
||
Stelle, an der ein Fehler Daten kostet.
|
||
|
||
GEPRUEFT WIRD DIE REGEL, NICHT DER TEXT (07.09.2026, gefunden von
|
||
pruef-spicy): SQLite hebt den CREATE-Text woertlich auf, mitsamt
|
||
Kommentaren. Ein erklaerender Satz mit dem Wort 'spicy' genuegte, und
|
||
die Umstellung hielt die Tabelle fuer schon umgestellt, obwohl die
|
||
CHECK-Regel noch die alte war. Deshalb wird die Regel herausgeschnitten
|
||
und NUR darin gesucht.
|
||
|
||
@param marker Die Rolle, an der erkannt wird, ob schon umgestellt ist.
|
||
@param rollen Die vollstaendige neue Liste erlaubter Rollen.
|
||
===================================================================== */
|
||
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 ----
|
||
|
||
"Das Rudel -- Hallo sagen, fragen, loben" war eine Liste aus
|
||
Karten. Der einzige Bereich, in dem Menschen MITEINANDER reden
|
||
sollen, zwang sie, nebeneinander zu reden: Jeder schreibt eine
|
||
eigene Karte, niemand antwortet jemandem.
|
||
|
||
EINE EIGENE TABELLE UND KEIN EINTRAG MIT "antwort_auf": Eine
|
||
Antwort hat keine Art, keinen Zustand, keine Frist und gehoert in
|
||
keinen Filter. Sie als halben Eintrag zu fuehren hiesse, ihr
|
||
zwanzig Spalten mitzugeben, die immer leer sind -- und sie taeuchte
|
||
in jeder Zaehlung auf, die Beitraege zaehlt.
|
||
|
||
ON DELETE CASCADE: Verschwindet der Beitrag, verschwinden die
|
||
Antworten. Eine Antwort ohne ihre Frage ist nicht halb so viel
|
||
wert, sondern gar nichts. */
|
||
try {
|
||
d.exec(`
|
||
CREATE TABLE IF NOT EXISTS eintrag_antworten (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
eintrag_id INTEGER NOT NULL REFERENCES eintraege(id) ON DELETE CASCADE,
|
||
person_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
text TEXT NOT NULL,
|
||
erstellt TEXT NOT NULL
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_antworten_eintrag
|
||
ON eintrag_antworten (eintrag_id, id);`);
|
||
} catch (fehler) {
|
||
console.error("[workspace] Antworten-Tabelle:", fehler?.message);
|
||
}
|
||
|
||
/* ---- Der Kreislauf: ein Eintrag kommt aus einem anderen (17.09.2026)
|
||
|
||
Aus dem Plan im Vault, Stufe 8 -- und die Stelle, an der der Plan
|
||
zum dritten Mal danebenlag: Dort stand "`aus_eintrag_id` gibt es in
|
||
der Tabelle bereits". Gibt es -- an den AUFGABEN. Die Eintraege
|
||
hatten nichts dergleichen.
|
||
|
||
WOZU: Heute sind die acht Bretter acht Silos. Ein Wunsch wird
|
||
angenommen, irgendwann passiert etwas im Stream, irgendwann liegt
|
||
ein Clip bei den Highlights -- und nichts davon weiss voneinander.
|
||
Wer den Wunsch geschrieben hat, erfaehrt nie, dass er erfuellt
|
||
wurde.
|
||
|
||
Mit dieser einen Spalte wird daraus eine Kette:
|
||
|
||
Wunsch -> Termin bei "Was ansteht" -> Clip bei "Highlights"
|
||
|
||
ON DELETE SET NULL und nicht CASCADE: Wird der Wunsch geloescht,
|
||
bleibt der Clip. Er ist ja trotzdem passiert. */
|
||
try {
|
||
const spalten = d.prepare("PRAGMA table_info(eintraege)").all().map((s) => s.name);
|
||
if (spalten.length && !spalten.includes("aus_eintrag_id")) {
|
||
d.exec("ALTER TABLE eintraege ADD COLUMN aus_eintrag_id INTEGER REFERENCES eintraege(id) ON DELETE SET NULL");
|
||
d.exec("CREATE INDEX IF NOT EXISTS idx_eintraege_aus ON eintraege (aus_eintrag_id)");
|
||
console.log("[workspace] Spalte 'aus_eintrag_id' an den Eintraegen ergaenzt.");
|
||
}
|
||
} catch (fehler) {
|
||
console.error("[workspace] Spalte 'aus_eintrag_id':", fehler?.message);
|
||
}
|
||
|
||
/* ---- Ein Bild an einem Community-Beitrag (17.09.2026) ----
|
||
|
||
"Highlights -- eure Clips und Bilder" konnte bis heute gar keine
|
||
Bilder tragen: `dateien` hing an `aufgabe_id`, nicht an einem
|
||
Eintrag. Der Plan im Vault sagte "Anhaenge sind schon da" -- sie
|
||
hingen nur am falschen Ding. */
|
||
try {
|
||
const spalten = d.prepare("PRAGMA table_info(dateien)").all().map((s) => s.name);
|
||
if (spalten.length && !spalten.includes("eintrag_id")) {
|
||
d.exec("ALTER TABLE dateien ADD COLUMN eintrag_id INTEGER REFERENCES eintraege(id) ON DELETE CASCADE");
|
||
d.exec("CREATE INDEX IF NOT EXISTS idx_dateien_eintrag ON dateien (eintrag_id)");
|
||
console.log("[workspace] Spalte 'eintrag_id' an den Dateien ergaenzt.");
|
||
}
|
||
} catch (fehler) {
|
||
console.error("[workspace] Spalte 'eintrag_id':", fehler?.message);
|
||
}
|
||
|
||
/* ---- Die Uhrzeit an einem Termin (17.09.2026) ----
|
||
|
||
"Was ansteht" bekommt einen Countdown: "Nächster Stream in 3 Std
|
||
12 Min". Beim Bauen stellte sich heraus, dass die Daten das gar
|
||
nicht hergeben -- `eintraege.datum` ist ein TAG
|
||
(`^\d{4}-\d{2}-\d{2}$`, die Pruefung lehnt eine Uhrzeit ab).
|
||
|
||
Das ist genau die Sorte Luecke, die man beim Planen uebersieht und
|
||
beim Bauen findet: Der Plan versprach eine Stundenangabe, und die
|
||
Datenbank kannte nur Tage.
|
||
|
||
Optional, nicht Pflicht. Viele Termine haben keine Uhrzeit ("diese
|
||
Woche", "im Oktober"), und ein Pflichtfeld haette dafuer eine
|
||
erfundene erzwungen. Ohne Uhrzeit zaehlt der Countdown in Tagen. */
|
||
try {
|
||
const spalten = d.prepare("PRAGMA table_info(eintraege)").all().map((s) => s.name);
|
||
if (spalten.length && !spalten.includes("uhrzeit")) {
|
||
d.exec("ALTER TABLE eintraege ADD COLUMN uhrzeit TEXT");
|
||
console.log("[workspace] Spalte 'uhrzeit' an den Eintraegen ergaenzt.");
|
||
}
|
||
} catch (fehler) {
|
||
console.error("[workspace] Spalte 'uhrzeit':", fehler?.message);
|
||
}
|
||
|
||
/* ---- Der Kanal an der Aufgabe (17.09.2026) ----
|
||
Drei Accounts, eine Aufgabenliste: Ohne diese Spalte ist
|
||
"schneide drei Ausschnitte" bei HasiDog und Clips mehrdeutig.
|
||
ADD COLUMN und kein Tabellenneubau -- es aendert sich keine
|
||
CHECK-Regel, alte Zeilen bleiben leer, und leer heisst hier
|
||
ausdruecklich "gilt fuer alles" und nicht "fehlt". */
|
||
try {
|
||
const spalten = d.prepare("PRAGMA table_info(aufgaben)").all().map((s) => s.name);
|
||
if (spalten.length && !spalten.includes("kanal")) {
|
||
d.exec("ALTER TABLE aufgaben ADD COLUMN kanal TEXT");
|
||
console.log("[workspace] Spalte 'kanal' an den Aufgaben ergaenzt.");
|
||
}
|
||
} catch (fehler) {
|
||
console.error("[workspace] Spalte 'kanal':", fehler?.message);
|
||
}
|
||
|
||
/* ---- Zeitpunkt des Hochladens in der Bibliothek ----
|
||
Eine neue Spalte laesst SQLite anstandslos anhaengen -- anders als
|
||
eine CHECK-Regel. Alte Eintraege bleiben leer und fallen auf das
|
||
Veroeffentlichungsdatum zurueck. */
|
||
try {
|
||
const spalten = d.prepare("PRAGMA table_info(wissen)").all().map((s) => s.name);
|
||
if (spalten.length && !spalten.includes("hochgeladen")) {
|
||
d.exec("ALTER TABLE wissen ADD COLUMN hochgeladen TEXT");
|
||
console.log("[workspace] Spalte 'hochgeladen' in der Bibliothek ergaenzt.");
|
||
}
|
||
} catch (fehler) {
|
||
console.error("[workspace] Spalte 'hochgeladen':", fehler?.message);
|
||
}
|
||
|
||
/* ---- Content-Planung: aus der Liste wird eine Strecke ----------------
|
||
|
||
Die Content-Planung war eine flache Liste mit drei Arten. Die
|
||
Recherche vom 31.08.2026 ist an dieser Stelle eindeutig: *"A content
|
||
calendar for creators is a pipeline, not a datebook."* Und der
|
||
wichtigste Einzelwert eines Kurzvideos ist der Aufhaenger -- die
|
||
ersten ein bis drei Sekunden entscheiden ueber die Verbreitung. Der
|
||
gehoert in ein eigenes Feld und nicht irgendwo in den Fliesstext.
|
||
|
||
Vier neue Spalten. ALTER TABLE ADD COLUMN laesst SQLite anstandslos
|
||
zu; alte Eintraege bleiben leer und stoeren nicht. */
|
||
/* ---- Der eigene Steckbrief (01.09.2026) ------------------------------
|
||
|
||
Jede Person -- Creator, Scout, Manager, DogFather -- bekommt ein
|
||
eigenes Profil: ein Bild, ein Satz ueber sich, und die Kanaele, auf
|
||
denen sie unterwegs ist.
|
||
|
||
Warum der TikTok-Name als eigene Spalte und nicht als freier Text:
|
||
Er wird zu einem Verweis, taucht in der Personenliste auf und soll
|
||
eindeutig sein. Gespeichert wird NUR der oeffentliche Name (das
|
||
Handle ohne @) -- kein Zugriffstoken, kein Passwort. Eine echte
|
||
Anmeldung bei TikTok braucht ein Entwicklerkonto mit Freischaltung
|
||
und laufender Pruefung durch TikTok; das waere eine eigene Baustelle
|
||
mit Kosten und Abhaengigkeit. Der Verweis leistet das, was hier
|
||
gebraucht wird: Man kommt mit einem Klick auf den Kanal, und in der
|
||
Uebersicht steht, wer wo unterwegs ist.
|
||
|
||
Das Bild liegt als Datei auf der Platte, in der Datenbank steht nur
|
||
der Dateiname. Bilder in eine SQLite-Datei zu legen macht jede
|
||
Sicherung um ein Vielfaches groesser -- und die Sicherung laeuft
|
||
jede Nacht. */
|
||
for (const [tabelle, spalte, typ] of [
|
||
["eintraege", "hook", "TEXT"], // die ersten Sekunden
|
||
["eintraege", "format", "TEXT"], // Talking Head, Tutorial, Story …
|
||
["eintraege", "saeule_id", "INTEGER"],// zu welcher Content-Saeule
|
||
["eintraege", "geplant", "TEXT"], // wann es raus soll
|
||
|
||
/* ---- WAS EIN SCOUT WIRKLICH AUFSCHREIBEN MUSS (11.09.2026) ----
|
||
|
||
Filipe, zur Pipeline: "perfektionnier diese kategorien ... und was
|
||
man da noch alles immer in jeder kategorie eintragen kann. weil
|
||
man kan da nichts machen."
|
||
|
||
Ein Lead hatte bis heute Name, Plattform, Handle, Priorität,
|
||
Aktivität, Potenzial, Notizen und zwei Daten. Das genügt, um
|
||
jemanden zu MERKEN, aber nicht, um ihn zu BEURTEILEN. Die sieben
|
||
Felder hier sind genau die Fragen, die ein Scout sonst im Kopf
|
||
behält -- und die der Nächste nicht mehr findet, wenn der Scout
|
||
im Urlaub ist.
|
||
|
||
`netzwerk` ist das wichtigste davon und kein Schmuck: Wer schon
|
||
bei einem anderen Creator-Netzwerk unter Vertrag steht, kann
|
||
nicht übernommen werden. Diese Frage ganz am Anfang zu stellen
|
||
spart die ganze übrige Arbeit -- und sie hier zu haben heisst,
|
||
dass niemand zweimal fragt.
|
||
|
||
`land` steht da, weil die Betreuung in DE, LU, AT und CH läuft
|
||
und die Schweiz rechtlich anders liegt als die drei EU-Länder
|
||
(siehe die TikTok-Recherche vom 10.09.). Auch dafür ist es
|
||
besser, es beim ersten Kontakt zu wissen als beim Vertrag. */
|
||
["leads", "follower", "INTEGER"], // Reichweite beim Fund
|
||
["leads", "land", "TEXT"], // DE / AT / CH / LU / andere
|
||
["leads", "woher", "TEXT"], // wie gefunden: LIVE, Empfehlung, Kommentar …
|
||
["leads", "kontaktweg", "TEXT"], // wo angeschrieben: TikTok-DM, Instagram …
|
||
["leads", "live_zeiten", "TEXT"], // wann die Person üblicherweise live ist
|
||
["leads", "netzwerk", "TEXT"], // schon bei einer Agentur? nein/ja/unbekannt
|
||
["leads", "absage_grund", "TEXT"], // warum es nicht gepasst hat
|
||
|
||
["personen", "bild", "TEXT"], // Dateiname des Profilbildes
|
||
["personen", "ueber_mich", "TEXT"], // ein Satz zur Person
|
||
["personen", "tiktok", "TEXT"], // öffentlicher Name, ohne @
|
||
["personen", "instagram", "TEXT"],
|
||
["personen", "youtube", "TEXT"],
|
||
["personen", "twitch", "TEXT"],
|
||
/* Die eigene Kachelfarbe im Chat (20.09.2026). Ein Schluessel aus
|
||
CHAT_KACHELN, nicht die Farbe selbst: Sonst stuende in der
|
||
Datenbank ein CSS-Wert, und eine Aenderung am Farbton hiesse,
|
||
jede Zeile anzufassen. NULL heisst "Standard" und ist der
|
||
Normalfall -- ein Pflichtwert waere eine Entscheidung, die
|
||
niemand treffen wollte. */
|
||
["personen", "chat_kachel", "TEXT"],
|
||
|
||
/* ---- Wiederkehrende Termine (02.09.2026) ----
|
||
Drei Spalten an `termine`, damit eine Ausprägung weiss, woher sie
|
||
stammt:
|
||
serie_id die Regel, aus der sie entstanden ist
|
||
serie_tag der geplante Tag -- daran erkennt der Nachfüller,
|
||
dass es diese Ausprägung schon gibt, und legt sie
|
||
kein zweites Mal an
|
||
serie_beruehrt jemand hat sie von Hand angefasst (verschoben,
|
||
umbenannt, abgehakt). Solche Termine bleiben beim
|
||
Abstellen der Serie stehen -- was jemand
|
||
bearbeitet hat, wird ihm nicht weggeräumt. */
|
||
["termine", "serie_id", "INTEGER REFERENCES termin_serien(id) ON DELETE SET NULL"],
|
||
["termine", "serie_tag", "TEXT"],
|
||
["termine", "serie_beruehrt", "INTEGER NOT NULL DEFAULT 0"],
|
||
|
||
/* ---- Frei eingetragene Namen (02.09.2026) ----
|
||
Wunsch: "ich will da auch sachen selber noch eintragen können,
|
||
also aussuchen und selbst eintragen."
|
||
|
||
Bis hierher konnte an einem Eintrag nur stehen, wer auch ein Konto
|
||
im Workspace hat. Eine Agentur, eine Marke, ein Gast liess sich
|
||
nicht eintragen -- man haette dafuer ein Konto anlegen muessen,
|
||
das nie jemand benutzt.
|
||
|
||
Eine EIGENE Spalte je Feld, kein Missbrauch der id-Spalte: Ein
|
||
Fremdschluessel zeigt auf eine Person oder auf nichts. Einen
|
||
Namen dort hineinzuschreiben haette jede Verknuepfung zerstoert.
|
||
|
||
Es gilt entweder/oder: Steht eine Person drin, ist das Feld leer,
|
||
und umgekehrt. Durchgesetzt wird das beim Speichern (siehe
|
||
externPruefen), nicht durch eine CHECK-Regel -- die liesse sich
|
||
an einer bestehenden Tabelle nicht mehr nachtragen.
|
||
|
||
WICHTIG, damit nichts still verschwindet: Ueberall, wo bisher der
|
||
Name der verknuepften Person gelesen wurde, faellt die Abfrage
|
||
jetzt auf diesen Text zurueck (siehe externSql). Ein frei
|
||
eingetragener Name steht dadurch in jeder Liste, jeder Suche und
|
||
jeder Uebersicht mit da -- gekennzeichnet als "(extern)", damit
|
||
niemand ihn fuer ein Konto haelt. */
|
||
["termine", "teilnehmer_extern", "TEXT"],
|
||
["termin_serien", "teilnehmer_extern", "TEXT"],
|
||
["aufgaben", "creator_extern", "TEXT"],
|
||
["aufgaben", "verantwortlich_extern", "TEXT"],
|
||
["eintraege", "creator_extern", "TEXT"],
|
||
["dateien", "creator_extern", "TEXT"],
|
||
|
||
/* ABBRECHEN (05.09.2026). Wunsch: *"ich will dass man in jedem
|
||
status die aufgaben auch abbrechen kann."*
|
||
|
||
Drei Spalten, weil "abgebrochen" allein zu wenig sagt:
|
||
abbruch_grund WARUM. Pflichtfeld in der Oberflaeche -- ohne
|
||
Grund weiss in vier Wochen niemand mehr, warum
|
||
etwas wegfiel, und es sieht aus wie Loeschen.
|
||
abgebrochen_am WANN.
|
||
abbruch_von WER. Nur Leitung und Scouts duerfen es, und
|
||
wer es war, gehoert dazu.
|
||
status_vorher IN WELCHEM ZUSTAND. Diese Spalte ist der Grund,
|
||
warum "abgebrochen" ein eigener Status wurde
|
||
und nicht bloss ein Haken: Ohne sie ginge beim
|
||
Wechsel verloren, ob die Arbeit schon lief.
|
||
"Im Review abgebrochen" ist eine ganz andere
|
||
Aussage als "nie angefangen". */
|
||
/* WIE EINE AUFGABE VERTEILT WURDE (21.09.2026).
|
||
|
||
Filipe, Abschnitt 3: *"Dogfather und die rechte Hand muessen
|
||
eine Aufgabe einer einzelnen Person zuweisen koennen. Sie
|
||
muessen eine Aufgabe auch mehreren bestimmten Personen
|
||
gleichzeitig zuweisen koennen. Zusaetzlich soll es eine Option
|
||
geben, bei der eine Aufgabe fuer mehrere Modis sichtbar ist und
|
||
die erste Modi, die gerade Zeit hat, die Aufgabe uebernehmen
|
||
kann."*
|
||
|
||
DREI WERTE, UND DER UNTERSCHIED ZWISCHEN DEN LETZTEN BEIDEN IST
|
||
DER GANZE PUNKT:
|
||
|
||
einzeln eine Person, sie macht es
|
||
mehrere mehrere Personen, JEDE macht ihren Teil
|
||
pool mehrere sehen es, EINE nimmt es -- danach ist es
|
||
fuer die anderen erledigt
|
||
|
||
Ohne diese Spalte waeren "mehrere" und "pool" in der Datenbank
|
||
nicht zu unterscheiden: Beide sind n Zeilen in
|
||
aufgaben_zuteilung. Die Oberflaeche muesste dann raten, ob eine
|
||
uebernommene Aufgabe fuer die anderen noch offen ist -- und
|
||
genau daraus entsteht die doppelte Bearbeitung, die der Auftrag
|
||
ausdruecklich verhindern will.
|
||
|
||
KEIN CHECK an dieser Spalte, mit Absicht: Sie wird nachgetragen,
|
||
und ein CHECK auf einer nachgetragenen Spalte hiesse die Tabelle
|
||
neu zu bauen -- der Weg, der in diesem Haus am 11.09. und am
|
||
21.09. schon Spalten gekostet hat. Geprueft wird beim
|
||
Schreiben, in der Route. */
|
||
["aufgaben", "verteilart", "TEXT"],
|
||
|
||
["aufgaben", "abbruch_grund", "TEXT"],
|
||
["aufgaben", "abgebrochen_am", "TEXT"],
|
||
["aufgaben", "abbruch_von", "INTEGER REFERENCES personen(id) ON DELETE SET NULL"],
|
||
["aufgaben", "status_vorher", "TEXT"],
|
||
|
||
/* AGENTUR-EVENTS (07.09.2026).
|
||
|
||
Ein Event ist kein Notizzettel mit Ueberschrift und Text. Wer
|
||
mitmachen soll, muss vier Dinge finden, ohne zu fragen: bis wann
|
||
es laeuft, was zu tun ist, was es zu gewinnen gibt und was
|
||
verboten ist. Genau daran scheitern Aktionen -- nicht am Aufruf,
|
||
sondern an den Rueckfragen danach.
|
||
|
||
Der BEGINN ist `datum` (gibt es schon, ist NOT NULL). Neu ist
|
||
das Ende und die beiden Textfelder. `event_`-Vorsatz, weil es im
|
||
Workspace bereits eine Tabelle `aufgaben` gibt -- eine Spalte
|
||
`aufgaben` auf `eintraege` waere beim Lesen von Code jedes Mal
|
||
eine Verwechslung.
|
||
|
||
ADD COLUMN und kein Tabellenneubau: Es aendert sich kein CHECK,
|
||
also darf die Tabelle stehen bleiben. Der Neubau vom 06.09. war
|
||
noetig, weil dort der erlaubte Wertebereich von `bereich` wuchs
|
||
-- und er ist der Weg, bei dem man Inhalte verlieren kann. */
|
||
["eintraege", "event_ende", "TEXT"],
|
||
["eintraege", "event_aufgaben", "TEXT"],
|
||
["eintraege", "event_regeln", "TEXT"],
|
||
|
||
/* DER TAGESRUF BRAUCHT MEHR ALS AN/AUS (08.09.2026, screen10).
|
||
|
||
Filipe: "ich will das neben diesem kreis auch ein kleiner button
|
||
ist fuer den wecker von den aufgaben, oder quasi eine taetige
|
||
meldung einmal am tag zu aktivieren wenn noch aufgaben auf sind."
|
||
|
||
Eine Uhrzeit ist kein Schalter. Statt einer eigenen Tabelle fuer
|
||
eine einzige Zahl bekommt `push_einstellungen` eine allgemeine
|
||
Spalte `wert`: Wo eine Art mehr braucht als an/aus, steht es
|
||
dort. Die vorhandenen Arten lassen sie leer und merken nichts
|
||
davon.
|
||
|
||
KEIN `NOT NULL`, KEINE VORGABE IN DER DATENBANK: Die Vorgabe
|
||
gehoert nach workspace-push.js zu den Arten, sonst stuende sie an
|
||
zwei Stellen -- und die in der Datenbank wuerde bei einer
|
||
Aenderung vergessen. Genau diese Sorte Doppelung hat mich heute
|
||
schon dreimal aufgehalten. */
|
||
["push_einstellungen", "wert", "TEXT"],
|
||
|
||
/* ANTWORTEN MIT ZITAT (09.09.2026, Punkt 15).
|
||
|
||
Filipe: "ich will dass du diese seite viel krasser und
|
||
detaillierter machst ... alles reinsetzt was wir noch gebrauchen
|
||
koennten."
|
||
|
||
In einem Gespraech zu zweit weiss man meistens, worauf sich
|
||
etwas bezieht. In einer Gruppe nicht -- dort laufen drei Faeden
|
||
parallel, und "ja, mach das" kann alles heissen. Die Spalte
|
||
zeigt auf die Nachricht, auf die geantwortet wird.
|
||
|
||
KEIN FREMDSCHLUESSEL AUF chat_nachrichten: Wird die zitierte
|
||
Nachricht zurueckgenommen, bleibt ihre Zeile ohnehin stehen
|
||
(nur der Text wird geleert) -- und wird der ganze Raum
|
||
geloescht, geht die Antwort per CASCADE ueber `raum_id`
|
||
ohnehin mit. Ein zweiter Schluessel brauchte nur eine zweite
|
||
Gelegenheit fuer eine Sperre. */
|
||
["chat_nachrichten", "antwort_auf", "INTEGER"],
|
||
|
||
/* WURDE HIER DAS GANZE RUDEL GERUFEN? (23.09.2026)
|
||
|
||
Filipe: „dan will ich auch dass nur die modis, rechte hand,
|
||
linke hand und dogfather, alle auch auf einmal markieren koennen
|
||
im chat mit einem, @rudel ,dann sollen alle eine benarichtigung
|
||
bekommen."
|
||
|
||
WARUM DAS GESPEICHERT WIRD UND NICHT BEIM LESEN NACHGERECHNET:
|
||
Beim Lesen muesste der Browser wissen, ob der ABSENDER es damals
|
||
durfte. Er weiss nur dessen heutige Rolle. Wechselt jemand die
|
||
Rolle, aenderte sich rueckwirkend, ob ein alter Satz das Rudel
|
||
gerufen hat -- die Hervorhebung verschwaende aus einer Nachricht
|
||
von vorletzter Woche, in der sie damals richtig war.
|
||
|
||
Und es waere die zweite Rechnung ueber dieselbe Frage: Der
|
||
Server entscheidet beim Schreiben, wer gerufen wird. Rechnete
|
||
der Browser beim Lesen noch einmal, haetten wir genau die Lage,
|
||
vor der chat-erwaehnung.js im ersten Absatz warnt -- die Seite
|
||
hebt jemanden hervor, den niemand benachrichtigt hat.
|
||
|
||
VORGABE 0. Alle Nachrichten von vor heute haben kein Rudel
|
||
gerufen, und das ist keine Annahme, sondern eine Tatsache: Es
|
||
gab das Wort noch nicht. */
|
||
["chat_nachrichten", "rudel", "INTEGER NOT NULL DEFAULT 0"],
|
||
|
||
/* GESPRAECH LOESCHEN -- NUR BEI MIR (09.09.2026).
|
||
|
||
Filipe: "man muss die chats auch geloescht bekommen!!!"
|
||
Auf Nachfrage entschieden: nur bei mir, nicht bei beiden. Das
|
||
Gespraech verschwindet aus MEINER Liste, das Gegenueber behaelt
|
||
seinen Verlauf. Sonst koennte jeder jedem die Unterhaltung
|
||
vernichten -- und in einem Gespraech, in dem Absprachen stehen,
|
||
waere das ein Loch, das niemand mehr fuellen kann.
|
||
|
||
ZWEI SPALTEN, NICHT EINE, und der Grund ist ein Randfall:
|
||
`geloescht_bis` haelt fest, bis zu welcher Nachricht ich es nicht
|
||
mehr sehen will. Bei einem Gespraech OHNE jede Nachricht waere
|
||
das 0 -- und 0 heisst auch "nie geloescht". `geloescht_am`
|
||
unterscheidet die beiden Faelle. Kommt spaeter etwas Neues
|
||
(id > geloescht_bis), taucht das Gespraech von selbst wieder auf;
|
||
das Alte bleibt weg. */
|
||
["chat_teilnehmer", "geloescht_bis", "INTEGER NOT NULL DEFAULT 0"],
|
||
["chat_teilnehmer", "geloescht_am", "TEXT"],
|
||
|
||
/* ANHAENGE: FOTOS UND PDF (09.09.2026).
|
||
|
||
Filipe: "am besten waere es auch wenn man da auch im chat pdfs
|
||
schicken koennte. pdfs und fotos."
|
||
|
||
Je Nachricht hoechstens ein Anhang -- deshalb Spalten und keine
|
||
eigene Tabelle. Eine Tabelle waere die richtige Wahl fuer
|
||
"mehrere je Nachricht"; die gibt es hier nicht, und eine leere
|
||
Verbundabfrage bei jedem Verlauf waere Aufwand fuer einen Fall,
|
||
den es nicht gibt.
|
||
|
||
`anhang_datei` ist der ERZEUGTE Name auf der Platte, niemals der
|
||
eingeschickte. `anhang_art` ist "bild" oder "pdf" und wird aus
|
||
den ersten Bytes der Datei bestimmt, nicht aus dem Namen und
|
||
nicht aus dem Content-Type -- beide kommen vom Absender.
|
||
Breite und Hoehe stehen dabei, damit der Verlauf beim Laden
|
||
eines Bildes nicht springt. */
|
||
["chat_nachrichten", "anhang_datei", "TEXT"],
|
||
["chat_nachrichten", "anhang_name", "TEXT"],
|
||
["chat_nachrichten", "anhang_art", "TEXT"],
|
||
["chat_nachrichten", "anhang_typ", "TEXT"],
|
||
["chat_nachrichten", "anhang_groesse", "INTEGER"],
|
||
["chat_nachrichten", "anhang_breite", "INTEGER"],
|
||
["chat_nachrichten", "anhang_hoehe", "INTEGER"],
|
||
|
||
/* WIE LANG IST DIE SPRACHNACHRICHT? (23.09.2026)
|
||
|
||
Filipe: „dan will ich auch dass man im chat auch sprachnachrichten
|
||
und gifts reinschicken kann."
|
||
|
||
In Millisekunden, und sie ist AUSDRUECKLICH nur fuer die Anzeige,
|
||
bevor die Datei geladen ist: In der Gespraechsliste steht
|
||
„Sprachnachricht (0:14)", und in der Blase steht die Laenge schon
|
||
da, waehrend der Ton noch laedt. Ohne sie springt die Zeile in
|
||
dem Moment, in dem die Zahl nachkommt.
|
||
|
||
SIE KOMMT VOM ABSENDER und ist damit eine Behauptung, keine
|
||
Messung -- das gehoert gesagt. Sie zu ueberpruefen hiesse, vier
|
||
Behaelterformate zu zerlegen (WebM schreibt die Dauer beim
|
||
Aufnehmen oft gar nicht erst hinein). Der Wert wird deshalb
|
||
begrenzt, und die WAHRE Laenge sagt ohnehin das Abspielgeraet,
|
||
sobald es die Datei kennt. Was wirklich schuetzt, ist die
|
||
Byte-Grenze, nicht diese Zahl. */
|
||
["chat_nachrichten", "anhang_dauer", "INTEGER"],
|
||
|
||
/* WELCHE VORLAGE DAHINTERSTECKT (09.09.2026).
|
||
|
||
Filipe: "ich will nur dass du dich informierst und schaust was
|
||
man vielleicht noch besser machen koennte von der benutzung ...
|
||
ich red von den aufgaben."
|
||
|
||
Beim Durchgehen der Bedienung ist aufgefallen: Man konnte
|
||
dieselbe Vorlage zweimal uebernehmen und bekam die Aufgabe
|
||
zweimal auf das Brett -- ohne jeden Hinweis. Im Vorlagenbrett
|
||
sah man nicht, was man schon geholt hatte.
|
||
|
||
Die Kennung der Vorlage steht jetzt an der Aufgabe. Damit kann
|
||
das Brett zeigen, was bereits uebernommen ist, und es braucht
|
||
dafuer KEINE zweite Abfrage: Die Aufgabenliste hat der Browser
|
||
ohnehin schon. Ueber den TITEL zu vergleichen waere die
|
||
naheliegende Abkuerzung gewesen -- und faellt in dem Moment um,
|
||
in dem jemand den Titel einer uebernommenen Aufgabe aendert. */
|
||
["aufgaben", "vorlage", "TEXT"],
|
||
|
||
/* DER SUCHSCHLUESSEL ZUM CODE (09.09.2026).
|
||
|
||
Bisher gab es nur den scrypt-Hash, und der ist mit Absicht
|
||
langsam (rund 150 ms). Wer eine Person AM CODE finden will, muss
|
||
deshalb alle Kandidaten der Reihe nach durchrechnen -- die
|
||
Anmeldung tut genau das, und im Code steht seit langem der
|
||
Hinweis, dass das ab etwa 50 Codes je Rolle spuerbar wird.
|
||
|
||
Diese Spalte ist die schnelle Antwort auf "zu welcher Person
|
||
gehoert dieser Code?": ein HMAC ueber den Code, in Mikrosekunden
|
||
berechnet und mit einem Index hinterlegt.
|
||
|
||
WARUM DAS UNBEDENKLICH IST -- nachgemessen, nicht angenommen: Ein
|
||
Code besteht aus 16 Zeichen eines 32er-Alphabets, das sind 80 Bit
|
||
Zufall. Selbst wenn jemand die ganze Datenbank haette, waere
|
||
Durchprobieren aussichtslos, egal wie schnell die Rechnung ist.
|
||
Der Schluessel steckt zusaetzlich nicht im Code, sondern in den
|
||
Einstellungen (siehe kennungSchluessel).
|
||
|
||
DER SCRYPT-HASH BLEIBT DIE ENTSCHEIDUNG. Der Suchschluessel sagt
|
||
nur, WEN man pruefen soll; ob der Code stimmt, sagt weiterhin
|
||
allein scrypt. Ein Treffer hier allein laesst niemanden herein. */
|
||
["personen", "code_kennung", "TEXT"],
|
||
|
||
/* DIE STUFE IM TEAM (10.09.2026).
|
||
|
||
Blueprint V3.0, Kapitel 4.1: Probe, Standard, Senior. Ein
|
||
Probe-Modi ist in der Einarbeitung, ein Senior kann die rechte
|
||
Hand bei Abwesenheit vertreten.
|
||
|
||
WARUM OHNE CHECK-REGEL, anders als bei `rolle`: Eine CHECK-Liste
|
||
laesst sich in SQLite nur ueber einen Tabellenneubau erweitern
|
||
(siehe checkListeErweitern) -- und die Stufen sind eine
|
||
ARBEITSEINTEILUNG, keine Rechtegrenze. Kaeme morgen eine vierte
|
||
dazu, waere ein Tabellenneubau auf einer Live-Datenbank ein
|
||
hoher Preis fuer eine Beschriftung. Geprueft wird deshalb im
|
||
Code, an genau einer Stelle (STUFEN), und was dort nicht
|
||
draufsteht, wird abgewiesen.
|
||
|
||
NULL HEISST "Probe" und nicht "unbekannt". Ein dritter Zustand
|
||
waere eine Frage, die niemand beantworten kann ("was ist er
|
||
denn nun?"); und ein neuer Mensch faengt ohnehin in der
|
||
Einarbeitung an. Beim Lesen wird NULL deshalb auf 'probe'
|
||
abgebildet -- an einer Stelle, nicht in jeder Abfrage.
|
||
|
||
Die Spalte heisst wie `punkt_stand.stufe`, meint aber etwas
|
||
anderes (dort: gut/verbessern). Zwei Tabellen, kein Konflikt --
|
||
aber wer beides im selben Kopf hat, sollte es wissen. */
|
||
["personen", "stufe", "TEXT"],
|
||
|
||
/* DIE KATEGORIE EINER AUFGABE (10.09.2026).
|
||
|
||
Kapitel 6.1 des Anforderungsdokuments verlangt sie an jeder
|
||
Aufgabe. Filipes Entscheidung dazu: nur bei den Modis -- fuer
|
||
Creator, Scouts und Manager bleibt das Formular, wie es war.
|
||
|
||
KEIN CHECK IN DER SPALTE, obwohl die Werte fest sind. Ein CHECK
|
||
laesst sich in SQLite nicht aendern; die Liste um eine Kategorie
|
||
zu erweitern hiesse jedes Mal, die ganze Tabelle neu zu bauen.
|
||
Geprueft wird stattdessen beim Schreiben (siehe KATEGORIEN in
|
||
workspace-aufgaben.js) -- an EINER Stelle, durch die alle
|
||
Schreibwege laufen. */
|
||
["aufgaben", "kategorie", "TEXT"],
|
||
|
||
/* WAS DOGFATHER DAZU TUN MUESSTE (10.09.2026).
|
||
|
||
Filipe: *"auch entscheiden was ich machen muss wenn das und das
|
||
erledigt wird."* Der Modi schreibt es beim Angebot dazu; wird
|
||
das Angebot angenommen, wird genau daraus seine Aufgabe.
|
||
|
||
Ein eigenes Feld und nicht im Fliesstext: Aus einem Absatz laesst
|
||
sich keine Aufgabe machen, ohne zu raten, welcher Satz gemeint
|
||
war. */
|
||
["eintraege", "einsatz", "TEXT"],
|
||
|
||
/* KATEGORIE-KANAELE (10.09.2026, Kapitel 7.2).
|
||
|
||
"Kategorie-Kanaele: z. B. Clipping-Team, Live-Moderation --
|
||
Owner/rechte Hand haben Zugriff auf alle Kanaele."
|
||
|
||
Ein Kanal ist KEINE vierte Tabelle, sondern eine dritte Art Raum.
|
||
Damit gilt fuer ihn ohne eine einzige neue Zeile alles, was schon
|
||
da ist: Verlauf, Anhaenge, Suche, Ungelesen-Zaehler, der
|
||
Live-Strom, das Wegraeumen. Eine eigene Tabelle haette all das
|
||
ein zweites Mal gebraucht -- und die zweite Fassung waere die
|
||
gewesen, in der die Zugriffsregel fehlt.
|
||
|
||
WELCHE Zustaendigkeit, steht in kategorie und kommt aus
|
||
MODI_KATEGORIEN -- derselben Liste, aus der auch die Aufgaben
|
||
ihre Kategorie nehmen. Ein Kanal "Clipping" und Aufgaben
|
||
"clipping" waeren sonst zwei Woerter fuer dieselbe Sache. */
|
||
["chat_raeume", "kategorie", "TEXT"],
|
||
|
||
/* WER DARF DIESE RUECKMELDUNG LESEN? (10.09.2026)
|
||
|
||
0 = das ganze Team, 1 = nur DogFather.
|
||
|
||
WARUM ES DIESE WAHL UEBERHAUPT GIBT: Filipe will beides -- eine
|
||
offene Teamkultur UND ehrliche Rueckmeldung an sich selbst. Das
|
||
ist kein Widerspruch, aber es sind zwei verschiedene Raeume. Wer
|
||
"was koenntest du besser machen" vor versammelter Mannschaft
|
||
sagen muss, sagt es nicht; wer alles nur unter vier Augen sagen
|
||
kann, hat kein Team-Gespraech. Also entscheidet der Schreibende,
|
||
Zeile fuer Zeile.
|
||
|
||
UND "NUR DOGFATHER" HEISST NUR DOGFATHER -- die rechte Hand
|
||
ausdruecklich nicht. Sie sieht sonst ueberall dasselbe wie er;
|
||
hier nicht, und zwar weil das Etikett sonst nicht stimmen wuerde.
|
||
Eine Zusage, die im Kleingedruckten eine Ausnahme hat, ist keine.
|
||
Sie kann selbst genauso schreiben -- auch ueber ihn. */
|
||
["eintraege", "nur_leitung", "INTEGER"],
|
||
|
||
/* WANN WURDE ES DER PERSON GESAGT? (11.09.2026)
|
||
|
||
Der Entwicklungs-Bereich ist eine Aufzeichnung fuer DogFather und
|
||
die rechte Hand -- die Person selbst sieht ihn nicht. Das ist
|
||
Absicht: Eine halb sichtbare Akte ist schlimmer als eine
|
||
geschlossene, weil niemand mehr weiss, was der andere gerade
|
||
liest.
|
||
|
||
Damit Anerkennung trotzdem ANKOMMT (die Forschung dazu ist
|
||
eindeutig: Dank und Rueckmeldung sind der staerkste Grund, warum
|
||
Freiwillige bleiben), kann jeder Eintrag als Nachricht an die
|
||
Person geschickt werden. Hier steht, WANN das geschehen ist.
|
||
|
||
Ohne diese Spalte gaebe es die Frage "habe ich ihr das eigentlich
|
||
schon gesagt?" -- und die beantwortet man nach zwei Wochen
|
||
falsch. */
|
||
["eintraege", "gesendet_am", "TEXT"],
|
||
|
||
/* ANGEHEFTETE NACHRICHTEN (10.09.2026, Kapitel 7.2).
|
||
|
||
"Ankuendigungs-Funktion: Pin-Nachrichten von Owner/rechte Hand
|
||
an alle."
|
||
|
||
Am Anheften haengt ein ZWEITER Name (angeheftet_von) und nicht
|
||
nur ein Datum. Eine Ankuendigung ohne Absender ist ein Aushang
|
||
ohne Unterschrift -- und wer sie geloest hat, ist genau die
|
||
Frage, die spaeter gestellt wird.
|
||
|
||
KEIN FREMDSCHLUESSEL AUF personen: Wird jemand geloescht, soll
|
||
die Ankuendigung nicht mitverschwinden. Dieselbe Ueberlegung wie
|
||
bei antwort_auf weiter oben. */
|
||
["chat_nachrichten", "angeheftet_am", "TEXT"],
|
||
["chat_nachrichten", "angeheftet_von", "INTEGER"],
|
||
|
||
/* DAS BESTAETIGTE ALTER (15.09.2026, Stufe 8).
|
||
|
||
Auf der Regelseite des Treffs steht seit dem 11.09. "ab 18" --
|
||
und geprueft wurde es nirgends. Eine Regel, die nur dasteht, ist
|
||
keine; im Streitfall ist sie sogar schlechter als keine, weil man
|
||
sie versprochen hat.
|
||
|
||
NUR EIN ZEITSTEMPEL, KEIN GEBURTSDATUM. Gebraucht wird die
|
||
Antwort auf "hat bestaetigt, alt genug zu sein, und wann" --
|
||
nicht der Geburtstag. Ein Geburtsdatum waere mehr Daten fuer
|
||
dieselbe Auskunft, und Datenminimierung (Art. 5 Abs. 1 lit. c)
|
||
gilt auch fuer das eigene Nachweisbeduerfnis.
|
||
|
||
WARUM KEINE SPALTE FUER DIE RECHTSGRUNDLAGE: Sie folgt aus der
|
||
Rolle -- ein Modi arbeitet hier (Vertrag), ein Community-Mitglied
|
||
ist freiwillig da (Einwilligung). Was sich ableiten laesst, wird
|
||
nicht gespeichert; sonst haette man zwei Wahrheiten, von denen
|
||
eine veraltet. Siehe grundlageVon() in workspace-aufbewahrung.js. */
|
||
["personen", "alter_bestaetigt_am", "TEXT"],
|
||
|
||
/* WOHER EINE AUFGABE KAM (15.09.2026, Stufe 9: "Idee -> Aufgabe").
|
||
|
||
MIT deklariertem Fremdschluessel, und das ist hier nicht
|
||
Geschmack: Seit dem 15.09. prueft pruef-auskunft, dass JEDE
|
||
Spalte auf _id entweder einen Fremdschluessel hat oder
|
||
namentlich eingeordnet ist. Eine undeklarierte Spalte waere dort
|
||
sofort rot -- und genau so soll es sein, denn niemand kann einer
|
||
Zahl ansehen, worauf sie zeigt.
|
||
|
||
ON DELETE SET NULL und nicht CASCADE: Wird die Idee spaeter
|
||
geloescht, bleibt die Arbeit. Sie ist inzwischen etwas Eigenes;
|
||
sie mit der Notiz zu entfernen, aus der sie entstand, waere die
|
||
teuerste Art, Ordnung zu machen. */
|
||
["aufgaben", "aus_eintrag_id", "INTEGER REFERENCES eintraege(id) ON DELETE SET NULL"],
|
||
|
||
/* FASSUNGEN STATT UNVERBUNDENER DOPPEL (15.09.2026, Stufe 9).
|
||
|
||
GEMESSEN, BEVOR ETWAS GEBAUT WURDE: Der Plan sagt "Dateiversionen
|
||
statt Ueberschreiben" -- ueberschrieben wurde aber nie.
|
||
„name_datei“ ist eindeutig und zufaellig, jedes Hochladen legt
|
||
eine neue Zeile an. Der Mangel ist ein anderer, und er ist
|
||
gefaehrlicher als Ueberschreiben:
|
||
|
||
Zwei Dateien gleichen Namens stehen BEZIEHUNGSLOS nebeneinander.
|
||
Die alte kann "freigegeben" sein, die neue "entwurf" -- und wer
|
||
die Liste ansieht, laedt die freigegebene herunter. Also die
|
||
falsche. Kein Fehler, keine Meldung, nur die alte Fassung in der
|
||
Hand.
|
||
|
||
ON DELETE SET NULL: Wird die alte Fassung geloescht, bleibt die
|
||
neue -- sie verliert nur ihre Vorgeschichte. */
|
||
["dateien", "ersetzt_id", "INTEGER REFERENCES dateien(id) ON DELETE SET NULL"],
|
||
|
||
/* ANHAENGE AN AUFGABEN (15.09.2026, Stufe 9).
|
||
|
||
KEIN ZWEITER HOCHLADEWEG. Dateien leben in der Dateiablage -- mit
|
||
ihrer Sichtbarkeit, ihren Freigaben, ihren Fassungen und ihrem
|
||
Protokoll. Ein eigener Weg an der Aufgabe waere eine zweite
|
||
Ablage mit einer zweiten Rechtelogik, und die zweite ist immer
|
||
die, die etwas durchlaesst.
|
||
|
||
Deshalb nur eine Spalte: Die Datei WEISS, zu welcher Aufgabe sie
|
||
gehoert. Alles andere bleibt, wo es ist.
|
||
|
||
ON DELETE SET NULL, NICHT CASCADE -- und das ist der wichtigste
|
||
Teil dieser Zeile: Wer eine Aufgabe loescht, will die Aufgabe
|
||
loeschen, nicht den Vertrag, der daran hing. Die Datei verliert
|
||
nur ihren Bezug und steht danach wieder in der Ablage. */
|
||
["dateien", "aufgabe_id", "INTEGER REFERENCES aufgaben(id) ON DELETE SET NULL"],
|
||
|
||
/* DER AUFWAND EINER AUFGABE (15.09.2026, Stufe 9).
|
||
|
||
"Wichtig" gibt es schon -- das ist die Prioritaet. Was fehlte, ist
|
||
die zweite Angabe, ohne die man nicht entscheiden kann: Schaffe
|
||
ich das zwischendurch, oder muss ich mir Zeit nehmen?
|
||
|
||
OHNE CHECK-LISTE in der Datenbank, und das ist Absicht: Die
|
||
erlaubten Werte stehen in workspace-womit.js, und eine CHECK-Liste
|
||
daneben waere eine zweite Wahrheit, die beim naechsten Wert
|
||
nachgezogen werden muesste -- ueber einen Tabellenneubau, an dem
|
||
im Projekt schon dreimal Spalten verlorengegangen sind. Geprueft
|
||
wird beim Schreiben, an der Stelle, die die Liste kennt. */
|
||
["aufgaben", "aufwand", "TEXT"],
|
||
|
||
/* 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);
|
||
}
|
||
}
|
||
|
||
loeschspurenAufraeumen(d);
|
||
|
||
/* =====================================================================
|
||
DIE BLAUEN HERZEN WERDEN BABYBLAU (22.09.2026)
|
||
=====================================================================
|
||
|
||
Filipe: "jedes normales blaues herz durch babyblaues herz
|
||
ersetzen ... jeder normale blaue farbe, sei es die kachel im chat
|
||
oder emojis, nur babyblau bitte."
|
||
|
||
Der Katalog (workspace-reaktionen.js) kennt seit heute nur noch
|
||
das babyblaue. WAS SCHON IN DER DATENBANK STEHT, WANDERT MIT --
|
||
ohne diese Zeilen waeren es vier Reaktionen, die `ERLAUBT` nicht
|
||
mehr kennt. Der Verlauf filtert danach: Sie verschwaenden still,
|
||
und niemand koennte sagen, wann und warum.
|
||
|
||
`UPDATE OR IGNORE`, DANN `DELETE`: Der Primaerschluessel ist
|
||
(nachricht_id, person_id, zeichen). Hat jemand auf dieselbe
|
||
Nachricht BEIDES gesetzt, liefe ein blankes UPDATE auf den
|
||
Schluessel auf. So gewinnt das vorhandene babyblaue, und das
|
||
blaue faellt weg -- zweimal dasselbe Herz an einer Nachricht
|
||
waere ohnehin keine zwei Reaktionen.
|
||
|
||
WIEDERHOLBAR: Beim zweiten Start gibt es nichts mehr zu tun. */
|
||
try {
|
||
const blau = "\u{1F499}", babyblau = "\u{1FA75}";
|
||
const offen = d.prepare(
|
||
"SELECT COUNT(*) AS n FROM chat_reaktionen WHERE zeichen = ?").get(blau).n;
|
||
if (offen > 0) {
|
||
d.prepare("UPDATE OR IGNORE chat_reaktionen SET zeichen = ? WHERE zeichen = ?")
|
||
.run(babyblau, blau);
|
||
const rest = d.prepare("DELETE FROM chat_reaktionen WHERE zeichen = ?").run(blau).changes;
|
||
console.log(`[workspace] ${offen} blaue Herzen auf babyblau umgestellt`
|
||
+ (rest ? ` (${rest} doppelte entfernt)` : ""));
|
||
}
|
||
} catch (fehler) {
|
||
console.error("[workspace] Herzen umstellen:", fehler?.message);
|
||
}
|
||
|
||
/* Content-Saeulen: die drei bis fuenf Themen, aus denen der Kanal
|
||
besteht. Aus der Recherche: 3-5 Saeulen nach der 70/20/10-Regel
|
||
(70 % Wert, 20 % Community, 10 % Eigenwerbung); eine Saeule wird
|
||
erst brauchbar, wenn Formate an ihr haengen, und einmal im Monat
|
||
schaut man nach, welche ihr Gewicht traegt.
|
||
|
||
Je Creator eigene Saeulen -- ein Gaming-Kanal und ein Bastelkanal
|
||
haben nichts gemeinsam, eine gemeinsame Liste waere fuer beide
|
||
falsch. */
|
||
try {
|
||
d.exec(`
|
||
CREATE TABLE IF NOT EXISTS content_saeulen (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
creator_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
name TEXT NOT NULL,
|
||
ziel INTEGER, -- gewuenschter Anteil in Prozent
|
||
slot INTEGER NOT NULL DEFAULT 1, -- Farbplatz 1..5, siehe content.css
|
||
erstellt TEXT NOT NULL
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_saeulen_creator ON content_saeulen (creator_id);
|
||
`);
|
||
} catch (fehler) {
|
||
console.error("[workspace] content_saeulen:", fehler?.message);
|
||
}
|
||
|
||
/* Die alte Art "produktion" wird zu "gedreht". Vorher gab es drei
|
||
Stufen (Idee, Produktion, Veroeffentlicht) -- dazwischen lag alles,
|
||
was in Wahrheit den Unterschied macht: Aufhaenger geschrieben,
|
||
abgedreht, geschnitten, terminiert. Ohne diese Umbenennung fielen
|
||
bestehende Eintraege aus jeder Spalte heraus und waeren unsichtbar. */
|
||
try {
|
||
const betroffen = d.prepare(
|
||
"SELECT COUNT(*) AS n FROM eintraege WHERE bereich = 'content' AND art = 'produktion'").get()?.n ?? 0;
|
||
if (betroffen) {
|
||
d.prepare("UPDATE eintraege SET art = 'gedreht' WHERE bereich = 'content' AND art = 'produktion'").run();
|
||
console.log(`[workspace] ${betroffen} Content-Eintraege von 'produktion' auf 'gedreht' umgestellt.`);
|
||
}
|
||
} catch (fehler) {
|
||
console.error("[workspace] Content-Arten:", fehler?.message);
|
||
}
|
||
|
||
/* ---- Status "abgebrochen" erlauben (05.09.2026) ----
|
||
|
||
Der CHECK-Constraint einer Tabelle laesst sich in SQLite nicht
|
||
aendern -- kein ALTER TABLE der Welt hilft, die Tabelle muss neu
|
||
gebaut werden. Deshalb derselbe Weg wie bei der Rolle "manager"
|
||
darunter: erst sichern, dann tauschen, danach die Verweise pruefen.
|
||
|
||
WARUM UEBERHAUPT EIN NEUER STATUS und nicht bloss ein Haken
|
||
"abgebrochen": Weil die vier Spalten des Bretts nach Status
|
||
gruppieren. Eine abgebrochene Aufgabe faellt damit von selbst aus
|
||
allen vieren heraus -- ohne dass irgendeine Abfrage angefasst
|
||
werden muss. Ein zusaetzlicher Haken haette bedeutet, JEDE Abfrage
|
||
um "AND nicht abgebrochen" zu ergaenzen, und die eine vergessene
|
||
waere ein stiller Fehler gewesen: Die Aufgabe stuende weiter im
|
||
Brett, und niemand wuesste warum. */
|
||
/* ---- BIS ZUM 21.09.2026 STAND HIER EIN NEUBAU VON HAND ----
|
||
|
||
Er zaehlte die Spalten einzeln auf -- einmal im CREATE, einmal im
|
||
INSERT ... SELECT. Achtzehn Stueck. Die Tabelle hatte zu dem
|
||
Zeitpunkt DREIUNDZWANZIG: `vorlage`, `kategorie`, `aus_eintrag_id`,
|
||
`aufwand` und `aus_punkt` kamen spaeter dazu und wurden nie
|
||
nachgetragen.
|
||
|
||
WAS DAS BEDEUTET HAETTE: Wer die Umstellung noch vor sich hat --
|
||
eine zurueckgespielte Sicherung, eine frische Anlage aus einem
|
||
alten Stand -- verliert diese fuenf Spalten MIT INHALT. Ohne
|
||
Fehlermeldung, bei unveraenderter Zeilenzahl. Die Zaehlung, die
|
||
als Sicherung gedacht war, kann einen Spaltenverlust gar nicht
|
||
sehen: 40 Zeilen vorher, 40 Zeilen nachher, alles "in Ordnung".
|
||
|
||
GENAU DAS IST IN DIESEM HAUS SCHON EINMAL PASSIERT (11.09.2026,
|
||
Eintragstabelle: 24 Spalten hinein, 21 heraus). Damals wurde die
|
||
Lehre gezogen, die Liste abzuleiten statt zu pflegen -- und
|
||
`checkListeErweitern` unten tut genau das (`PRAGMA table_info`).
|
||
Dieser Block hier war aelter und wurde bei der Umstellung
|
||
uebersehen; eine Liste, die niemand pflegt, kann nicht veralten,
|
||
aber eine zweite Fassung daneben eben doch.
|
||
|
||
Gefunden hat es nicht das Lesen, sondern pruef-abbrechen: Sie baut
|
||
absichtlich eine ALTE Datenbank und laesst die Umstellung darauf
|
||
laufen. Danach meldete der Server
|
||
"Aufgaben lesen: no such column: a.aufwand" -- und die Pruefung
|
||
bekam eine 503 statt einer Liste.
|
||
|
||
Auf dem echten Server ist die Umstellung laengst gelaufen (24
|
||
Spalten, CHECK mit 'abgebrochen'); dort ist nichts verloren
|
||
gegangen. Der Aufruf unten ist deshalb im Betrieb ein Nulldurchgang
|
||
-- er steht hier fuer den Tag, an dem jemand eine Sicherung
|
||
zurueckspielt. */
|
||
checkListeErweitern(d, "aufgaben", "status", "abgebrochen",
|
||
["offen", "arbeit", "review", "erledigt", "abgebrochen"], jetztStempel);
|
||
|
||
/* DER ZUSTAND „beworben" (22.09.2026).
|
||
|
||
Eine Bewerbung ist keine neue Tabelle, sondern ein weiterer
|
||
Zustand derselben Zeile. Das ist nicht Sparsamkeit, sondern die
|
||
richtige Form: Die Frage „wie steht diese Aufgabe bei DIESEM
|
||
Menschen" wird schon hier beantwortet, und eine Bewerbung ist
|
||
genau eine Antwort darauf. Eine zweite Tabelle daneben haette
|
||
zwei Wahrheiten ueber dieselbe Beziehung -- und jede Liste,
|
||
jede Ansicht und jede Rueckmeldung muesste beide lesen.
|
||
|
||
Der Ablauf: beworben -> angenommen (die rechte Hand oder
|
||
DogFather sagt ja) oder -> abgelehnt (mit Kommentar).
|
||
|
||
`checkListeErweitern` baut die Tabelle dafuer neu -- der einzige
|
||
Weg, eine CHECK-Regel in SQLite zu aendern. Die Spaltenliste
|
||
kommt aus PRAGMA, die Indizes gehen mit, und vorher wird
|
||
gesichert. Steht seit dem 09.09. an einer Stelle, genau damit
|
||
diese Umstellung hier keine vierte Abschrift wird. */
|
||
checkListeErweitern(d, "aufgaben_zuteilung", "zustand", "beworben",
|
||
["offen", "angenommen", "arbeit", "erledigt", "abgelehnt", "beworben"],
|
||
jetztStempel);
|
||
|
||
/* WER ENTSCHIEDEN HAT, UND WAS ER DAZU GESAGT HAT.
|
||
`grund` traegt weiterhin die Worte der Person selbst (beim
|
||
Ablehnen ihre Begruendung, beim Bewerben ihr Anliegen). Der
|
||
Kommentar der Leitung ist etwas anderes und gehoert nicht in
|
||
dasselbe Feld -- sonst ueberschreibt die Antwort die Frage. */
|
||
for (const [spalte, form] of [
|
||
["entscheid_text", "TEXT"],
|
||
["entschieden_von", "INTEGER REFERENCES personen(id) ON DELETE SET NULL"],
|
||
["entschieden_am", "TEXT"],
|
||
]) {
|
||
const da = d.prepare("PRAGMA table_info(aufgaben_zuteilung)").all()
|
||
.map((z) => z.name);
|
||
if (da.includes(spalte)) continue;
|
||
try {
|
||
d.exec(`ALTER TABLE aufgaben_zuteilung ADD COLUMN ${spalte} ${form}`);
|
||
console.log(`[workspace] Spalte '${spalte}' an der Zuteilung ergaenzt.`);
|
||
} catch (f) {
|
||
console.error(`[workspace] Spalte '${spalte}':`, f?.message);
|
||
}
|
||
}
|
||
|
||
/* ---- Der sechste Bereich: "agentur" (06.09.2026) ----
|
||
|
||
Kernprinzip 04 des Konzepts lautet woertlich "Die Agentur bleibt
|
||
angebunden". Fuenf der sechs Bereiche standen (LIVE, Content,
|
||
Technik, Community, Schutz) -- der sechste fehlte vollstaendig. Was
|
||
dadurch nirgends stand: welche Kampagne laeuft und bis wann, welche
|
||
Schulung ansteht, und vor allem der OFFIZIELLE WEG zur Agentur mit
|
||
Stand und Antwort. Das lief bisher ueber private Nachrichten und
|
||
war damit weder auffindbar noch nachvollziehbar.
|
||
|
||
Und die Trennung, die das Konzept ausdruecklich verlangt und die im
|
||
Alltag sonst verschwimmt: Betreuung und Umsetzung sind UNSERE
|
||
Seite, offizielle Wege und Plattformthemen die der Agentur. Wer
|
||
wofuer zustaendig ist, gehoert auf den Bildschirm, nicht ins
|
||
Gedaechtnis.
|
||
|
||
Derselbe Umbau wie bei "abgebrochen" darueber -- ein CHECK laesst
|
||
sich in SQLite nicht aendern, die Tabelle muss neu gebaut werden:
|
||
erst sichern, dann tauschen, Zeilen zaehlen, Verweise pruefen. */
|
||
/* ---- Der sechste Bereich: "agentur" (06.09.2026) ----
|
||
ABGELEITET STATT ABGESCHRIEBEN (11.09.2026).
|
||
|
||
Hier stand bis heute der Bauplan der Tabelle von Hand abgeschrieben
|
||
-- alle Spalten, zweimal (einmal fuer CREATE, einmal fuer INSERT).
|
||
Am 07.09.2026 fiel auf, dass dabei drei Event-Spalten fehlten; sie
|
||
wurden nachgetragen, und daneben kam ein Kommentar, der genau vor
|
||
dieser Verlustart warnt.
|
||
|
||
DIESELBE FALLE HAT DANACH EIN ZWEITES MAL ZUGESCHNAPPT. Seit dem
|
||
07.09. kamen `einsatz`, `nur_leitung` und `gesendet_am` dazu. Der
|
||
Spalten-Nachtrag legt sie an, dieser Neubau warf sie unmittelbar
|
||
danach weg -- mit Inhalt, ohne Fehlermeldung, bei unveraenderter
|
||
Zeilenzahl. Gemessen: 24 Spalten hinein, 21 heraus.
|
||
|
||
Gefunden hat es pruef-agentur, und zwar seit Tagen: 31-mal "FEHL"
|
||
mit HTTP 503. Sagen konnte sie es erst, seit sie die
|
||
Serverausgabe zeigt -- dort stand es dann in drei Zeilen
|
||
untereinander.
|
||
|
||
DESHALB WIRD NICHTS MEHR ABGESCHRIEBEN. `checkListeErweitern` holt
|
||
den Bauplan aus sqlite_master und die Spaltenliste aus
|
||
PRAGMA table_info -- sie kann gar nicht veralten, weil sie nicht
|
||
gepflegt wird. Sie sichert vorher, zaehlt die Zeilen innerhalb der
|
||
Transaktion, nimmt die Indizes mit und prueft die Verweise, also
|
||
alles, was der Block hier auch tat.
|
||
|
||
Die echten Daten auf dem Server sind nicht betroffen: Ihr CHECK
|
||
kennt 'agentur' laengst, die Umstellung laeuft dort nie wieder.
|
||
Gefaehrlich war es beim Zurueckspielen einer Sicherung von vor dem
|
||
06.09.2026 -- also genau dann, wenn man sich am wenigsten einen
|
||
stillen Datenverlust leisten kann. */
|
||
checkListeErweitern(d, "eintraege", "bereich", "agentur",
|
||
["live", "content", "technik", "community", "schutz", "agentur"], jetztStempel);
|
||
|
||
/* ---- Rolle "manager" erlauben ---- */
|
||
/* ---- AUCH HIER STAND EIN NEUBAU VON HAND (ersetzt 21.09.2026) ----
|
||
|
||
Er zaehlte NEUN Spalten auf. `personen` hat neunzehn. Verloren
|
||
gegangen waeren:
|
||
|
||
bild, ueber_mich, tiktok, instagram, youtube, twitch,
|
||
chat_kachel, code_kennung, stufe, alter_bestaetigt_am
|
||
|
||
Das ist die empfindlichste Tabelle des Hauses, und zwei Eintraege
|
||
darin sind mehr als Komfort: `bild` sind die Profilfotos, und
|
||
`alter_bestaetigt_am` ist die Altersbestaetigung. Waere sie beim
|
||
Zurueckspielen einer Sicherung verschwunden, haette niemand eine
|
||
Fehlermeldung gesehen -- alle waeren still wieder unbestaetigt
|
||
gewesen, auf einer Seite, die genau das nicht sein darf.
|
||
|
||
Derselbe Befund wie beim Block darueber, und dieselbe Loesung:
|
||
`checkListeErweitern` leitet die Spaltenliste aus
|
||
`PRAGMA table_info` ab. Eine Liste, die niemand pflegt, kann nicht
|
||
veralten.
|
||
|
||
DIE KETTE IST KUMULATIV -- jeder Schritt nennt alle bis dahin
|
||
erlaubten Werte, und dieser ist der erste. Deshalb steht er VOR
|
||
den Aufrufen fuer spicy/modi/hand/gast weiter unten; die Reihenfolge
|
||
ist Teil der Bedeutung, nicht Zufall. */
|
||
checkListeErweitern(d, "personen", "rolle", "manager",
|
||
["admin", "manager", "scout", "creator"], jetztStempel);
|
||
|
||
/* =====================================================================
|
||
ROLLE "spicy" (Spicy Media) FREISCHALTEN — 07.09.2026
|
||
|
||
Wunsch Filipe: "ich will dass ueber dogfather auch eine kategorie,
|
||
eine neue rolle entsteht die den namen traegt, spicy media."
|
||
|
||
---------------------------------------------------------------------
|
||
DAS IST DER RISKANTESTE UMBAU IM GANZEN HAUS
|
||
|
||
Eine Rolle ist ein erlaubter Wert in einer Spalte, und der steckt in
|
||
SQLite in einem CHECK. Ein CHECK laesst sich nicht aendern -- die
|
||
Tabelle muss neu gebaut werden. Bei `eintraege` war das schon
|
||
unangenehm; hier geht es um `personen`, und auf die zeigen
|
||
ZWEIUNDFUENFZIG Fremdschluessel aus einem Dutzend Tabellen. Jede
|
||
Sitzung, jede Aufgabe, jeder Termin, jede Datei haengt daran.
|
||
|
||
DESHALB WIRD DER BAUPLAN NICHT ABGESCHRIEBEN, SONDERN GELESEN.
|
||
|
||
Die beiden Umstellungen darueber schreiben ihre Spaltenliste von
|
||
Hand ab. Das ging gut, solange die Liste stimmte -- aber `personen`
|
||
hat seit damals SIEBEN Spalten dazubekommen (bild, ueber_mich,
|
||
tiktok, instagram, youtube, twitch und weitere, siehe den
|
||
Spalten-Nachtrag oben). Wer hier eine Liste abschreibt, verliert
|
||
beim naechsten Mal genau die Spalte, die jemand letzte Woche
|
||
ergaenzt hat -- samt allen Profilbildern.
|
||
|
||
Also: Der vorhandene CREATE-Text wird aus sqlite_master geholt und
|
||
darin AUSSCHLIESSLICH die Rollenliste ersetzt. Alles andere -- jede
|
||
Spalte, jeder Typ, jede Vorgabe -- bleibt woertlich stehen. Und die
|
||
Kopierliste kommt aus PRAGMA table_info, also aus der Tabelle
|
||
selbst.
|
||
|
||
WENN DIE ERSETZUNG NICHT GREIFT, WIRD NICHT UMGESTELLT. Ein
|
||
`replace`, das nichts findet, gibt den Text unveraendert zurueck --
|
||
die Umstellung liefe dann durch, baute dieselbe Tabelle noch einmal
|
||
und meldete Erfolg. Genau die Sorte gruener Haken, die nichts
|
||
geprueft hat. Deshalb wird danach nachgesehen, ob 'spicy' wirklich
|
||
drinsteht.
|
||
===================================================================== */
|
||
/* =====================================================================
|
||
MEHR TERMINARTEN (08.09.2026)
|
||
|
||
Wunsch Filipe: "da sollen auch noch kategorien wie bigmatch,
|
||
turniere, special-live ... informier dich, was man da alles noch
|
||
gebrauchen koennte."
|
||
|
||
Recherchiert (Streams Charts, Kick, StreamerCollabs): Was ein
|
||
Creator-Team im Kalender wirklich unterscheidet, sind neben den
|
||
internen Terminen die AUFTRITTE -- Wettkaempfe (BigMatch, Turnier),
|
||
gemeinsame Formate (Collab, Raid-Train) und die grossen Ausnahmen
|
||
(Special-Live, Charity). Genau diese sechs kommen dazu.
|
||
|
||
WARUM EIN TABELLENUMBAU: Die erlaubten Werte stecken in einem
|
||
CHECK, und ein CHECK laesst sich in SQLite nicht aendern. Dieselbe
|
||
Lage wie bei der Rolle 'spicy' -- und deshalb hier dasselbe,
|
||
bewaehrte Verfahren: Bauplan aus sqlite_master lesen, NUR die Liste
|
||
ersetzen, das Ergebnis pruefen, Spalten aus PRAGMA holen, Zeilen
|
||
INNERHALB der Transaktion zaehlen, Sicherung vorher.
|
||
|
||
Zwei Tabellen statt einer (termine und termin_serien), deshalb
|
||
eine Schleife -- zweimal derselbe Text waere zweimal dieselbe
|
||
Gelegenheit, eine Stelle zu vergessen. */
|
||
const ARTEN_NEU = "'termin','call','review','bigmatch','turnier','special','collab','raid','charity'";
|
||
for (const tabelle of ["termine", "termin_serien"]) {
|
||
const plan = d.prepare(
|
||
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get(tabelle)?.sql || "";
|
||
/* Geprueft wird die REGEL, nicht der ganze Bauplan: SQLite hebt ihn
|
||
woertlich auf, samt Kommentaren -- ein erklaerender Satz mit dem
|
||
Wort 'bigmatch' wuerde sonst genuegen, damit die Umstellung sich
|
||
fuer erledigt haelt. Genau dieser Fehler ist bei 'spicy' passiert. */
|
||
const artRegel = plan.match(/art\s+IN\s*\(([^)]*)\)/i)?.[1] || "";
|
||
if (!plan || !artRegel || artRegel.includes("'bigmatch'")) continue;
|
||
|
||
/* DER TABELLENNAME GEHOERT IN DEN DATEINAMEN (08.09.2026).
|
||
|
||
Hier stand `.vor-arten-${jetztStempel}` -- ohne die Tabelle. Der
|
||
Stempel wird EINMAL pro Serverstart gebildet, diese Schleife
|
||
laeuft aber ZWEIMAL. Der zweite Durchlauf wollte also dieselbe
|
||
Datei anlegen, `VACUUM INTO` weigert sich (die Datei ist schon
|
||
da), und das `break` unten beendete daraufhin die ganze Schleife.
|
||
|
||
Ergebnis: `termine` war umgestellt, `termin_serien` NICHT. Und
|
||
zwar still -- die Meldung ging in die Serverausgabe, die niemand
|
||
liest. Aufgefallen waere es erst, wenn jemand eine wiederkehrende
|
||
BigMatch-Reihe anlegt und die Datenbank sie ohne erkennbaren
|
||
Grund ablehnt.
|
||
|
||
Das `break` bleibt richtig: Wenn sich keine Sicherung anlegen
|
||
laesst, wird nicht umgebaut. Falsch war nur der Name. */
|
||
const sicherung = `${DB_PFAD}.vor-arten-${tabelle}-${jetztStempel}`;
|
||
try {
|
||
d.exec(`VACUUM INTO '${sicherung.replace(/'/g, "''")}'`);
|
||
console.log("[workspace] Sicherung vor der Artenumstellung:", sicherung);
|
||
} catch (fehler) {
|
||
console.error("[workspace] Sicherung fehlgeschlagen, Artenumstellung abgebrochen:",
|
||
fehler?.message);
|
||
break;
|
||
}
|
||
|
||
const neuerPlan = plan
|
||
/* DAS MUSTER WIRD ZUSAMMENGESETZT, NICHT IN EINEN TEMPLATE-STRING
|
||
GESCHRIEBEN. Dort wird \s beim Einlesen zu einem blossen "s" --
|
||
aus "CREATE TABLE\s+" wuerde "CREATE TABLEs+", das Muster
|
||
passte auf nichts, und die Umstellung waere STILL ausgeblieben.
|
||
Genau so stand es hier beim ersten Anlauf; nachgemessen mit
|
||
einem Einzeiler, der beide Schreibweisen gegen den echten
|
||
Bauplan haelt. */
|
||
.replace(new RegExp(
|
||
"CREATE TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?[\"'`]?" + tabelle + "[\"'`]?", "i"),
|
||
`CREATE TABLE ${tabelle}_neu`)
|
||
.replace(/art\s+IN\s*\([^)]*\)/i, `art IN (${ARTEN_NEU})`);
|
||
if (!neuerPlan.includes("'bigmatch'") || !neuerPlan.includes(`${tabelle}_neu`)) {
|
||
console.error(`[workspace] Artenumstellung abgebrochen: Bauplan von ${tabelle} `
|
||
+ "liess sich nicht umschreiben.");
|
||
continue;
|
||
}
|
||
|
||
const spalten = d.prepare(`PRAGMA table_info(${tabelle})`).all().map((z) => z.name);
|
||
if (!spalten.length) continue;
|
||
const liste = spalten.map((n) => `"${n}"`).join(", ");
|
||
|
||
d.exec("PRAGMA foreign_keys = OFF");
|
||
try {
|
||
const vorher = d.prepare(`SELECT COUNT(*) AS n FROM ${tabelle}`).get().n;
|
||
d.exec("BEGIN");
|
||
d.exec(neuerPlan);
|
||
d.exec(`INSERT INTO ${tabelle}_neu (${liste}) SELECT ${liste} FROM ${tabelle};`);
|
||
const nachher = d.prepare(`SELECT COUNT(*) AS n FROM ${tabelle}_neu`).get().n;
|
||
if (nachher !== vorher) {
|
||
d.exec("ROLLBACK");
|
||
console.error(`[workspace] Artenumstellung abgebrochen: ${vorher} Zeilen vorher, `
|
||
+ `${nachher} nachher. Sicherung: ${sicherung}`);
|
||
} else {
|
||
d.exec(`DROP TABLE ${tabelle};`);
|
||
d.exec(`ALTER TABLE ${tabelle}_neu RENAME TO ${tabelle};`);
|
||
d.exec("COMMIT");
|
||
const kaputt = d.prepare("PRAGMA foreign_key_check").all();
|
||
if (kaputt.length) {
|
||
console.error("[workspace] ACHTUNG: nach der Artenumstellung", kaputt.length,
|
||
"verwaiste Verweise. Sicherung:", sicherung);
|
||
} else {
|
||
console.log(`[workspace] Terminarten erweitert in ${tabelle}: ${nachher} Zeilen, `
|
||
+ `${spalten.length} Spalten uebernommen, Verweise geprueft.`);
|
||
}
|
||
}
|
||
} catch (fehler) {
|
||
try { d.exec("ROLLBACK"); } catch { /* schon zurueckgerollt */ }
|
||
console.error("[workspace] Artenumstellung fehlgeschlagen:", fehler?.message,
|
||
"-- Sicherung:", sicherung);
|
||
} finally {
|
||
d.exec("PRAGMA foreign_keys = ON");
|
||
}
|
||
}
|
||
|
||
/* Welche Rollen die Datenbank zulaesst. Der Ablauf steht in
|
||
rollenRegelUmstellen() -- einmal, nicht je Rolle abgeschrieben.
|
||
Beide Aufrufe stehen hier: Auf einer Datenbank, die 'spicy' schon
|
||
kennt, tut der erste nichts und nur der zweite laeuft. */
|
||
checkListeErweitern(d, "personen", "rolle", "spicy",
|
||
["spicy", "admin", "manager", "scout", "creator"], jetztStempel);
|
||
checkListeErweitern(d, "personen", "rolle", "modi",
|
||
["spicy", "admin", "manager", "scout", "creator", "modi"], jetztStempel);
|
||
checkListeErweitern(d, "personen", "rolle", "hand",
|
||
["spicy", "admin", "manager", "scout", "creator", "modi", "hand"], jetztStempel);
|
||
/* DIE COMMUNITY (11.09.2026). Dieselbe Umstellung wie die drei
|
||
darueber: Sicherung vorher, Neubau, Zaehlung innerhalb der
|
||
Transaktion, Indizes zurueck. */
|
||
checkListeErweitern(d, "personen", "rolle", "gast",
|
||
["spicy", "admin", "manager", "scout", "creator", "modi", "hand", "gast"], jetztStempel);
|
||
/* DIE LINKE HAND (21.09.2026) -- UND SIE STEHT MIT ABSICHT GANZ
|
||
UNTEN, NICHT BEI DER RECHTEN HAND.
|
||
|
||
`checkListeErweitern` baut die Regel NEU aus der Liste, die man
|
||
ihm gibt. Stand mein Schritt weiter oben, nahm der Schritt fuer
|
||
"gast" darunter die Rolle wieder heraus -- seine Liste kennt sie
|
||
ja nicht. Die Kette ist kumulativ und deshalb von der Reihenfolge
|
||
abhaengig; wer hier etwas einschiebt, gehoert ans ENDE.
|
||
|
||
Ohne diesen Schritt laesst die Datenbank die Rolle nicht zu -- und
|
||
zwar erst beim ersten echten Anlegen, nicht beim Start. */
|
||
checkListeErweitern(d, "personen", "rolle", "linke",
|
||
["spicy", "admin", "manager", "scout", "creator", "modi", "hand",
|
||
"gast", "linke"], jetztStempel);
|
||
|
||
/* DER BEREICH FUER DAS IDEEN-BOARD (10.09.2026, Kapitel 5.6).
|
||
|
||
Dieselbe Umstellung, andere Tabelle -- und genau deshalb steht sie
|
||
ab jetzt EINMAL da. Der alte agentur-Block weiter unten schreibt
|
||
den ganzen Bauplan von Hand ab; im Kommentar dort steht, dass dabei
|
||
schon einmal drei Spalten vergessen wurden. Eine vierte Abschrift
|
||
waere die vierte Gelegenheit dazu. */
|
||
checkListeErweitern(d, "eintraege", "bereich", "ideen",
|
||
["live", "content", "technik", "community", "schutz", "agentur", "ideen"], jetztStempel);
|
||
/* Die Angebote (10.09.2026) -- Vorschlaege der Modis, ueber die
|
||
DogFather und VanVan entscheiden. */
|
||
checkListeErweitern(d, "eintraege", "bereich", "angebote",
|
||
["live", "content", "technik", "community", "schutz", "agentur", "ideen", "angebote"],
|
||
jetztStempel);
|
||
/* ZWEI NEUE ZUSTAENDE. Bisher kannte ein Eintrag nur offen/erledigt.
|
||
Ein Angebot hat aber eine ENTSCHEIDUNG: angenommen oder abgelehnt
|
||
-- und das ist etwas anderes als "erledigt". Wer beides in einen
|
||
Topf wirft, kann hinterher nicht mehr sagen, was aus einem
|
||
Vorschlag geworden ist. */
|
||
/* DER BEREICH FUER DIE RUECKMELDUNG (10.09.2026, Kapitel 5 und 6). */
|
||
checkListeErweitern(d, "eintraege", "bereich", "rueckmeldung",
|
||
["live", "content", "technik", "community", "schutz", "agentur", "ideen",
|
||
"angebote", "rueckmeldung"], jetztStempel);
|
||
|
||
/* ENTWICKLUNG UND TALENTE (11.09.2026, Wunsch Filipe). */
|
||
checkListeErweitern(d, "eintraege", "bereich", "entwicklung",
|
||
["live", "content", "technik", "community", "schutz", "agentur", "ideen",
|
||
"angebote", "rueckmeldung", "entwicklung"], jetztStempel);
|
||
checkListeErweitern(d, "eintraege", "bereich", "talente",
|
||
["live", "content", "technik", "community", "schutz", "agentur", "ideen",
|
||
"angebote", "rueckmeldung", "entwicklung", "talente"], jetztStempel);
|
||
/* DIE SIEBEN BRETTER DES TREFFS (11.09.2026).
|
||
|
||
EINE Umstellung fuer alle sieben, nicht sieben einzelne. Jeder
|
||
Aufruf baut die Tabelle neu auf -- siebenmal hintereinander waeren
|
||
sieben Sicherungen, sieben Neubauten und sieben Gelegenheiten, dass
|
||
etwas schiefgeht. Der Marker ist "treff"; ist der schon in der
|
||
CHECK-Liste, passiert gar nichts. */
|
||
checkListeErweitern(d, "eintraege", "bereich", "treff",
|
||
["live", "content", "technik", "community", "schutz", "agentur", "ideen",
|
||
"angebote", "rueckmeldung", "entwicklung", "talente",
|
||
"anschlag", "ansteht", "treff", "wunsch", "highlight", "regeln", "mitmachen"],
|
||
jetztStempel);
|
||
|
||
checkListeErweitern(d, "eintraege", "status", "angenommen",
|
||
["offen", "erledigt", "angenommen", "abgelehnt"], jetztStempel);
|
||
|
||
/* DIE DRITTE RAUMART (10.09.2026, Kapitel 7.2). Auf einer frischen
|
||
Anlage steht 'kanal' schon im Bauplan -- dann findet der Aufruf die
|
||
Regel bereits erweitert und tut nichts. */
|
||
checkListeErweitern(d, "chat_raeume", "art", "kanal",
|
||
["direkt", "gruppe", "kanal"], jetztStempel);
|
||
|
||
/* EIN KANAL JE ZUSTAENDIGKEIT, und die Datenbank haelt das fest.
|
||
|
||
Die Regel steht auch in der Route -- aber eine Regel, die nur in
|
||
einer Route steht, gilt genau so lange, bis jemand eine zweite
|
||
Route schreibt. Zwei Kanaele "Clipping" waeren zwei halbe
|
||
Verlaeufe, und man merkt es erst, wenn jemand die Antwort im
|
||
falschen sucht.
|
||
|
||
TEILINDEX (`WHERE art = 'kanal'`): Gruppen und Zweier-Gespraeche
|
||
haben gar keine Kategorie; ohne die Bedingung waere schon die
|
||
zweite Gruppe mit NULL ein Verstoss -- nein, NULL zaehlt in SQLite
|
||
nicht als Dublette, aber der Index waere trotzdem groesser als
|
||
noetig und wuerde etwas versprechen, was er nicht meint.
|
||
|
||
ER STEHT HIER UNTEN und nicht im Bauplan: Die Spalte `kategorie`
|
||
entsteht auf einer bestehenden Anlage erst durch die Schleife
|
||
weiter oben -- im Bauplan gaebe es sie beim Start noch nicht. */
|
||
try {
|
||
d.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_kanal_kategorie
|
||
ON chat_raeume (kategorie) WHERE art = 'kanal'`);
|
||
} catch (fehler) {
|
||
console.error("[workspace] Kanal-Index:", fehler?.message);
|
||
}
|
||
}
|
||
|
||
export function db() {
|
||
if (_db) return _db;
|
||
if (_dbFehler) throw _dbFehler;
|
||
try {
|
||
const { DatabaseSync } = require("node:sqlite");
|
||
mkdirSync(dirname(DB_PFAD), { recursive: true });
|
||
const d = new DatabaseSync(DB_PFAD);
|
||
d.exec(`
|
||
PRAGMA journal_mode = WAL;
|
||
PRAGMA foreign_keys = ON;
|
||
|
||
/* ROLLENLISTE: 'spicy' steht hier mit drin, nicht nur in der
|
||
Umstellung (07.09.2026, gefunden von pruef-spicy). Vorher legte
|
||
eine frische Datenbank die alte Liste an, und die Umstellung
|
||
lief beim allerersten Start sofort hinterher -- Tabelle neu
|
||
bauen, Sicherung schreiben, umbenennen, fuer nichts. Ein
|
||
Bauplan, der sofort umgebaut werden muss, ist der falsche.
|
||
|
||
KEIN KOMMENTAR INNERHALB DIESES CREATE-TEXTES. SQLite hebt ihn
|
||
woertlich in sqlite_master auf -- samt Kommentaren. Ein Satz mit
|
||
dem Wort 'spicy' darin haette bedeutet, dass die Umstellung die
|
||
Tabelle fuer bereits umgestellt haelt, obwohl die CHECK-Regel
|
||
noch die alte ist. Genau das ist beim Bauen passiert: Der
|
||
Bauplan enthielt das Wort, die Regel nicht. */
|
||
CREATE TABLE IF NOT EXISTS personen (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name TEXT NOT NULL,
|
||
rolle TEXT NOT NULL CHECK (rolle IN ('spicy','admin','manager','scout','creator','modi')),
|
||
code_hash TEXT NOT NULL,
|
||
code_salt TEXT NOT NULL,
|
||
code_n INTEGER NOT NULL,
|
||
aktiv INTEGER NOT NULL DEFAULT 1,
|
||
erstellt TEXT NOT NULL,
|
||
letzter_login TEXT
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS sitzungen (
|
||
token_hash TEXT PRIMARY KEY,
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
erstellt TEXT NOT NULL,
|
||
gueltig_bis TEXT NOT NULL,
|
||
ip TEXT,
|
||
browser TEXT
|
||
);
|
||
|
||
/* Audit-Log. Im Konzept ausdrücklich gefordert ("Login-Historie,
|
||
kritische Aktionen protokollieren"). Nur anhängen, nie ändern. */
|
||
CREATE TABLE IF NOT EXISTS protokoll (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
zeitpunkt TEXT NOT NULL,
|
||
person_id INTEGER,
|
||
rolle TEXT,
|
||
aktion TEXT NOT NULL,
|
||
detail TEXT,
|
||
ip TEXT
|
||
);
|
||
|
||
/* Aufgaben. creator_id sagt, ZU WEM die Aufgabe gehört (wessen
|
||
Bereich), verantwortlich_id, WER sie erledigt. Beides getrennt,
|
||
weil im Konzept auch Aufgaben vorkommen, die das Management für
|
||
einen Creator anlegt. */
|
||
CREATE TABLE IF NOT EXISTS aufgaben (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
titel TEXT NOT NULL,
|
||
beschreibung TEXT,
|
||
status TEXT NOT NULL DEFAULT 'offen'
|
||
CHECK (status IN ('offen','arbeit','review','erledigt','abgebrochen')),
|
||
prioritaet TEXT NOT NULL DEFAULT 'mittel'
|
||
CHECK (prioritaet IN ('hoch','mittel','niedrig')),
|
||
creator_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
verantwortlich_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
frist TEXT,
|
||
erstellt TEXT NOT NULL,
|
||
erstellt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
geaendert TEXT,
|
||
erledigt_am TEXT
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_aufgaben_status ON aufgaben (status);
|
||
CREATE INDEX IF NOT EXISTS idx_aufgaben_creator ON aufgaben (creator_id);
|
||
|
||
/* Einstellungen, die sich zur Laufzeit aendern lassen. Bisher nur
|
||
eine: ob die KI-Vorschlaege angeboten werden. Als Tabelle statt
|
||
als Datei, damit sie dieselbe Sicherung bekommt wie alles
|
||
andere -- eine Einstellung, die beim Wiederherstellen fehlt,
|
||
faellt erst auf, wenn etwas nicht mehr geht. */
|
||
CREATE TABLE IF NOT EXISTS einstellungen (
|
||
schluessel TEXT PRIMARY KEY,
|
||
wert TEXT NOT NULL,
|
||
geaendert TEXT,
|
||
von INTEGER REFERENCES personen(id) ON DELETE SET NULL
|
||
);
|
||
|
||
/* BENACHRICHTIGUNGEN (05.09.2026) ---------------------------------
|
||
|
||
Wunsch Filipe, mit dem Bild eines "Benachrichtigungen aus"-
|
||
Knopfes: *"ich will auch sowas und dass es perfekt funktioniert
|
||
auf der seite fuer jeden."*
|
||
|
||
Eine Anmeldung ist die Adresse, unter der ein bestimmter
|
||
BROWSER auf einem bestimmten GERAET erreichbar ist -- nicht die
|
||
Person. Wer den Workspace auf dem Rechner und auf dem Handy
|
||
benutzt, hat zwei; beide sollen klingeln, und beide muessen
|
||
einzeln abschaltbar sein.
|
||
|
||
Der Endpunkt ist der Schluessel, nicht eine eigene Nummer: Meldet
|
||
sich derselbe Browser erneut an (nach dem Leeren der
|
||
Website-Daten etwa), soll daraus KEIN zweiter Eintrag werden,
|
||
sonst kaeme jede Nachricht doppelt.
|
||
|
||
p256dh und auth sind die Schluessel des Browsers. Ohne sie
|
||
laesst sich nichts verschluesseln -- und ohne Verschluesselung
|
||
nimmt kein Push-Dienst etwas an. */
|
||
CREATE TABLE IF NOT EXISTS push_anmeldungen (
|
||
endpunkt TEXT PRIMARY KEY,
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
p256dh TEXT NOT NULL,
|
||
auth TEXT NOT NULL,
|
||
geraet TEXT,
|
||
erstellt TEXT NOT NULL,
|
||
zuletzt_ok TEXT,
|
||
fehler INTEGER NOT NULL DEFAULT 0
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_push_person ON push_anmeldungen (person_id);
|
||
|
||
/* Was jemand bekommen WILL -- je Art einzeln.
|
||
|
||
Der Plan ist an dieser Stelle deutlich: *"je Person abschaltbar
|
||
-- pro Art, nicht alles oder nichts. Eine Benachrichtigung, die
|
||
nervt, wird abgeschaltet und dann fehlt auch die wichtige."*
|
||
|
||
Fehlt eine Zeile, gilt die Voreinstellung aus workspace-push.js
|
||
(an). Damit muss niemand erst etwas einstellen, um etwas zu
|
||
bekommen -- und wer etwas abstellt, bekommt genau das nicht
|
||
mehr. */
|
||
CREATE TABLE IF NOT EXISTS push_einstellungen (
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
art TEXT NOT NULL,
|
||
an INTEGER NOT NULL DEFAULT 1,
|
||
PRIMARY KEY (person_id, art)
|
||
);
|
||
|
||
/* Was schon verschickt wurde. Ohne dieses Gedaechtnis bekaeme
|
||
jemand dieselbe Erinnerung bei jedem Lauf erneut -- viermal am
|
||
Tag "Aufgabe ist ueberfaellig" ist der schnellste Weg, dass
|
||
jemand Benachrichtigungen komplett abschaltet.
|
||
|
||
Das Merkmal ist die Sache selbst (z. B. "aufgabe-faellig:42"),
|
||
nicht der Zeitpunkt: Dieselbe Aufgabe erinnert einmal, nicht
|
||
einmal je Stunde. */
|
||
CREATE TABLE IF NOT EXISTS push_verschickt (
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
merkmal TEXT NOT NULL,
|
||
zeit TEXT NOT NULL,
|
||
PRIMARY KEY (person_id, merkmal)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_push_verschickt_zeit ON push_verschickt (zeit);
|
||
|
||
/* Rueckmeldungen zu einer Aufgabe (Konzept: "Aufgaben & Feedback").
|
||
Ohne sie endet jede Rueckfrage ausserhalb des Systems -- in
|
||
WhatsApp, und damit ausserhalb dessen, was spaeter noch
|
||
nachvollziehbar ist.
|
||
Wer die Aufgabe sehen darf, darf auch die Rueckmeldungen sehen
|
||
und schreiben. Eine eigene Sichtbarkeitsregel gibt es bewusst
|
||
NICHT: Zwei Regeln fuer dieselbe Sache laufen irgendwann
|
||
auseinander. */
|
||
CREATE TABLE IF NOT EXISTS aufgaben_notizen (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
aufgabe_id INTEGER NOT NULL REFERENCES aufgaben(id) ON DELETE CASCADE,
|
||
person_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
text TEXT NOT NULL,
|
||
erstellt TEXT NOT NULL
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_aufgaben_notizen ON aufgaben_notizen (aufgabe_id);
|
||
|
||
/* =================================================================
|
||
WER HAT DIE AUFGABE -- UND WIE STEHT ES BEI IHM? (21.09.2026)
|
||
|
||
Filipe, Abschnitt 4: *"Wenn eine Aufgabe mehreren Modis
|
||
zugewiesen wird, darf sie im Resuemee von Dogfather und der
|
||
rechten Hand nicht lediglich als eine einzige grosse
|
||
Sammelaufgabe erscheinen. Die Aufgabe muss in der Auswertung
|
||
auf die einzelnen zugewiesenen Personen aufgeteilt werden."*
|
||
|
||
GENAU DESHALB EINE EIGENE TABELLE und nicht mehrere Spalten an
|
||
der Aufgabe. Eine Aufgabe hat EINEN Titel und EINE Frist --
|
||
aber je Mensch einen eigenen Stand, einen eigenen Grund, wenn
|
||
er ablehnt, ein eigenes Erledigt-Datum und eine eigene
|
||
Bewertung. Das in Spalten zu pressen hiesse, dieselbe Aufgabe
|
||
mehrfach anzulegen; dann waere die uebergeordnete Struktur
|
||
weg, die der Auftrag ausdruecklich erhalten will.
|
||
|
||
"zustand" ist der Stand DIESES Menschen, nicht der Aufgabe:
|
||
|
||
offen er hat sie bekommen, aber noch nicht geantwortet
|
||
angenommen er hat ja gesagt
|
||
arbeit er ist dran
|
||
erledigt er ist fertig
|
||
abgelehnt er hat nein gesagt -- "grund" sagt, warum
|
||
|
||
"aufgaben.status" bleibt daneben unberuehrt. Zwei Ebenen, weil
|
||
es zwei Fragen sind: "Wie steht die Aufgabe?" und "Wie steht
|
||
SIE bei ihm?" Ein einziges Feld koennte bei drei Leuten nicht
|
||
gleichzeitig "angenommen" und "abgelehnt" sein.
|
||
|
||
WARUM HIER EIN CHECK STEHT, oben aber keiner: Diese Tabelle
|
||
entsteht neu. Ein CHECK auf einer neuen Tabelle kostet nichts;
|
||
einer auf einer nachgetragenen Spalte kostet einen Neubau.
|
||
|
||
KEINE BACKTICKS IN DIESEM KOMMENTAR: Er steht INNERHALB eines
|
||
Template-Literals (d.exec). Ein Backtick beendet es mitten im
|
||
Satz, und die Datei ist ab da syntaktisch kaputt -- an einer
|
||
Stelle, die nichts mit dem Fehler zu tun hat.
|
||
|
||
UNIQUE (aufgabe_id, person_id): Niemand bekommt dieselbe
|
||
Aufgabe zweimal. Ohne das entstuenden bei einem doppelten
|
||
Klick zwei Zeilen, und das Resuemee zaehlte eine Aufgabe
|
||
doppelt -- ein Fehler, den man erst an einer krummen Zahl
|
||
bemerkt und dann nicht mehr erklaeren kann.
|
||
|
||
ON DELETE CASCADE: Wird die Aufgabe geloescht, gehen die
|
||
Zuteilungen mit. Fremdschluessel sind in diesem Haus
|
||
eingeschaltet (am 20.09. gemessen, nicht angenommen). */
|
||
CREATE TABLE IF NOT EXISTS aufgaben_zuteilung (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
aufgabe_id INTEGER NOT NULL REFERENCES aufgaben(id) ON DELETE CASCADE,
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
zustand TEXT NOT NULL DEFAULT 'offen'
|
||
CHECK (zustand IN ('offen','angenommen','arbeit','erledigt','abgelehnt')),
|
||
grund TEXT,
|
||
zugeteilt_am TEXT NOT NULL,
|
||
zugeteilt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
geantwortet_am TEXT,
|
||
erledigt_am TEXT,
|
||
/* DIE RUECKMELDUNG (Abschnitt 5). Sie haengt am MENSCHEN und
|
||
nicht an der Aufgabe: Bei drei Leuten koennen drei
|
||
verschiedene Rueckmeldungen richtig sein. */
|
||
bewertung TEXT CHECK (bewertung IN ('gut','nicht_gut','verbessern')),
|
||
bewertung_text TEXT,
|
||
bewertet_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
bewertet_am TEXT,
|
||
UNIQUE (aufgabe_id, person_id)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_zuteilung_aufgabe ON aufgaben_zuteilung (aufgabe_id);
|
||
CREATE INDEX IF NOT EXISTS idx_zuteilung_person ON aufgaben_zuteilung (person_id, zustand);
|
||
|
||
/* =================================================================
|
||
CHAT (06.09.2026)
|
||
|
||
Wunsch Filipe: *"eine kategorie chat, wo die creator mit ihren
|
||
manager nachrichten austauschen können, wie erinnerungen, fragen
|
||
und noch vieles mehr, wo jeder immer mit denen schreiben kann
|
||
wie verbindung auch läuft."* Auf Nachfrage: Zweier-Gespraeche
|
||
UND Gruppen, und Manager/Scouts duerfen sich auch ohne
|
||
gemeinsamen Creator schreiben.
|
||
|
||
DREI TABELLEN, und die Aufteilung hat einen Grund:
|
||
|
||
chat_raeume das Gespraech selbst
|
||
chat_teilnehmer wer darin ist -- und bis wohin er gelesen hat
|
||
chat_nachrichten was gesagt wurde
|
||
|
||
WARUM DIE TEILNEHMER EINE EIGENE TABELLE SIND und nicht zwei
|
||
Spalten am Raum: Ein Zweier-Gespraech kaeme mit "person_a,
|
||
person_b" aus, eine Gruppe nicht. Zwei verschiedene Bauweisen
|
||
fuer dieselbe Sache waeren der sichere Weg dazu, dass eine
|
||
Abfrage die eine Haelfte vergisst. So ist ein Zweier-Gespraech
|
||
schlicht eine Gruppe mit zwei Leuten.
|
||
|
||
WARUM "gelesen_bis" AM TEILNEHMER haengt und nicht an der
|
||
Nachricht: Sonst braeuchte es je Person und Nachricht eine
|
||
Zeile -- bei drei Leuten und tausend Nachrichten dreitausend
|
||
Eintraege, nur um "gelesen" zu wissen. Eine Nummer je Person
|
||
und Raum genuegt: Alles darunter ist gelesen. */
|
||
CREATE TABLE IF NOT EXISTS chat_raeume (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
art TEXT NOT NULL DEFAULT 'direkt'
|
||
CHECK (art IN ('direkt','gruppe','kanal')),
|
||
/* Gruppen und Kanaele haben einen Namen. Ein Zweier-Gespraech
|
||
heisst immer nach dem Gegenueber -- und zwar fuer jeden
|
||
anders. Ein gespeicherter Name waere fuer eine der beiden
|
||
Seiten falsch.
|
||
Bei einem Kanal steht in kategorie, WORUM es geht; der Name
|
||
wird daraus gebildet. Zwei Namen fuer dieselbe Zustaendigkeit
|
||
sind der sichere Weg zu zwei Kanaelen mit je der Haelfte der
|
||
Nachrichten darin. */
|
||
name TEXT,
|
||
kategorie TEXT,
|
||
erstellt TEXT NOT NULL,
|
||
erstellt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
/* Der Zeitpunkt der letzten Nachricht, mitgefuehrt. Ohne ihn
|
||
braeuchte die Gespraechsliste je Raum eine Unterabfrage --
|
||
bei zwanzig Raeumen zwanzig Abfragen fuer eine Liste. */
|
||
letzte_am TEXT
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_chat_raeume_letzte ON chat_raeume (letzte_am DESC);
|
||
|
||
CREATE TABLE IF NOT EXISTS chat_teilnehmer (
|
||
raum_id INTEGER NOT NULL REFERENCES chat_raeume(id) ON DELETE CASCADE,
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
seit TEXT NOT NULL,
|
||
/* Bis zu welcher Nachricht diese Person gelesen hat. */
|
||
gelesen_bis INTEGER NOT NULL DEFAULT 0,
|
||
/* Wer eine Gruppe angelegt hat, darf Leute hinzufuegen und
|
||
entfernen. In Zweier-Gespraechen bedeutungslos. */
|
||
leitung INTEGER NOT NULL DEFAULT 0,
|
||
/* Verlassene Gruppen: Die Zeile bleibt mit einem Datum stehen,
|
||
damit der Verlauf bis dahin lesbar bleibt und niemand
|
||
rueckwirkend aus der Geschichte verschwindet. */
|
||
raus_am TEXT,
|
||
PRIMARY KEY (raum_id, person_id)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_chat_teilnehmer_person ON chat_teilnehmer (person_id);
|
||
|
||
CREATE TABLE IF NOT EXISTS chat_nachrichten (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
raum_id INTEGER NOT NULL REFERENCES chat_raeume(id) ON DELETE CASCADE,
|
||
person_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
text TEXT NOT NULL,
|
||
erstellt TEXT NOT NULL,
|
||
/* Zurueckgenommen: Der Text wird geleert, die Zeile bleibt.
|
||
Ein Loch im Verlauf wirft mehr Fragen auf als der Hinweis,
|
||
dass hier etwas zurueckgenommen wurde. */
|
||
weg_am TEXT
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_chat_nachrichten_raum
|
||
ON chat_nachrichten (raum_id, id);
|
||
|
||
/* ERWAEHNUNGEN -- "@Name" IM CHAT (20.09.2026).
|
||
|
||
Filipe: "ich will das man die leute mit @ markieren kann im
|
||
chat."
|
||
|
||
WARUM EINE EIGENE TABELLE UND NICHT NUR FARBIGER TEXT:
|
||
Markieren ist kein Schmuck, sondern eine Ansprache. Wer
|
||
gemeint ist, soll es MERKEN -- auch wenn er den Raum gerade
|
||
nicht offen hat und auch dann noch, wenn er ihn Stunden
|
||
spaeter aufmacht. Dafuer muss irgendwo stehen, wer gemeint
|
||
war.
|
||
|
||
WARUM NICHT AUS DEM TEXT NACHRECHNEN: Man koennte beim Lesen
|
||
jedes Mal "@" suchen. Dann muesste man aber bei jedem Aufbau
|
||
der Gespraechsliste den Text JEDER ungelesenen Nachricht
|
||
durchgehen -- bei einem lebhaften Raum sind das hunderte. Hier
|
||
steht das Ergebnis einmal und wird nie wieder gerechnet.
|
||
|
||
WER GEMEINT IST, ENTSCHEIDET DER SERVER -- nicht der Browser.
|
||
Der Browser schickt nur Text; wer daraus eine Person wird,
|
||
loest erwaehnungenFinden() gegen die TEILNEHMER DES RAUMS
|
||
auf. So kann niemand jemanden ansprechen, der gar nicht dabei
|
||
ist, und ein von Hand getipptes "@Anna" wirkt genauso wie
|
||
eines aus der Auswahlliste.
|
||
|
||
ON DELETE CASCADE an beiden Enden, wie bei den Reaktionen:
|
||
Verschwindet die Nachricht oder der Mensch, geht der Eintrag
|
||
mit. Eine Erwaehnung ohne Nachricht waere ein Hinweis, der
|
||
nirgendwohin fuehrt.
|
||
|
||
Der zusammengesetzte Primaerschluessel sagt "jede Person hoechstens
|
||
einmal je Nachricht": Wer dreimal "@Anna" in einen Satz
|
||
schreibt, spricht sie trotzdem nur einmal an. */
|
||
CREATE TABLE IF NOT EXISTS chat_erwaehnungen (
|
||
nachricht_id INTEGER NOT NULL REFERENCES chat_nachrichten(id) ON DELETE CASCADE,
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
PRIMARY KEY (nachricht_id, person_id)
|
||
);
|
||
/* Die Frage, die im Betrieb gestellt wird, lautet "bin ICH
|
||
irgendwo erwaehnt?" -- deshalb liegt person_id vorn. */
|
||
CREATE INDEX IF NOT EXISTS idx_chat_erwaehnungen_person
|
||
ON chat_erwaehnungen (person_id, nachricht_id);
|
||
|
||
/* DIE GIF-KISTE DES RUDELS (23.09.2026).
|
||
|
||
Filipe: "dan will ich auch dass man im chat auch
|
||
sprachnachrichten und gifts reinschicken kann. also nur die
|
||
modis rechte linke hand und dogfather."
|
||
|
||
WARUM EINE EIGENE KISTE UND KEIN ANSCHLUSS AN GIPHY ODER
|
||
TENOR: Beide brauchen ein Konto, einen Schluessel und schicken
|
||
bei jedem Tastendruck mit, wonach hier gesucht wird -- an eine
|
||
fremde Firma, aus einem Arbeitsplatz heraus, in dem sonst
|
||
nichts nach aussen geht. Dazu kosten sie ab einer Menge Geld.
|
||
Beides steht quer zu dem, wie dieses Haus gebaut ist.
|
||
|
||
Die Kiste kostet nichts, laeuft auf demselben Server wie alles
|
||
andere, und sie wird mit der Zeit besser statt schlechter: Was
|
||
das Team hineinlegt, hat das Team ausgesucht. Ein fremder
|
||
Katalog mit zehn Millionen Eintraegen ist nicht mehr Auswahl,
|
||
sondern weniger -- man sucht laenger und findet Fremdes.
|
||
|
||
WARUM EINE EIGENE TABELLE UND NICHT EINFACH ALTE NACHRICHTEN
|
||
DURCHSUCHEN: Ein GIF in der Kiste ist etwas anderes als ein
|
||
GIF, das jemand einmal geschickt hat. Die Kiste ist eine
|
||
Entscheidung ("das wollen wir wieder benutzen"), der Verlauf
|
||
ist eine Geschichte. Und ein Verlauf laesst sich wegraeumen --
|
||
die Kiste duerfte davon nicht leer werden.
|
||
|
||
"pruef" IST DER INHALTSSCHLUESSEL (SHA-256 der Datei). Er
|
||
beantwortet "haben wir das schon?" richtig, naemlich am
|
||
Inhalt: Dasselbe GIF unter drei Namen ist dreimal dieselbe
|
||
Kachel in der Auswahl. Ueber den Namen zu vergleichen waere
|
||
die naheliegende Abkuerzung -- und sie versagt genau dort, wo
|
||
Dateien "giphy.gif", "giphy(1).gif", "download.gif" heissen,
|
||
also fast immer.
|
||
|
||
KEIN ON DELETE CASCADE AUF personen: Wer das Haus verlaesst,
|
||
nimmt die GIFs des Teams nicht mit. "von_id" wird null, die
|
||
Kachel bleibt. */
|
||
/* BEWERBUNGEN AUF EINE VORLAGE (23.09.2026).
|
||
|
||
Filipe: "die modis sollen bei all diesen voschlaegen auch nur
|
||
bewerben koennen. die aufgaben aus der vorlage, da sollen die
|
||
modis sich nur bewerben koennen und nur dogfather und die
|
||
rechte hand sollen annehmen oder ablehnen koennen, mit einem
|
||
text als notiz."
|
||
|
||
WARUM NICHT DIE VORHANDENE BEWERBUNG AUS aufgaben_zuteilung:
|
||
Die beantwortet "wie steht DIESE Aufgabe bei DIESEM Menschen"
|
||
-- sie braucht also eine Aufgabe, die es gibt. Hier gibt es
|
||
noch keine. Es ist eine Bitte um etwas, das erst entstehen
|
||
soll.
|
||
|
||
Der naheliegende Weg waere gewesen, beim Bewerben gleich die
|
||
Aufgabe anzulegen und die vorhandene Bewerbung daranzuhaengen.
|
||
Dann steht nach zwoelf Absagen zwoelfmal Arbeit auf dem Brett,
|
||
die niemand bestellt hat -- und um das zu vermeiden, muesste
|
||
das Ablehnen Aufgaben wieder LOESCHEN. Loeschen als Nebenwirkung
|
||
einer Absage ist genau die Sorte Regel, die irgendwann das
|
||
Falsche trifft.
|
||
|
||
Also: Die Aufgabe entsteht erst mit der Zusage. Bis dahin gibt
|
||
es nur diese Zeile.
|
||
|
||
DIE WORTE SIND DIESELBEN WIE DRUEBEN (zustand, entscheid_text,
|
||
entschieden_von, entschieden_am). Zwei Namen fuer dieselbe
|
||
Sache waeren zwei Sprachen im selben Haus.
|
||
|
||
"aufgabe_id" steht nach der Zusage darin -- damit laesst sich
|
||
von der Bewerbung aus zeigen, was aus ihr geworden ist. Kein
|
||
Fremdschluessel: Wird die Aufgabe spaeter geloescht, bleibt die
|
||
Bewerbung als Vorgang stehen; sie hat stattgefunden.
|
||
|
||
ON DELETE CASCADE auf personen: Wer das Haus verlaesst,
|
||
hinterlaesst keine Bewerbung, auf die jemand noch antworten
|
||
soll. */
|
||
CREATE TABLE IF NOT EXISTS vorlagen_bewerbungen (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
vorlage TEXT NOT NULL,
|
||
kategorie TEXT,
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
text TEXT,
|
||
zustand TEXT NOT NULL DEFAULT 'beworben',
|
||
entscheid_text TEXT,
|
||
entschieden_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
entschieden_am TEXT,
|
||
aufgabe_id INTEGER,
|
||
erstellt TEXT NOT NULL
|
||
);
|
||
/* EINMAL BEWERBEN, NICHT DREIMAL -- und zwar von der Datenbank
|
||
durchgesetzt, nicht von einer Abfrage davor, die man vergessen
|
||
kann.
|
||
|
||
NUR AUF OFFENE: Wer abgelehnt wurde, darf sich spaeter wieder
|
||
bewerben (Lage aendert sich, Woche aendert sich). Ein Index
|
||
ueber ALLE Zustaende haette genau das verboten. */
|
||
CREATE UNIQUE INDEX IF NOT EXISTS idx_vorlagen_bewerbung_offen
|
||
ON vorlagen_bewerbungen (vorlage, person_id) WHERE zustand = 'beworben';
|
||
/* Die Frage im Betrieb lautet "was wartet gerade auf Antwort?" */
|
||
CREATE INDEX IF NOT EXISTS idx_vorlagen_bewerbung_zustand
|
||
ON vorlagen_bewerbungen (zustand, id);
|
||
|
||
CREATE TABLE IF NOT EXISTS chat_gifs (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
datei TEXT NOT NULL,
|
||
name TEXT NOT NULL,
|
||
pruef TEXT NOT NULL UNIQUE,
|
||
typ TEXT NOT NULL,
|
||
groesse INTEGER NOT NULL,
|
||
breite INTEGER,
|
||
hoehe INTEGER,
|
||
von_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
erstellt TEXT NOT NULL
|
||
);
|
||
/* Gezeigt wird "das Neueste zuerst" -- wer gerade etwas
|
||
hineingelegt hat, findet es oben. */
|
||
CREATE INDEX IF NOT EXISTS idx_chat_gifs_neu ON chat_gifs (id DESC);
|
||
|
||
/* REAKTIONEN (14.09.2026).
|
||
|
||
Filipe: "ich will dass man auf die nachrichten auch reagieren
|
||
kann mit einem emoji, die nachricht selbst ohne zu antworten."
|
||
|
||
DER SCHLUESSEL IST DIE ANTWORT AUF "WIE OFT?": Eine Person darf
|
||
dieselbe Nachricht mit VERSCHIEDENEN Zeichen bedenken, aber
|
||
jedes nur EINMAL. Genau das sagt der zusammengesetzte
|
||
Primaerschluessel -- und zwar so, dass die Datenbank es
|
||
durchsetzt und nicht eine Abfrage davor, die man vergessen
|
||
kann. Ein zweiter Klick auf dasselbe Zeichen nimmt es zurueck.
|
||
|
||
ON DELETE CASCADE an beiden Enden: Wird die Nachricht geloescht
|
||
oder der Mensch entfernt, geht die Reaktion mit. Eine Reaktion
|
||
ohne Nachricht waere eine Zeile, die niemand je wiederfindet.
|
||
|
||
WARUM KEIN ZAEHLER IN chat_nachrichten: Eine mitgefuehrte Zahl
|
||
muesste bei jedem Hin und Her stimmen -- und sie ist genau die
|
||
Art Angabe, die irgendwann nicht mehr stimmt und die niemand
|
||
nachrechnet. Gezaehlt wird beim Lesen. */
|
||
CREATE TABLE IF NOT EXISTS chat_reaktionen (
|
||
nachricht_id INTEGER NOT NULL REFERENCES chat_nachrichten(id) ON DELETE CASCADE,
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
zeichen TEXT NOT NULL,
|
||
wann TEXT NOT NULL,
|
||
PRIMARY KEY (nachricht_id, person_id, zeichen)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_chat_reaktionen_nachricht
|
||
ON chat_reaktionen (nachricht_id);
|
||
|
||
/* FAVORITEN (14.09.2026).
|
||
|
||
Filipe: "man soll auch auswaehlen koennen, also paar als
|
||
vorschlaege mit der moeglichkeit andere auszuwaehlen und als
|
||
favoriten zu speichern."
|
||
|
||
AM MENSCHEN, NICHT AM GERAET. Im Browser abgelegt waeren sie
|
||
am Handy andere als am Rechner -- und beim naechsten Loeschen
|
||
der Seitendaten weg. Wer sich etwas merkt, will es ueberall
|
||
wiederfinden.
|
||
|
||
Die Spalte "platz" haelt die REIHENFOLGE. Ohne sie waere die
|
||
Vorschlagszeile bei jedem Laden anders sortiert, und man
|
||
greift daneben, weil das Zeichen gewandert ist. */
|
||
CREATE TABLE IF NOT EXISTS chat_reaktion_favoriten (
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
zeichen TEXT NOT NULL,
|
||
platz INTEGER NOT NULL,
|
||
PRIMARY KEY (person_id, zeichen)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_chat_favoriten_person
|
||
ON chat_reaktion_favoriten (person_id, platz);
|
||
|
||
/* Creator-Profil (Onboarding aus dem Konzept). Eine Zeile je
|
||
Creator, entsteht erst beim ersten Speichern.
|
||
admin_notiz ist bewusst Teil dieser Tabelle, wird aber nur an
|
||
das Management ausgeliefert -- siehe workspace-profil.js. */
|
||
CREATE TABLE IF NOT EXISTS profile (
|
||
person_id INTEGER PRIMARY KEY REFERENCES personen(id) ON DELETE CASCADE,
|
||
handles TEXT,
|
||
nische TEXT,
|
||
live_zeiten TEXT,
|
||
technik TEXT,
|
||
ziel_live TEXT,
|
||
ziel_content TEXT,
|
||
ziel_community TEXT,
|
||
ziel_technik TEXT,
|
||
plan_start TEXT,
|
||
plan_prio1 TEXT,
|
||
plan_prio2 TEXT,
|
||
plan_prio3 TEXT,
|
||
naechster_review TEXT,
|
||
admin_notiz TEXT,
|
||
geaendert TEXT,
|
||
geaendert_von INTEGER REFERENCES personen(id) ON DELETE SET NULL
|
||
);
|
||
|
||
/* Termine, Calls und Reviews. Der Beginn steht als lokale Zeit
|
||
(JJJJ-MM-TTThh:mm) -- genau so, wie ein datetime-local-Feld sie
|
||
liefert. Bewusst ohne Zeitzone: Alle Beteiligten sitzen in
|
||
derselben, und eine falsch umgerechnete Uhrzeit waere schlimmer
|
||
als gar keine Umrechnung. */
|
||
CREATE TABLE IF NOT EXISTS termine (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
titel TEXT NOT NULL,
|
||
beschreibung TEXT,
|
||
art TEXT NOT NULL DEFAULT 'termin'
|
||
CHECK (art IN ('termin','call','review','bigmatch','turnier','special','collab','raid','charity')),
|
||
beginn TEXT NOT NULL,
|
||
dauer_min INTEGER NOT NULL DEFAULT 30,
|
||
ort TEXT,
|
||
creator_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
teilnehmer_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
erledigt INTEGER NOT NULL DEFAULT 0,
|
||
erstellt TEXT NOT NULL,
|
||
erstellt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_termine_beginn ON termine (beginn);
|
||
|
||
/* MEHRERE TEILNEHMER AN EINEM TERMIN (05.09.2026) ------------------
|
||
|
||
Wunsch Filipe: *"wenn ich im kalender was eintrage will ich dass
|
||
ich auch 2 leute markieren kann mit denen der call ist."*
|
||
|
||
Bis hierher hatte ein Termin GENAU EIN Gegenueber
|
||
(termine.teilnehmer_id). Fuer ein Gespraech zu dritt musste man
|
||
zwei Termine anlegen -- und hatte damit zwei Wahrheiten ueber
|
||
dieselbe halbe Stunde.
|
||
|
||
Gebaut nach demselben Muster wie datei_personen weiter unten:
|
||
eine eigene kleine Tabelle statt weiterer Spalten. Zwei Spalten
|
||
"teilnehmer2_id", "teilnehmer3_id" waeren beim vierten Menschen
|
||
wieder am Ende, und jede Abfrage muesste alle einzeln
|
||
aufzaehlen.
|
||
|
||
WICHTIG -- teilnehmer_id BLEIBT und behaelt seine Bedeutung:
|
||
Es ist das HAUPT-Gegenueber, mit dem der Termin vereinbart
|
||
wurde. Diese Tabelle sagt, WER SONST NOCH dabei ist. Beim
|
||
Umstellen wird der vorhandene Wert mit uebernommen, damit die
|
||
Teilnehmerliste von Anfang an vollstaendig ist und keine
|
||
Abfrage zwei Stellen zusammensuchen muss.
|
||
|
||
An dieser Tabelle haengt die SICHTBARKEIT (siehe
|
||
termineSichtbar): Wer hier steht, sieht den Termin. Ein
|
||
vergessener Eintrag ist deshalb kein Schoenheitsfehler,
|
||
sondern ein Termin, den jemand nicht findet. */
|
||
CREATE TABLE IF NOT EXISTS termin_teilnehmer (
|
||
termin_id INTEGER NOT NULL REFERENCES termine(id) ON DELETE CASCADE,
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
PRIMARY KEY (termin_id, person_id)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_termin_teilnehmer ON termin_teilnehmer (person_id);
|
||
|
||
/* =================================================================
|
||
DER WECKER (07.09.2026)
|
||
|
||
Wunsch Filipe: "wie so ein wecker, den man auch in den
|
||
eintraegen aktivieren oder ausschalten kann, den soll man sogar
|
||
so einstellen koennen, dass er einen auch mehrmals informiert,
|
||
einmal eine woche vorher, einmal drei tage vorher und einmal am
|
||
tag selber. das soll jeder fuer sich selbst einstellen koennen."
|
||
|
||
EINE ZEILE IST EIN WECKER. Nicht ein Feld am Termin mit einer
|
||
Liste darin: Mehrere Vorlaufzeiten je Termin sind der ganze
|
||
Zweck, und "jeder fuer sich" heisst, dass zwei Personen am
|
||
SELBEN Termin verschiedene Wecker haben. Beides zusammen ist
|
||
eine klassische n:m-Beziehung, und die gehoert in eine eigene
|
||
Tabelle. Ein Feld mit kommagetrennten Zahlen waere beim ersten
|
||
"zeig mir alle faelligen Wecker" nicht mehr abfragbar.
|
||
|
||
Die Spalte minuten_vorher statt eines Zeitpunkts: Ein Zeitpunkt muesste
|
||
bei JEDER Terminverschiebung nachgezogen werden -- und genau
|
||
das vergisst man. Ein Abstand rechnet sich beim Wecken aus dem
|
||
aktuellen Beginn und ist damit immer richtig, egal wie oft der
|
||
Termin wandert.
|
||
|
||
ON DELETE CASCADE an beiden Seiten: Ein Wecker ohne Termin
|
||
weckt niemanden, und ein Wecker ohne Person auch nicht.
|
||
Zusammengesetzter Primaerschluessel: Dieselbe Person kann
|
||
denselben Abstand nicht zweimal setzen -- sonst klingelt es
|
||
doppelt, und das merkt man erst nachts.
|
||
|
||
Recherche (Google Calendar, Outlook, Morgen): Fuenf Wecker je
|
||
Termin sind das uebliche Maximum, und die verbreitete Empfehlung
|
||
fuer Wichtiges lautet "eine Woche, ein Tag, am Tag selbst".
|
||
Genau diese Staffel bietet die Oberflaeche an. */
|
||
CREATE TABLE IF NOT EXISTS termin_wecker (
|
||
termin_id INTEGER NOT NULL REFERENCES termine(id) ON DELETE CASCADE,
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
minuten_vorher INTEGER NOT NULL,
|
||
gesetzt TEXT NOT NULL,
|
||
PRIMARY KEY (termin_id, person_id, minuten_vorher)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_termin_wecker_person
|
||
ON termin_wecker (person_id);
|
||
|
||
/* Dasselbe fuer die Wiederholungen. Eine Serie ist eine Regel --
|
||
die Teilnehmer gehoeren zur Regel, nicht zur einzelnen
|
||
Auspraegung, sonst muesste man sie jede Woche neu eintragen.
|
||
Der Nachfueller kopiert sie beim Anlegen jedes Termins hierher
|
||
hinueber (siehe workspace-serien.js). */
|
||
CREATE TABLE IF NOT EXISTS serie_teilnehmer (
|
||
serie_id INTEGER NOT NULL REFERENCES termin_serien(id) ON DELETE CASCADE,
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
PRIMARY KEY (serie_id, person_id)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_serie_teilnehmer ON serie_teilnehmer (person_id);
|
||
|
||
/* Wiederkehrende Termine (02.09.2026) --------------------------------
|
||
|
||
Eine Serie ist eine REGEL, kein Termin: "jeden Dienstag um 18:00
|
||
Uhr, bis ich es abstelle". Aus ihr entstehen echte Zeilen in
|
||
der Tabelle termine.
|
||
|
||
Warum echte Zeilen und nicht bloss gerechnete Ausprägungen:
|
||
ACHT andere Stellen lesen termine direkt -- Calls, Protokolle,
|
||
Berichte, Hinweise/Ampel, Suche, Aufgaben ("nächster Termin"),
|
||
Übersicht, Startseite. Eine nur im Kalender gerechnete
|
||
Wiederholung wäre in all diesen Ansichten unsichtbar gewesen,
|
||
ohne dass es jemandem auffällt, und beim geplanten ICS-Abo
|
||
ebenfalls. Der Nachfüller hält stattdessen einen Horizont echt
|
||
gefüllt -- damit sieht jedes Modul die Wiederholung, ohne dass
|
||
dort eine einzige Zeile geändert werden musste.
|
||
|
||
uhrzeit statt beginn: Eine Regel kennt keinen Zeitpunkt, nur
|
||
eine Uhrzeit und einen Takt. Gerechnet wird auf reinen
|
||
Datumstexten -- deshalb bleibt 18:00 auch über die
|
||
Zeitumstellung hinweg 18:00 und wandert nicht auf 17:00. */
|
||
CREATE TABLE IF NOT EXISTS termin_serien (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
titel TEXT NOT NULL,
|
||
beschreibung TEXT,
|
||
art TEXT NOT NULL DEFAULT 'termin'
|
||
CHECK (art IN ('termin','call','review','bigmatch','turnier','special','collab','raid','charity')),
|
||
takt TEXT NOT NULL
|
||
CHECK (takt IN ('taeglich','werktags','woechentlich',
|
||
'zweiwoechentlich','monatlich_datum',
|
||
'monatlich_letzter','monatlich_wochentag',
|
||
'jaehrlich')),
|
||
start_tag TEXT NOT NULL,
|
||
uhrzeit TEXT NOT NULL,
|
||
ende_tag TEXT,
|
||
dauer_min INTEGER NOT NULL DEFAULT 30,
|
||
ort TEXT,
|
||
creator_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
teilnehmer_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
aktiv INTEGER NOT NULL DEFAULT 1,
|
||
erstellt TEXT NOT NULL,
|
||
erstellt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
geaendert TEXT
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_serien_aktiv ON termin_serien (aktiv);
|
||
|
||
/* Ausgelassene Tage einer Serie. Wer eine einzelne Ausprägung
|
||
löscht, meint "dieses eine Mal nicht" -- ohne diese Tabelle
|
||
legte der Nachfüller sie beim nächsten Aufruf wieder an, und der
|
||
gelöschte Termin wäre kommentarlos zurück. */
|
||
CREATE TABLE IF NOT EXISTS termin_serien_aus (
|
||
serie_id INTEGER NOT NULL REFERENCES termin_serien(id) ON DELETE CASCADE,
|
||
tag TEXT NOT NULL,
|
||
PRIMARY KEY (serie_id, tag)
|
||
);
|
||
|
||
/* Dateiablage. Auf der Platte traegt jede Datei einen erzeugten
|
||
Zufallsnamen (name_datei), der Originalname steht nur hier.
|
||
Dadurch kann ein Dateiname weder Pfade verlassen noch etwas
|
||
ueberschreiben. Der Freigabe-Ablauf aus dem Konzept steckt in
|
||
status: entwurf -> review -> freigegeben. */
|
||
CREATE TABLE IF NOT EXISTS dateien (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name_original TEXT NOT NULL,
|
||
name_datei TEXT NOT NULL UNIQUE,
|
||
groesse INTEGER NOT NULL,
|
||
typ TEXT,
|
||
status TEXT NOT NULL DEFAULT 'entwurf'
|
||
CHECK (status IN ('entwurf','review','freigegeben')),
|
||
notiz TEXT,
|
||
creator_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
hochgeladen_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
erstellt TEXT NOT NULL,
|
||
geaendert TEXT
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_dateien_creator ON dateien (creator_id);
|
||
|
||
/* Eintraege der Phase-2-Bereiche (LIVE, Content, Technik,
|
||
Community, Schutz). Eine gemeinsame Tabelle statt fuenf fast
|
||
gleicher: Die Bereiche unterscheiden sich nur in den erlaubten
|
||
Arten und darin, ob Bewertung oder Dringlichkeit dazugehoeren --
|
||
das steht in workspace-bereiche.js, nicht in der Datenbank. */
|
||
CREATE TABLE IF NOT EXISTS eintraege (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
bereich TEXT NOT NULL
|
||
CHECK (bereich IN ('live','content','technik','community','schutz','agentur','ideen')),
|
||
art TEXT NOT NULL,
|
||
titel TEXT NOT NULL,
|
||
text TEXT,
|
||
datum TEXT NOT NULL,
|
||
bewertung INTEGER CHECK (bewertung IS NULL OR (bewertung BETWEEN 1 AND 5)),
|
||
dringlichkeit TEXT NOT NULL DEFAULT 'mittel'
|
||
CHECK (dringlichkeit IN ('hoch','mittel','niedrig')),
|
||
status TEXT NOT NULL DEFAULT 'offen'
|
||
CHECK (status IN ('offen','erledigt')),
|
||
creator_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
erstellt TEXT NOT NULL,
|
||
erstellt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
geaendert TEXT
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_eintraege_bereich ON eintraege (bereich, creator_id);
|
||
/* Die Tabellen des Rudels folgen weiter unten, gleich nachdem
|
||
dieser Bauplan durch ist -- sie haengen an eintraege und
|
||
personen und koennen deshalb nicht davor stehen. Ihr SQL steht
|
||
in treff-tabellen.js, samt Begruendung je Tabelle. */
|
||
|
||
/* Wer darf welche Datei sehen. Bisher hatte eine Datei genau
|
||
EINEN Bereich (dateien.creator_id) -- damit liess sich eine
|
||
Datei nicht zweien geben, ohne sie zweimal hochzuladen.
|
||
Diese Tabelle erlaubt beliebig viele Empfaenger, Creator wie
|
||
Scouts. creator_id bleibt bestehen: Es sagt weiterhin, zu
|
||
wessen BEREICH eine Datei gehoert, waehrend hier steht, WER sie
|
||
sehen darf. Zwei verschiedene Fragen. */
|
||
CREATE TABLE IF NOT EXISTS datei_personen (
|
||
datei_id INTEGER NOT NULL REFERENCES dateien(id) ON DELETE CASCADE,
|
||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
PRIMARY KEY (datei_id, person_id)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_datei_personen ON datei_personen (person_id);
|
||
|
||
/* Wissens-Bibliothek: Anleitungen als PDF, hinter dem Login.
|
||
Jede PDF hat GENAU EINE Hauptkategorie -- so gibt es sie nur
|
||
einmal. Zusaetzliche Themen laufen ueber Tags, damit dieselbe
|
||
Anleitung trotzdem ueber mehrere Suchbegriffe auffindbar ist.
|
||
Die Kategorien selbst stehen im Code (workspace-wissen.js),
|
||
nicht hier: Sie sind eine bewusste Gliederung, keine Nutzdaten. */
|
||
CREATE TABLE IF NOT EXISTS wissen (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
titel TEXT NOT NULL,
|
||
beschreibung TEXT,
|
||
kategorie TEXT NOT NULL,
|
||
stufe TEXT NOT NULL DEFAULT 'einsteiger'
|
||
CHECK (stufe IN ('einsteiger','fortgeschritten','profi')),
|
||
geraet TEXT NOT NULL DEFAULT 'egal'
|
||
CHECK (geraet IN ('pc','handy','beide','egal')),
|
||
tags TEXT,
|
||
name_original TEXT NOT NULL,
|
||
name_datei TEXT NOT NULL,
|
||
groesse INTEGER NOT NULL,
|
||
wichtig INTEGER NOT NULL DEFAULT 0,
|
||
veroeffentlicht TEXT NOT NULL,
|
||
aktualisiert TEXT,
|
||
erstellt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_wissen_kat ON wissen (kategorie);
|
||
|
||
/* =================================================================
|
||
LEISTUNG — die Zahlen, an denen die Betreuung haengt (06.09.2026)
|
||
|
||
DER GROSSE BEFUND aus dem Plan vom 31.08.2026: Der Workspace
|
||
organisiert ARBEIT sehr gut, aber es gibt keine einzige Zahl
|
||
ueber das GESCHAEFT. Damit haengt die ganze Betreuung in der
|
||
Luft -- der Start-Check bewertet ohne zu messen, der Report
|
||
fragt "was hat funktioniert" ohne Beleg, das 90-Tage-Ziel ist
|
||
ein Satz statt eines Fortschritts.
|
||
|
||
EINE ZEILE JE CREATOR UND TAG. Tag und nicht Woche: Feineres
|
||
laesst sich immer zusammenfassen, Groeberes nie aufteilen. Wer
|
||
mit Wochenwerten anfaengt, kann spaeter nie mehr sagen, ob der
|
||
Einbruch am Dienstag oder am Wochenende lag.
|
||
|
||
WELCHE FELDER -- und warum genau diese (Recherche 31.08. und
|
||
06.09.2026, TikTok LIVE Backstage):
|
||
|
||
diamanten die Zahl, an der TikTok und die Agentur den
|
||
Erfolg messen
|
||
dauer_min Grundlage fuer "gueltige Tage"/"aktive Stunden"
|
||
gueltiger_tag der Zaehler, an dem Ranghochstufungen haengen
|
||
zuschauer_avg sagt, ob es TRAEGT
|
||
zuschauer_max sagt, was MOEGLICH waere
|
||
verweildauer_s DIE LEITZAHL (Recherche 06.09.2026): 2026
|
||
haengt der Algorithmus alles daran, und sie
|
||
gehoert als Betriebskennzahl behandelt --
|
||
sie soll die naechste Runde steuern, nicht
|
||
die letzte erklaeren
|
||
schenker wie BREIT die Unterstuetzung steht, nicht nur
|
||
wie hoch. Ein Grossspender ist ein Risiko,
|
||
zwanzig kleine sind ein Fundament
|
||
follower_neu waechst der Kanal oder dreht er sich im Kreis
|
||
notiz ein Satz Kontext ("PK gegen X", "krank")
|
||
|
||
WARUM DIE NOTIZ DAZUGEHOERT: Ohne sie sieht man in drei Monaten
|
||
einen Einbruch und weiss nicht mehr, dass die Person Grippe
|
||
hatte. Zahlen ohne Umstaende fuehren in die Irre.
|
||
|
||
PRIMAERSCHLUESSEL (creator_id, tag): Derselbe Creator am
|
||
selben Tag kann nur EINE Zeile haben. Das ist die halbe Miete
|
||
beim Import -- wer eine Datei zweimal einliest, ueberschreibt
|
||
dann, statt zu verdoppeln. Verdoppelte Zahlen sind schlimmer
|
||
als fehlende: Sie sehen richtig aus. */
|
||
CREATE TABLE IF NOT EXISTS leistung (
|
||
creator_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
tag TEXT NOT NULL, /* JJJJ-MM-TT, Ortszeit */
|
||
diamanten INTEGER,
|
||
dauer_min INTEGER,
|
||
gueltiger_tag INTEGER NOT NULL DEFAULT 0,
|
||
zuschauer_avg INTEGER,
|
||
zuschauer_max INTEGER,
|
||
verweildauer_s INTEGER, /* Sekunden, die Leitzahl */
|
||
schenker INTEGER,
|
||
follower_neu INTEGER,
|
||
notiz TEXT,
|
||
erfasst TEXT NOT NULL,
|
||
erfasst_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
/* Woher der Wert kommt. Wichtig, wenn eine Zahl seltsam
|
||
aussieht: von Hand getippt oder aus dem Export? */
|
||
quelle TEXT NOT NULL DEFAULT 'hand'
|
||
CHECK (quelle IN ('hand','import')),
|
||
PRIMARY KEY (creator_id, tag)
|
||
);
|
||
|
||
/* ZAHLEN FUER EINEN ZEITRAUM (14.09.2026).
|
||
|
||
Filipe: "ich will dass das auch so geht, mach dass es
|
||
funktioniert ... sieh zu dass die excel auch so durch geht."
|
||
|
||
Backstage gibt eine Ausgabe fuer einen ZEITRAUM heraus --
|
||
"2026-09-01 ~ 2026-09-13", eine Zeile je Creator, alle Zahlen
|
||
aufsummiert. Bis heute wurde so eine Zeile abgewiesen, weil
|
||
daraus kein Tageswert wird.
|
||
|
||
DREI WEGE WAEREN MOEGLICH GEWESEN, und zwei davon waeren
|
||
Zahlenfaelschung:
|
||
* auf den ersten Tag schreiben -> 13 Tage auf einem Tag,
|
||
sieht richtig aus, ist es nicht;
|
||
* gleichmaessig verteilen -> erfundene Tage, die es nie gab.
|
||
Der dritte ist dieser: den Zeitraum ALS Zeitraum ablegen. Was
|
||
drinsteht, ist dann wahr -- und was es nicht sagt (welcher Tag
|
||
wie lief), behauptet es auch nicht.
|
||
|
||
EIGENE TABELLE UND NICHT EIN FELD IN "leistung": Dort ist der
|
||
Schluessel (creator, tag). Ein Zeitraum ab dem 1. September
|
||
wuerde mit dem ECHTEN 1. September zusammenstossen -- und eine
|
||
der beiden Zahlen waere weg. Getrennt kann keines das andere
|
||
ueberschreiben, und die Wochenzahlen bleiben, was sie sind:
|
||
aus Tagen gerechnet.
|
||
|
||
DER SCHLUESSEL IST (creator, von, bis): Dieselbe Datei zweimal
|
||
einzulesen ersetzt denselben Zeitraum, statt ihn zu
|
||
verdoppeln. */
|
||
CREATE TABLE IF NOT EXISTS leistung_zeitraum (
|
||
creator_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
von TEXT NOT NULL,
|
||
bis TEXT NOT NULL,
|
||
tage INTEGER NOT NULL,
|
||
diamanten INTEGER,
|
||
dauer_min INTEGER,
|
||
gueltige_tage INTEGER,
|
||
zuschauer_avg INTEGER,
|
||
zuschauer_max INTEGER,
|
||
verweildauer_s INTEGER,
|
||
schenker INTEGER,
|
||
follower_neu INTEGER,
|
||
erfasst TEXT NOT NULL,
|
||
erfasst_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
PRIMARY KEY (creator_id, von, bis)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_leistung_zeitraum_creator
|
||
ON leistung_zeitraum (creator_id, bis DESC);
|
||
CREATE INDEX IF NOT EXISTS idx_leistung_tag ON leistung (tag DESC);
|
||
|
||
/* ALLES, WAS IN DER DATEI STAND (16.09.2026)
|
||
|
||
Filipe: "ich will dass alles von der excel datei genommen wird
|
||
... wenn ich dir runter lade soll alles notiert und angezeigt
|
||
werden."
|
||
|
||
Seine echte Backstage-Ausgabe hat 41 SPALTEN. Die Tabelle
|
||
darueber nimmt acht davon. Dreiunddreissig Spalten -- Zahlen
|
||
vom letzten Monat, Prozentwerte, Matches, Multi-Gast-LIVEs,
|
||
Fanclub, Graduierungs- und Stufenstatus -- wurden beim Einlesen
|
||
weggeworfen, ohne dass irgendwo stand, dass es sie gab.
|
||
|
||
WARUM NICHT 33 WEITERE SPALTEN OBEN? Weil das eine ABGESCHRIEBENE
|
||
LISTE waere, und die hat in diesem Haus schon zweimal Daten
|
||
gekostet (siehe die Notiz zur Agentur-Umstellung). Sie waere
|
||
ausserdem beim naechsten Backstage-Update falsch: TikTok
|
||
benennt Spalten um und fuegt welche hinzu, ohne jemanden zu
|
||
fragen.
|
||
|
||
EINE ZEILE JE SPALTE, und der NAME ist der Schluessel. Damit
|
||
gilt: Was in der Datei steht, steht danach in der Datenbank --
|
||
auch eine Spalte, die es heute noch gar nicht gibt. Nichts zu
|
||
pflegen, nichts zu vergessen.
|
||
|
||
Die Spalte nr IST DIE REIHENFOLGE DER DATEI, nicht die Identitaet. Wer
|
||
die Reihenfolge zum Schluessel machte, verwechselte zwei
|
||
Spalten, sobald Backstage eine dazwischenschiebt.
|
||
|
||
BEIDES WIRD GESPEICHERT: der Text woertlich, wie er dastand
|
||
(daraus kann man immer noch alles herleiten), und die Zahl, wenn
|
||
es eine ist. Nur die Zahl zu behalten hiesse, "Nicht graduiert"
|
||
und "6Std. 24Min. 7Sek." zu verlieren; nur den Text zu behalten
|
||
hiesse, nicht rechnen zu koennen. */
|
||
CREATE TABLE IF NOT EXISTS leistung_zeitraum_feld (
|
||
creator_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
von TEXT NOT NULL,
|
||
bis TEXT NOT NULL,
|
||
nr INTEGER NOT NULL,
|
||
name TEXT NOT NULL,
|
||
text TEXT,
|
||
zahl REAL,
|
||
PRIMARY KEY (creator_id, von, bis, name)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_leistung_zfeld
|
||
ON leistung_zeitraum_feld (creator_id, von, bis, nr);
|
||
|
||
/* ZIELE je Creator. Aus dem 90-Tage-Plan, den es im Profil schon
|
||
als FREITEXT gibt -- daraus wird hier eine pruefbare Groesse.
|
||
"Mehr Reichweite" ist kein Ziel, "4 LIVE-Tage pro Woche" ist
|
||
eines.
|
||
|
||
Eine Zeile je Creator, nicht je Ziel und Zeitraum: Ein Ziel,
|
||
das sich woechentlich aendert, ist keines. Wer es anpasst,
|
||
ueberschreibt -- der Verlauf steht ohnehin in der Tabelle
|
||
"leistung". */
|
||
CREATE TABLE IF NOT EXISTS leistung_ziele (
|
||
creator_id INTEGER PRIMARY KEY REFERENCES personen(id) ON DELETE CASCADE,
|
||
diamanten_woche INTEGER,
|
||
tage_woche INTEGER,
|
||
verweildauer_s INTEGER,
|
||
gesetzt TEXT NOT NULL,
|
||
gesetzt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL
|
||
);
|
||
|
||
/* Start-Check / Erstanalyse (Konzept, Seite 5). Eine Zeile je
|
||
geprueftem Punkt -- ungeprueft heisst: gar keine Zeile. So sieht
|
||
man am Bestand sofort, wie weit die Analyse ist, ohne leere
|
||
Platzhalter mitschleppen zu muessen.
|
||
Die Punkte selbst stehen im Code, nicht hier: Waeren sie
|
||
Datensaetze, koennte jeder eigene anlegen -- und zwei Creator
|
||
waeren nicht mehr vergleichbar, was der ganze Zweck ist. */
|
||
CREATE TABLE IF NOT EXISTS startcheck (
|
||
creator_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
feld TEXT NOT NULL,
|
||
punkt TEXT NOT NULL,
|
||
bewertung TEXT CHECK (bewertung IS NULL OR bewertung IN ('ok','mittel','handlung')),
|
||
notiz TEXT,
|
||
geaendert TEXT NOT NULL,
|
||
geaendert_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
PRIMARY KEY (creator_id, feld, punkt)
|
||
);
|
||
|
||
/* Betreuung: wer kuemmert sich um welchen Creator.
|
||
Scouts betreuen Creator wie das Management, aber nur die ihnen
|
||
zugeteilten -- deshalb braucht es eine ausdrueckliche Zuordnung
|
||
und keine Rollenregel. Ein Creator hat genau eine zustaendige
|
||
Person (PRIMARY KEY), damit nie unklar ist, wer gefragt ist.
|
||
Das Management sieht ohnehin alle und braucht keinen Eintrag. */
|
||
CREATE TABLE IF NOT EXISTS betreuung (
|
||
creator_id INTEGER PRIMARY KEY REFERENCES personen(id) ON DELETE CASCADE,
|
||
betreuer_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
seit TEXT NOT NULL,
|
||
gesetzt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_betreuung_betreuer ON betreuung (betreuer_id);
|
||
|
||
/* Welcher Scout gehoert zu welchem Manager (01.09.2026).
|
||
|
||
"er soll nur die Scouts sehen, die ihm zugeteilt sind."
|
||
|
||
WARUM EINE EIGENE TABELLE und nicht ein zweiter Eintrag in
|
||
betreuung: Dort heisst die Spalte creator_id, und der ganze
|
||
uebrige Code liest sie als "das ist ein Creator". Ein Scout in
|
||
dieser Spalte waere technisch moeglich und fachlich eine Luege --
|
||
betreuteIds() gaebe dann Scout-Nummern zurueck, die anderswo als
|
||
Creator behandelt wuerden. Solche Abkuerzungen raechen sich
|
||
genau dann, wenn niemand mehr weiss, dass sie getroffen wurden.
|
||
|
||
Ein Scout gehoert zu hoechstens EINEM Manager -- deshalb ist
|
||
scout_id der Schluessel. Eine Zuteilung an mehrere waere die
|
||
Frage "wer ist zustaendig?" ohne Antwort.
|
||
|
||
ON DELETE CASCADE auf beiden Seiten: Verschwindet der Scout oder
|
||
der Manager, ist die Zuteilung gegenstandslos. Sie stehenzulassen
|
||
hiesse, dass eine geloeschte Person weiter Sichtbarkeit steuert. */
|
||
CREATE TABLE IF NOT EXISTS scout_zuteilung (
|
||
scout_id INTEGER PRIMARY KEY REFERENCES personen(id) ON DELETE CASCADE,
|
||
manager_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||
seit TEXT NOT NULL,
|
||
gesetzt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_scout_zuteilung_manager
|
||
ON scout_zuteilung (manager_id);
|
||
|
||
/* Meeting-Protokoll zu einem Termin (Konzept, Seite 13).
|
||
Bewusst eine eigene Tabelle statt neuer Spalten in termine: Es
|
||
gibt hier keine Migrationen, und ein Protokoll gehoert ohnehin
|
||
nur zu einem Bruchteil der Termine. Ein Termin hat hoechstens
|
||
ein Protokoll -- daher UNIQUE. */
|
||
CREATE TABLE IF NOT EXISTS protokolle (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
termin_id INTEGER NOT NULL UNIQUE REFERENCES termine(id) ON DELETE CASCADE,
|
||
punkte TEXT,
|
||
entscheidungen TEXT,
|
||
naechster_termin_id INTEGER REFERENCES termine(id) ON DELETE SET NULL,
|
||
erstellt TEXT NOT NULL,
|
||
erstellt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
geaendert TEXT
|
||
);
|
||
|
||
/* "Jeder Call endet mit klaren To-dos, die direkt ins Board
|
||
uebernommen werden." Die To-dos leben deshalb NICHT im Protokoll,
|
||
sondern sind echte Aufgaben. Diese Tabelle merkt sich nur, aus
|
||
welchem Gespraech eine Aufgabe entstanden ist. */
|
||
CREATE TABLE IF NOT EXISTS protokoll_aufgaben (
|
||
protokoll_id INTEGER NOT NULL REFERENCES protokolle(id) ON DELETE CASCADE,
|
||
aufgabe_id INTEGER NOT NULL REFERENCES aufgaben(id) ON DELETE CASCADE,
|
||
PRIMARY KEY (protokoll_id, aufgabe_id)
|
||
);
|
||
|
||
/* Scout-CRM: die Pipeline vom ersten Fund bis zur Uebergabe.
|
||
creator_id ist gesetzt, sobald aus einem uebergebenen Lead eine
|
||
echte Person angelegt wurde -- damit bleibt nachvollziehbar, wer
|
||
einen Creator gefunden hat, auch Jahre spaeter. */
|
||
CREATE TABLE IF NOT EXISTS leads (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name TEXT NOT NULL,
|
||
plattform TEXT,
|
||
handle TEXT,
|
||
status TEXT NOT NULL DEFAULT 'neu'
|
||
CHECK (status IN ('neu','angesprochen','gespraech','interessiert','uebergeben','abgelehnt')),
|
||
prioritaet TEXT NOT NULL DEFAULT 'mittel'
|
||
CHECK (prioritaet IN ('hoch','mittel','niedrig')),
|
||
aktivitaet TEXT,
|
||
potenzial TEXT,
|
||
notizen TEXT,
|
||
letzte_nachricht TEXT,
|
||
naechster_followup TEXT,
|
||
scout_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
creator_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
erstellt TEXT NOT NULL,
|
||
erstellt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||
geaendert TEXT,
|
||
uebergeben_am TEXT
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_leads_scout ON leads (scout_id, status);
|
||
|
||
CREATE TABLE IF NOT EXISTS versuche (
|
||
ip TEXT NOT NULL,
|
||
zeitpunkt TEXT NOT NULL
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_versuche_ip ON versuche (ip, zeitpunkt);
|
||
CREATE INDEX IF NOT EXISTS idx_sitzungen_gueltig ON sitzungen (gueltig_bis);
|
||
`);
|
||
/* Die Tabellen des Treffs. Eigene Datei, weil workspace-treff.js
|
||
aus DIESER Datei importiert -- ein Importkreis waere die Folge,
|
||
und ueber einen Kreis kommen Konstanten als undefined an, ohne
|
||
dass irgendwo ein Fehler erscheint. */
|
||
treffTabellen(d);
|
||
/* Der vertrauliche Meldeweg (19.09.2026). Haengt an personen und
|
||
kommt deshalb hierher, nicht in den Bauplan darueber. */
|
||
hilfeTabellen(d);
|
||
supportTabellen(d);
|
||
umstellungen(d);
|
||
_db = d;
|
||
return d;
|
||
} catch (fehler) {
|
||
_dbFehler = fehler;
|
||
console.error("[workspace] Datenbank nicht verfügbar:", fehler?.message);
|
||
throw fehler;
|
||
}
|
||
}
|
||
|
||
/* ---------- Hilfsmittel ---------------------------------------------- */
|
||
|
||
const jetzt = () => new Date().toISOString();
|
||
|
||
/* Die echte Besucher-IP.
|
||
|
||
Warum das nötig ist: Die Kette lautet Besucher -> Cloudflare -> Caddy ->
|
||
Express. `trust proxy` steht auf 1, Express nimmt daher den letzten
|
||
Eintrag aus X-Forwarded-For -- und das ist die Adresse von Cloudflare,
|
||
nicht die des Besuchers. Folge: Im Protokoll stand bei jedem dieselbe
|
||
IP, und viel schlimmer -- ALLE Nutzer teilten sich einen einzigen
|
||
Sperr-Zähler. Ein paar Tippfehler von drei Leuten hätten den Zugang für
|
||
alle gesperrt.
|
||
|
||
Cloudflare setzt die echte Adresse in CF-Connecting-IP und überschreibt
|
||
den Kopf bei jeder Anfrage, ein Besucher kann ihn also nicht fälschen.
|
||
Einschränkung: Wer die Adresse des Servers kennt und ihn unter Umgehung
|
||
von Cloudflare direkt anspricht, könnte den Kopf frei setzen. Deshalb
|
||
wird auf die Peer-Adresse zurückgefallen, sobald der Kopf fehlt -- und
|
||
beide Werte landen im Protokoll, damit so etwas auffällt. */
|
||
export function echteIp(req) {
|
||
const cf = req.get("cf-connecting-ip");
|
||
if (cf && cf.length <= 45) return cf.trim();
|
||
return req.ip || "?";
|
||
}
|
||
|
||
function hashe(code, salt, N) {
|
||
return scryptSync(code, salt, SCRYPT.keylen,
|
||
{ N, r: SCRYPT.r, p: SCRYPT.p, maxmem: 256 * 1024 * 1024 }).toString("hex");
|
||
}
|
||
|
||
/* =====================================================================
|
||
DER SUCHSCHLUESSEL ZU EINEM CODE (09.09.2026)
|
||
|
||
Beantwortet in Mikrosekunden, WEN man pruefen soll -- nicht, ob der
|
||
Code stimmt. Das bleibt Sache von scrypt darunter.
|
||
|
||
WARUM EIN HMAC UND KEIN EINFACHER HASH: Ein einfacher Hash ueber den
|
||
Code waere ueberall gleich. Mit einem Schluessel, der nur in dieser
|
||
einen Datenbank steht, ist der Wert ausserhalb wertlos -- er laesst
|
||
sich nirgends nachschlagen und nirgends wiedererkennen.
|
||
|
||
WARUM DER SCHLUESSEL IN DIE DATENBANK GEHOERT UND NICHT IN DIE
|
||
UMGEBUNG: Eine neue Pflichtangabe in server/.env ist eine Angabe, die
|
||
beim naechsten Aufsetzen fehlen kann. Sie wuerde nicht laut scheitern,
|
||
sondern leise: Der stille Zugang fuende niemanden mehr, und die
|
||
Anmeldung saehe aus wie ein falscher Code. Aus der Datenbank kann sie
|
||
nicht verschwinden -- sie wird mitgesichert wie alles andere.
|
||
|
||
OHNE SCHLUESSEL GIBT ES KEINE ANTWORT, nicht etwa eine schlechtere:
|
||
`null` heisst "konnte nicht nachsehen", und die Anmeldung behandelt
|
||
das wie "nicht gefunden". Ein Rueckfall auf einen ungeschluesselten
|
||
Wert waere die stille Verschlechterung, die niemandem auffaellt. */
|
||
let _kennungSchluessel = null;
|
||
function kennungSchluessel() {
|
||
if (_kennungSchluessel) return _kennungSchluessel;
|
||
try {
|
||
const d = db();
|
||
const lies = () => d.prepare(
|
||
"SELECT wert FROM einstellungen WHERE schluessel = 'code_kennung_schluessel'").get()?.wert;
|
||
let wert = lies();
|
||
if (!wert) {
|
||
/* Bewusst NICHT ueber einstellungSetzen(): Das schriebe einen
|
||
Eintrag ins Protokoll, und ein Protokolleintrag ist genau die
|
||
Spur, die es hier nicht geben soll. */
|
||
d.prepare(`INSERT INTO einstellungen (schluessel, wert, geaendert, von)
|
||
VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING`)
|
||
.run("code_kennung_schluessel", randomBytes(32).toString("hex"),
|
||
new Date().toISOString());
|
||
/* Danach nochmal LESEN statt den erzeugten Wert zu benutzen:
|
||
Legen zwei Vorgaenge ihn im selben Augenblick an, gewinnt einer
|
||
-- und beide muessen mit demselben weiterrechnen. */
|
||
wert = lies();
|
||
}
|
||
_kennungSchluessel = wert || null;
|
||
return _kennungSchluessel;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
export function codeKennung(code) {
|
||
const schluessel = kennungSchluessel();
|
||
if (!schluessel || !code) return null;
|
||
return createHmac("sha256", schluessel).update(String(code)).digest("hex");
|
||
}
|
||
|
||
/* Vergleich in konstanter Zeit. Ein normaler ===-Vergleich bricht beim
|
||
ersten abweichenden Zeichen ab; aus den Laufzeitunterschieden lässt sich
|
||
ein Geheimnis Zeichen für Zeichen erraten. */
|
||
function gleich(a, b) {
|
||
const x = Buffer.from(String(a), "utf8");
|
||
const y = Buffer.from(String(b), "utf8");
|
||
if (x.length !== y.length) return false;
|
||
return timingSafeEqual(x, y);
|
||
}
|
||
|
||
const tokenHash = (t) => createHash("sha256").update(t).digest("hex");
|
||
|
||
export function protokolliere(aktion, { personId = null, rolle = null, detail = null, ip = null } = {}) {
|
||
try {
|
||
db().prepare(
|
||
"INSERT INTO protokoll (zeitpunkt, person_id, rolle, aktion, detail, ip) VALUES (?,?,?,?,?,?)"
|
||
).run(jetzt(), personId, rolle, aktion, detail, ip);
|
||
} catch { /* Ein fehlgeschlagener Protokolleintrag darf nichts blockieren. */ }
|
||
}
|
||
|
||
function zuVieleVersuche(ip) {
|
||
const grenze = new Date(Date.now() - VERSUCHE_FENSTER_MIN * 60_000).toISOString();
|
||
db().prepare("DELETE FROM versuche WHERE zeitpunkt < ?").run(grenze);
|
||
const { anzahl } = db()
|
||
.prepare("SELECT COUNT(*) AS anzahl FROM versuche WHERE ip = ? AND zeitpunkt >= ?")
|
||
.get(ip, grenze);
|
||
return anzahl >= VERSUCHE_MAX;
|
||
}
|
||
|
||
/* ---------- Sitzung ---------------------------------------------------- */
|
||
|
||
export function sitzungLesen(req) {
|
||
const token = req.cookies?.[COOKIE];
|
||
if (!token) return null;
|
||
try {
|
||
const reihe = db().prepare(`
|
||
SELECT s.gueltig_bis, p.id, p.name, p.rolle, p.aktiv
|
||
FROM sitzungen s JOIN personen p ON p.id = s.person_id
|
||
WHERE s.token_hash = ?`).get(tokenHash(token));
|
||
if (!reihe || !reihe.aktiv) return null;
|
||
if (reihe.gueltig_bis < jetzt()) {
|
||
db().prepare("DELETE FROM sitzungen WHERE token_hash = ?").run(tokenHash(token));
|
||
return null;
|
||
}
|
||
|
||
/* ==== DAS FENSTER GLEITET MIT (20.09.2026) =======================
|
||
|
||
Wer die Seite benutzt, bleibt angemeldet. Verlaengert wird erst,
|
||
wenn weniger als die HAELFTE des Fensters uebrig ist -- sonst
|
||
waere hier ein Schreibzugriff bei jedem einzelnen Aufruf, auch
|
||
bei jedem Herzschlag des Ereignisstroms.
|
||
|
||
DER KEKS MUSS MIT. Ohne ihn liefe die Zeile in der Datenbank
|
||
weiter und der Browser wuerfe den Keks trotzdem weg -- die
|
||
Person waere abgemeldet, obwohl der Server sie noch kennt. Das
|
||
ist die Sorte Fehler, die niemand findet, weil beide Seiten fuer
|
||
sich richtig aussehen.
|
||
|
||
`req.res` STATT EINES ZWEITEN ARGUMENTS: Diese Funktion wird an
|
||
26 Stellen aufgerufen -- in jedem Fachmodul einmal. Einen
|
||
Parameter zu ergaenzen hiesse, 26 Aufrufe zu aendern und beim
|
||
27. Modul daran zu denken. Express haengt die Antwort ohnehin an
|
||
die Anfrage; wo sie fehlt (etwa in einer Pruefung), wird eben
|
||
nur die Datenbank verlaengert. */
|
||
const restMs = new Date(reihe.gueltig_bis).getTime() - Date.now();
|
||
if (restMs < SITZUNG_MS / 2) {
|
||
const neuBis = new Date(Date.now() + SITZUNG_MS).toISOString();
|
||
db().prepare("UPDATE sitzungen SET gueltig_bis = ? WHERE token_hash = ?")
|
||
.run(neuBis, tokenHash(token));
|
||
try {
|
||
req.res?.cookie?.(COOKIE, token, {
|
||
httpOnly: true, secure: !!req.secure, sameSite: "lax",
|
||
path: "/workspace", maxAge: SITZUNG_MS,
|
||
});
|
||
} catch { /* kein res (z. B. in einer Pruefung) -- die Zeile reicht */ }
|
||
}
|
||
/* DIE ADRESSE ENTSCHEIDET MIT (10.09.2026).
|
||
|
||
Seit die Modi-App unter crew.dogfather-universe.com laeuft, gehoert
|
||
jede Sitzung zu genau EINER Adresse: Modis dorthin, alle anderen auf
|
||
den Workspace. Die Regel steht in crew-adresse.js und ist dort ohne
|
||
laufenden Server pruefbar.
|
||
|
||
WARUM HIER UND NICHT IN DEN MODULEN: Durch diese Funktion geht jedes
|
||
der 26 Fachmodule und jede Seitenschranke. Eine Middleware daneben
|
||
koennte man in einem neuen Modul vergessen -- und ein vergessener
|
||
Rechteschutz faellt nicht auf, weil dann alles geht.
|
||
|
||
`null` heisst hier dasselbe wie bei einer abgelaufenen Sitzung:
|
||
nicht angemeldet. Die Seitenschranke schickt zur Zugangswand, die
|
||
Schnittstellen antworten mit 401. Beides ist genau richtig -- auf
|
||
dieser Adresse ist diese Person schlicht niemand. */
|
||
if (!sitzungPasstZurAdresse(reihe.rolle, req.get?.("host"))) return null;
|
||
|
||
/* IN WELCHEM HAUS STEHT DIESE PERSON GERADE? (10.09.2026)
|
||
|
||
Filipe: "die daten von dieser seite sollen nichts mit den daten
|
||
am hut haben von der workspace seite ... die hier soll ihre
|
||
eigene daten haben und komplett von der anderen getrennt sein.
|
||
dogfather soll die daten auch auf der anderen seite sehen in der
|
||
team dogi kategorie aber auch nur er und vanvan."
|
||
|
||
Dasselbe Konto, dieselbe Datenbank -- aber je nach Adresse ein
|
||
anderer AUSSCHNITT. Auf der Adresse von Team Dogi sieht auch
|
||
DogFather nur sein Team: keine Creator, keine Scouts, keine
|
||
Agentur. Auf der Agenturadresse aendert sich nichts.
|
||
|
||
ES HAENGT AN DER PERSON UND NICHT AN EINEM ZWEITEN PARAMETER.
|
||
Die Sichtbarkeitsregeln bekommen ueberall dieselbe Person
|
||
gereicht -- `sichtbar(person)`, `sichtbareCreatorIds(person)`,
|
||
`bereicheFuer(person)`. Ein zusaetzliches Argument muesste an
|
||
ueber dreissig Aufrufstellen mitgeschleift werden, und die eine
|
||
vergessene waere das Loch. So reist die Antwort mit dem
|
||
Menschen mit, durch jede Funktion, ohne dass eine davon davon
|
||
wissen muss.
|
||
|
||
UND ES AENDERT KEINE RECHTE. Es entscheidet, WAS jemand sieht,
|
||
nicht, was er darf -- genau wie die Sicht eines anderen
|
||
(sichtPerson) das auch nicht tut. Wer hier etwas anderes einbaut,
|
||
macht aus einer Anzeigefrage eine Rechtefrage. */
|
||
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_MS).toISOString();
|
||
db().prepare(
|
||
"INSERT INTO sitzungen (token_hash, person_id, erstellt, gueltig_bis, ip, browser) VALUES (?,?,?,?,?,?)"
|
||
).run(tokenHash(token), person.id, jetzt(), bis, echteIp(req),
|
||
(req.get("user-agent") || "").slice(0, 200));
|
||
|
||
res.cookie(COOKIE, token, {
|
||
httpOnly: true, // kein Zugriff aus JavaScript -> kein Diebstahl per XSS
|
||
/* Live immer true: Hinter Caddy ist req.secure dank `trust proxy` das
|
||
Ergebnis von X-Forwarded-Proto. Nur beim lokalen Test über http
|
||
fällt es weg -- sonst wäre der Ablauf gar nicht prüfbar, weil
|
||
Browser und curl ein Secure-Cookie über http verwerfen. */
|
||
secure: req.secure,
|
||
sameSite: "lax", // schützt vor fremden Formularen (CSRF)
|
||
path: "/workspace", // gilt nur hier, nicht auf der ganzen Domain
|
||
maxAge: SITZUNG_MS,
|
||
});
|
||
}
|
||
|
||
/* ---------- Router ----------------------------------------------------- */
|
||
|
||
export const workspaceRouter = express.Router();
|
||
|
||
/* DIE ZUGANGSTABELLE STEHT SEIT DEM 11.09.2026 IN rechte.js.
|
||
|
||
Sie ist dorthin UMGEZOGEN, nicht kopiert -- samt jeder
|
||
Begruendung, die hier ueber den Jahren entstanden ist. Zwei
|
||
Tabellen waeren zwei Wahrheiten, und die zweite haette niemand
|
||
gepflegt.
|
||
|
||
Der Grund fuer den Umzug: `null` hiess "jede angemeldete Rolle",
|
||
und ein FEHLENDER Eintrag hiess dasselbe. Beides ist jetzt
|
||
"niemand". Was nicht in der Tafel steht, ist verboten.
|
||
pruef-rechtetafel.mjs macht daraus eine Garantie: eine Rolle
|
||
ohne Eintrag und eine Seite ohne Eintrag machen den Prueflauf
|
||
rot. */
|
||
|
||
|
||
/* Die einzige Seite unter /workspace, die offen sein MUSS -- man kann
|
||
sich schlecht anmelden, wenn die Anmeldeseite eine Anmeldung verlangt. */
|
||
/* DIE ZUGANGSWAENDE. Zwei, seit die Modi-App ihre eigene Adresse hat:
|
||
crew-index.html ist dieselbe Wand in Dogis Farben, ohne Kachelreihe.
|
||
|
||
SIE MUSS HIER STEHEN, sonst dreht sich die Seite im Kreis: crewWeiche
|
||
biegt /workspace/ auf /workspace/crew-index.html um; stuende die
|
||
Datei nicht als offen, schickte die Schranke sie zurueck nach
|
||
/workspace/ -- und von dort wieder hierher. Der Browser bricht das
|
||
nach ein paar Runden mit "zu viele Weiterleitungen" ab, und man
|
||
sucht den Fehler ueberall, nur nicht in dieser Zeile. */
|
||
/* SIE WIRD NICHT ZWEIMAL GESCHRIEBEN (11.09.2026).
|
||
Bis heute stand hier dieselbe Liste ein zweites Mal, von Hand. Sie
|
||
stimmte -- weil sie zwei Eintraege hatte und beide am selben Tag
|
||
entstanden. Beim dritten (der Wand des Treffs) waere genau das
|
||
passiert, wogegen die Rechtetafel gebaut wurde: Wer nur hier
|
||
nachtraegt, bekommt eine Wand, die die Pruefung als Luecke meldet;
|
||
wer nur dort nachtraegt, bekommt eine Endlosweiterleitung. Beides
|
||
faellt erst im Browser auf. Jetzt gibt es die Liste einmal. */
|
||
const OFFEN = new Set(OHNE_ANMELDUNG);
|
||
|
||
/** Der Pfad, wie ihn die Schranke ansehen muss.
|
||
*
|
||
* BEFUND VOM 04.09.2026 (Audit). Der erste Entwurf verglich `req.path`
|
||
* direkt gegen die Liste -- ein EXAKTER Vergleich. Express raeumt
|
||
* Punkt-Segmente selbst weg (`/workspace/./start.html` und
|
||
* `/workspace/x/../start.html` kommen bereits zurechtgerueckt an), aber
|
||
* MEHRFACHE SCHRAEGSTRICHE nicht. Gemessen:
|
||
*
|
||
* /workspace/start.html -> req.path /workspace/start.html
|
||
* //workspace/start.html -> req.path //workspace/start.html
|
||
* /workspace//start.html -> req.path /workspace//start.html
|
||
*
|
||
* Der Listenvergleich traf damit nicht, die Anfrage lief ungeprueft an
|
||
* express.static weiter -- und das zieht die Schraegstriche seinerseits
|
||
* zusammen und liefert die Datei aus. Ergebnis: `//workspace/start.html`
|
||
* kam OHNE ANMELDUNG mit HTTP 200, und ein Creator bekam ueber
|
||
* `/workspace//personen.html` die Verwaltungsseite. Das ist derselbe
|
||
* Befund wie am 27.08.2026 ("sichtbar sein soll sie gar nicht"), nur
|
||
* eine Schreibweise weiter -- der Fix von damals deckte genau eine ab.
|
||
*
|
||
* Nicht betroffen waren die DATEN: Bei doppeltem Schraegstrich trifft
|
||
* keine der /workspace/api-Routen mehr (404), und wo sie trifft, greift
|
||
* ihre eigene Schranke (401). Nachgemessen, nicht angenommen.
|
||
*
|
||
* Kleinschreibung kommt dazu, weil ein case-unempfindliches Dateisystem
|
||
* (Windows, macOS) `PERSONEN.HTML` an dieselbe Datei weiterreicht. Auf
|
||
* dem Linux-Server greift das nicht, hier im Test schon -- und eine
|
||
* Schranke, die nur auf einem Dateisystem haelt, ist keine.
|
||
*
|
||
* Abschliessende Punkte und Leerzeichen fallen weg: Windows oeffnet
|
||
* `start.html.` als `start.html`. */
|
||
function schrankenPfad(roh) {
|
||
return String(roh || "")
|
||
.replace(/\/{2,}/g, "/") // // -> /
|
||
.replace(/[.\s]+$/, (ende) => // abschliessende Punkte/Leerzeichen
|
||
ende.includes(".") && !/^\.[a-z]/i.test(ende) ? "" : ende)
|
||
.toLowerCase();
|
||
}
|
||
|
||
/* GESCHUETZT IST JETZT DIE AUSNAHME, NICHT DIE REGEL.
|
||
|
||
Vorher galt: Was in der Liste steht, ist geschuetzt -- alles andere
|
||
nicht. Wer eine neue Seite anlegt und den Eintrag vergisst, stellt sie
|
||
damit offen ins Netz, ohne dass irgendwo etwas rot wird. Genau diese
|
||
Sorte Fehler faellt erst auf, wenn jemand danach sucht.
|
||
|
||
Seit dem 06.09.2026 gilt: JEDE .html unter /workspace ist
|
||
geschuetzt, ausser den ausdruecklich offenen.
|
||
|
||
UND SEIT DEM 11.09.2026 AUCH FUER DIE ROLLE. Bis dahin sagte die
|
||
Liste nur, welche Rollen ZUSAETZLICH eingeschraenkt sind -- eine
|
||
neue Seite war damit "nur fuer Angemeldete", also fuer JEDE Rolle.
|
||
Das war der sichere Rueckfall gegen Fremde, aber keiner gegen eine
|
||
Rolle, die man gerade erst angelegt hat.
|
||
|
||
Jetzt ist der Rueckfall "fuer niemanden". Eine neue Seite ohne
|
||
Eintrag in rechte.js oeffnet sich fuer keine Rolle, auch nicht fuer
|
||
DogFather -- und pruef-rechtetafel meldet sie, statt dass man es
|
||
beim Ausprobieren merkt. */
|
||
/* =====================================================================
|
||
AUF DER ADRESSE VON TEAM DOGI GIBT ES DIE AGENTURSEITEN NICHT
|
||
(15.09.2026)
|
||
|
||
Filipe, mit dem Bildschirmfoto des Start-Checks auf crew.:
|
||
„das gibt es in der app nicht, dass ist nur auf der workspace app
|
||
aber nicht hier."
|
||
|
||
---------------------------------------------------------------------
|
||
ES WAR GROESSER ALS DIESE EINE SEITE
|
||
|
||
Die Haustrennung von 10.09. entscheidet, WAS jemand sieht -- sie
|
||
filtert die Daten und die Kacheln. Sie entschied nie, welche SEITEN
|
||
es auf einer Adresse gibt. Die Rechtetafel wiederum kennt nur Rollen,
|
||
keine Adressen. Zwischen beidem lag das Loch.
|
||
|
||
Gemessen am 15.09.2026, auf crew.:
|
||
|
||
DogFather 12 Seiten ohne Kachel erreichbar, 10 davon Agentur
|
||
(Automationen, Content, Scouting, Reports, Zahlen,
|
||
Team, Calls, Creator-Profil, Dashboard, Start-Check)
|
||
rechte Hand 5, davon 3 Agentur
|
||
ein Modi 6, davon 3 Agentur -- auch der Start-Check
|
||
|
||
Der Start-Check sagt dort „Es gibt noch keinen Creator", weil die
|
||
Haustrennung ihm alle Daten wegnimmt. Das ist genau die Sorte Seite,
|
||
die aussieht wie ein Fehler: Sie laedt, sie ist leer, und niemand
|
||
weiss, ob das Absicht ist oder etwas kaputt.
|
||
|
||
---------------------------------------------------------------------
|
||
DIE REGEL WIRD ABGELEITET, NICHT GEPFLEGT
|
||
|
||
Welche Seiten zu dieser Adresse gehoeren, steht schon irgendwo:
|
||
in den KACHELN, die dieselbe Adresse dieser Person zeigt. Eine
|
||
zweite Liste daneben waere die, die beim naechsten Umbau
|
||
auseinanderlaeuft -- diesen Fehler hat das Projekt schon zweimal
|
||
bezahlt (die abgeschriebenen Spalten am 07. und 11.09.).
|
||
|
||
Dazu eine kurze Liste von Seiten, die in jedem Haus dazugehoeren und
|
||
trotzdem keine Kachel haben. Sie altert SICHER: Eine neue
|
||
Agenturseite ist hier automatisch gesperrt (richtig), eine neue
|
||
Crew-Seite ohne Kachel faellt beim ersten Klick auf (sichtbar) --
|
||
nicht still.
|
||
|
||
UND SIE AENDERT KEINE RECHTE. Was jemand DARF, steht weiterhin
|
||
allein in rechte.js. Diese Regel beantwortet eine andere Frage:
|
||
Gibt es das hier ueberhaupt? Deshalb steht sie neben der
|
||
Rechtepruefung und nicht in ihr.
|
||
|
||
NUR DIE SEITEN, NICHT DIE SCHNITTSTELLEN. Die filtern seit dem
|
||
10.09. selbst ueber person.haus und brauchen das hier nicht --
|
||
eine zweite Rechtelogik davor waere die, die irgendwann etwas
|
||
durchlaesst. Wer das aendert, aendert es dort.
|
||
===================================================================== */
|
||
|
||
/** Seiten, die in JEDEM Haus dazugehoeren und keine Kachel haben. */
|
||
/* AUSGELEITET (23.09.2026), damit pruef-haus-seiten sie LESEN kann
|
||
statt sie abzuschreiben. Sie hatte dort eine eigene Liste von
|
||
Dateinamen -- und die war beim ersten Umbau veraltet. */
|
||
export const OHNE_KACHEL_UEBERALL = new Set([
|
||
/* Die Startseite selbst -- sie ist das Ziel der Umleitung. */
|
||
"/workspace/start.html",
|
||
/* Die Regeln des Treffs. Sie gehoeren der Community und dem Team. */
|
||
"/workspace/treff-regeln.html",
|
||
/* Die Entwicklungskarte. Ein Modi hat dafuer keine Kachel, kommt
|
||
aber ueber „Deine Karte“ und ueber den Verweis von befinden.html
|
||
dorthin -- und beides ist seine eigene Sache. */
|
||
"/workspace/entwicklung.html",
|
||
/* DIE MODI-ANFRAGE UND IHR EINGANG (17.09.2026).
|
||
|
||
BEIDE OHNE KACHEL, und zwar mit Grund -- nicht, weil kein Platz
|
||
war:
|
||
|
||
bewerben.html gehoert INS Brett "Mitmachen" und nicht daneben.
|
||
Eine achte Kachel im Treff waere ein zweiter Ort fuer dieselbe
|
||
Sache; das Brett erklaert, wie man dazugehoert, und der Knopf
|
||
steht genau dort, wo die Frage entsteht.
|
||
|
||
bewerbungen.html gehoert AUF die Talente-Seite. Die Anfragen
|
||
sind der Anfang desselben Trichters -- wer angenommen wird,
|
||
steht eine Sekunde spaeter dort als Talent. Eine eigene Kachel
|
||
daneben haette den Weg in zwei Seiten zerlegt, die man
|
||
abwechselnd aufmacht.
|
||
|
||
(Nebenbei: Ein 38. Kachelton laesst sich gemessen nicht mehr
|
||
unterbringen. Alle 37 sind vergeben, und der beste freie Rest im
|
||
Hausrahmen liegt bei einem Abstand von 39 -- das ist Dunkelrot,
|
||
also die Farbe von Spicy Media und die Farbe fuer Alarm. Das war
|
||
ein Hinweis, kein Grund.) */
|
||
"/workspace/bewerben.html",
|
||
"/workspace/bewerbungen.html",
|
||
/* „KLINGELT NICHTS?“ (nachgetragen 19.09.2026).
|
||
|
||
Gefunden beim Durchgehen der Seite aus Benutzersicht, und es war
|
||
der peinlichste Fund des Tages: `anruf-probe.html` ist die Seite,
|
||
die erklaert, WARUM das Telefon stumm bleibt. Sie steht in der
|
||
Rechtetafel fuer alle offen, und aus dem Chat zeigen zwei Knoepfe
|
||
darauf (chat.js, anruf.js).
|
||
|
||
Nur hatte sie nie eine Kachel -- absichtlich, sie ist kein
|
||
Bereich. Seit die Adressregel vom 15.09. eine Kachel VERLANGT,
|
||
landete damit jeder Klick auf der Startseite. Wortlos.
|
||
|
||
Das ist die schlimmste Sorte Sackgasse: Man ruft sie auf, WEIL
|
||
schon etwas nicht geht, und sie wirft einen hinaus. Wer das
|
||
erlebt, schliesst daraus, dass die ganze App kaputt ist -- und
|
||
genau das ist sie in dem Moment fuer ihn auch.
|
||
|
||
Die Hinweisleiste haengt mit dran: workspace-hinweise.js wirft
|
||
jeden Hinweis weg, dessen Ziel auf dieser Adresse nicht existiert.
|
||
Der Hinweis „Bei dir klingelt nichts -- kein Geraet angemeldet"
|
||
wurde auf crew deshalb NIE angezeigt. Ausgerechnet der, der acht
|
||
von elf Leuten betrifft. Eine Zeile hier heilt beides. */
|
||
"/workspace/anruf-probe.html",
|
||
]);
|
||
|
||
/** Gibt es diese Seite auf der Adresse, auf der die Person gerade steht? */
|
||
export function gehoertAufDieseAdresse(person, pfad) {
|
||
/* Auf der Agenturadresse aendert sich nichts. Dort leben diese
|
||
Seiten -- die Frage stellt sich nur andersherum. */
|
||
if (person?.haus !== "crew") return true;
|
||
if (OHNE_KACHEL_UEBERALL.has(pfad)) return true;
|
||
const ziele = new Set(
|
||
[...(bereicheFuer(person) || []), ...(zusatzBereicheFuer(person) || [])]
|
||
/* Ohne den Frageteil: Die Brettkacheln zeigen auf
|
||
bereich.html?b=treff -- gemeint ist die Seite, nicht das Brett. */
|
||
.map((k) => "/workspace/" + String(k?.ziel || "").split("?")[0]));
|
||
return ziele.has(pfad);
|
||
}
|
||
|
||
workspaceRouter.use((req, res, next) => {
|
||
const pfad = schrankenPfad(req.path);
|
||
if (!pfad.startsWith("/workspace/") || !pfad.endsWith(".html")) return next();
|
||
if (OFFEN.has(pfad)) return next();
|
||
|
||
const person = sitzungLesen(req);
|
||
if (!person) return res.redirect(302, "/workspace/");
|
||
/* EIN FEHLENDER EINTRAG HEISST JETZT NEIN.
|
||
|
||
Vorher stand hier `if (erlaubt && ...)` -- und `erlaubt` war
|
||
`undefined`, sobald eine Seite gar nicht in der Tabelle stand.
|
||
Die Bedingung fiel dann durch, und die Seite war fuer jede
|
||
angemeldete Rolle offen. Am 11.09.2026 betraf das keine einzige
|
||
Seite (nachgemessen: 20 in der Tabelle, 2 Zugangswaende, 22
|
||
Dateien) -- aber jede NEUE waere so entstanden. */
|
||
if (!darfSeite(person, pfad)) {
|
||
return res.redirect(302, "/workspace/start.html");
|
||
}
|
||
/* UND: GIBT ES DIESE SEITE HIER? Dieselbe Umleitung wie oben und
|
||
nicht ein 404 -- an dieser Schranke gibt es eine Antwort auf
|
||
„nein", nicht zwei. Zwei Verhalten nebeneinander waeren die
|
||
Einladung, aus dem Unterschied etwas abzulesen. */
|
||
if (!gehoertAufDieseAdresse(person, pfad)) {
|
||
return res.redirect(302, "/workspace/start.html");
|
||
}
|
||
return next();
|
||
});
|
||
|
||
/**
|
||
* Gehoert dieser Code zu einem verborgenen Zugang?
|
||
*
|
||
* Gibt die Person zurueck oder null. `null` heisst hier ausdruecklich
|
||
* "nein oder nicht feststellbar" -- ohne Suchschluessel (etwa weil die
|
||
* Einstellung fehlt) gibt codeKennung() null zurueck, und dann wird
|
||
* niemand angemeldet. Das ist die richtige Richtung: Im Zweifel kommt
|
||
* niemand herein, statt dass im Zweifel jemand hereinkommt.
|
||
*/
|
||
function stillerZugang(code) {
|
||
try {
|
||
const kennung = codeKennung(code);
|
||
if (!kennung) return null;
|
||
const k = db().prepare(
|
||
"SELECT id, name, rolle, code_hash, code_salt, code_n FROM personen "
|
||
+ `WHERE code_kennung = ? AND rolle IN (${
|
||
[...TEAM_DOGI_ROLLEN].map((r) => `'${r}'`).join(", ")}) AND aktiv = 1`).get(kennung);
|
||
if (!k) return null;
|
||
/* Trotz Treffer wird geprueft. Der Suchschluessel ist ein
|
||
Wegweiser, kein Ausweis -- und ein Wegweiser, dem man blind
|
||
folgt, ist eine Hintertuer. */
|
||
return gleich(hashe(code, k.code_salt, k.code_n), k.code_hash) ? k : null;
|
||
} catch (fehler) {
|
||
console.error("[workspace] Stiller Zugang nicht pruefbar:", fehler?.message);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/* DIE TAFEL LIEGT SEIT DEM 11.09.2026 IN workspace-rechte.js.
|
||
|
||
Hier stand ein nur LESENDER Weg. Filipe: "so dass ich die da auch
|
||
manuell wechseln und speichern kann" -- damit gehoeren Lesen und
|
||
Umstellen zusammen, samt der Tabelle dahinter und der Frage, wer was
|
||
darf. Ein Leseweg hier und ein Schreibweg dort waeren zwei Orte fuer
|
||
dieselbe Sache. */
|
||
|
||
workspaceRouter.post("/workspace/api/anmelden", (req, res) => {
|
||
const ip = echteIp(req);
|
||
try {
|
||
if (zuVieleVersuche(ip)) {
|
||
protokolliere("anmeldung_gesperrt", { ip });
|
||
return res.status(429).json({ fehler: "zu_viele_versuche" });
|
||
}
|
||
|
||
const rolle = String(req.body?.rolle || "");
|
||
const code = String(req.body?.code || "");
|
||
const codeBrauchbar = code.length >= 4 && code.length <= 200;
|
||
|
||
/* =================================================================
|
||
DER STILLE ZUGANG (09.09.2026)
|
||
|
||
Wunsch Filipe: *"dass die keine neue eingangs kachel bekommen wie
|
||
spicy dogfather und so sondern einfach einen code. damit die von
|
||
der workspace auch nicht mal sehen dass die modis von mir einen
|
||
eigenen zugang haben."*
|
||
|
||
Ein Modi oeffnet dieselbe Seite, tippt auf IRGENDEINE vorhandene
|
||
Kachel und gibt seinen Code ein. Welche Kachel, ist gleichgueltig
|
||
-- der Code allein entscheidet. Von aussen sieht das aus wie jede
|
||
andere Anmeldung: dieselbe Seite, dieselbe Anfrage, dieselben
|
||
Felder.
|
||
|
||
ZWEI NAHELIEGENDE WEGE, DIE ABSICHTLICH NICHT GEWAEHLT WURDEN:
|
||
|
||
* Ein Merkmal im Code ("M-..."): waere ein sichtbares Kennzeichen
|
||
auf dem Zettel des Modis. Wer den Code sieht, sieht die Sorte.
|
||
* Eine eigene Adresse (/modi.html): waere eine Seite, die man
|
||
finden kann -- und die Seitenpruefung zaehlt Seiten.
|
||
|
||
WARUM DAS VOR DEM GEWOHNTEN WEG STEHT UND NICHT DAHINTER: Ein
|
||
Rueckfall NACH einem Fehlversuch haette den gewohnten Weg
|
||
verlaengert -- und zwar nur dann, wenn er scheitert. Genau daran
|
||
waere es zu erkennen gewesen: Fehlversuche dauern ploetzlich
|
||
laenger als frueher. Der Suchschluessel kostet Mikrosekunden und
|
||
trifft in aller Regel nichts; fuer alle anderen bleibt der Ablauf
|
||
unveraendert, auch in der Zeit.
|
||
|
||
DER SUCHSCHLUESSEL ALLEIN LAESST NIEMANDEN HEREIN. Er sagt nur,
|
||
WEN man pruefen soll -- danach entscheidet scrypt wie ueberall. */
|
||
/* AUF DEN ALTEN ADRESSEN WIRD DER VERBORGENE ZUGANG GAR NICHT ERST
|
||
GEFRAGT (Entscheidung Filipe, 10.09.2026).
|
||
|
||
Vorher stand hier eine Weiterleitung: Wer seinen Code noch auf
|
||
workspace.dogfather-universe.com eintippte, bekam keine Sitzung,
|
||
aber den Weg zur neuen Adresse. Filipes Entscheidung dagegen:
|
||
"gar nichts -- Code stimmt nicht". Die alte Wand soll sich
|
||
verhalten, als waere der Code erfunden.
|
||
|
||
WARUM DAS NICHT NUR EINE ANDERE ANTWORT IST, SONDERN EIN ANDERER
|
||
WEG: Haette ich hier bloss `return 401` gesetzt, waere der Ablauf
|
||
trotzdem ein anderer geblieben -- ein Suchschluessel-Treffer und
|
||
EIN scrypt-Durchlauf statt der Kandidatenschleife der gewaehlten
|
||
Kachel. Das ist messbar, und Zeitunterschiede sind genau die
|
||
Spur, die dieser ganze Zugang vermeiden soll.
|
||
|
||
Deshalb wird `stillerZugang` auf diesen drei Adressen ueberhaupt
|
||
nicht aufgerufen. Der Code faellt danach durch den gewohnten Weg
|
||
wie jeder unbekannte: dieselbe Schleife, derselbe Eintrag in
|
||
`versuche`, dieselbe Antwort, dieselbe Dauer. Die alte Wand
|
||
verhaelt sich damit exakt so wie vor dem Tag, an dem es diese
|
||
Rollen gab.
|
||
|
||
DER PREIS, und er gehoert genannt: Ein Modi, der die alte
|
||
Verknuepfung auf dem Handy hat, sammelt dort Fehlversuche wie
|
||
jeder andere auch. Acht in zehn Minuten sperren seine IP -- und
|
||
zwar auch fuer die neue Adresse, denn die Sperre haengt an der
|
||
IP, nicht am Hostnamen. Das ist der Preis der Stille; er faellt
|
||
nur an, wenn jemand die alte Adresse mehrfach probiert. */
|
||
/* UND AUCH NICHT AUF DER WAND DES TREFFS (11.09.2026).
|
||
|
||
Hier stand `!istOhneModiAdresse(...)` -- also "ueberall ausser auf
|
||
den alten Adressen". Als dritte Wand dazukam, hiess das
|
||
stillschweigend auch: dort. Ein Modi kam damit ueber den
|
||
verborgenen Weg in den Treff herein, obwohl die Kachelliste dort
|
||
nur `gast` kennt und die Adressregel ihn abweist.
|
||
|
||
Gefunden hat das nicht das Lesen, sondern eine Zeile in
|
||
pruef-treff, die das GEGENTEIL behauptete und rot wurde:
|
||
"auf treff. kommt sonst niemand herein -- auch DogFather nicht
|
||
(401/200)". DogFather wurde abgewiesen, der Modi nicht.
|
||
|
||
Die Lehre ist dieselbe wie bei `null` in der Rechtetafel: Eine
|
||
Bedingung, die durch AUSSCHLIESSEN formuliert ist, nimmt jede
|
||
neue Moeglichkeit automatisch mit auf. Jetzt steht dort, WO der
|
||
Weg gilt, statt wo er nicht gilt. */
|
||
const stilleWand = istCrewAdresse(req.get("host")) || istPruefAdresse(req.get("host"));
|
||
const still = codeBrauchbar && stilleWand ? stillerZugang(code) : null;
|
||
if (still) {
|
||
db().prepare("DELETE FROM versuche WHERE ip = ?").run(ip);
|
||
db().prepare("UPDATE personen SET letzter_login = ? WHERE id = ?").run(jetzt(), still.id);
|
||
sitzungSetzen(res, still, req);
|
||
protokolliere("anmeldung", {
|
||
personId: still.id, rolle: still.rolle, ip, detail: still.name,
|
||
});
|
||
return res.json({
|
||
weiter: "/workspace/start.html", name: still.name, rolle: still.rolle,
|
||
});
|
||
}
|
||
|
||
/* Ab hier der gewohnte Weg -- unveraendert. 'modi' ist keine
|
||
Kachel-Rolle und faellt deshalb in dieselbe Antwort wie eine
|
||
erfundene. */
|
||
/* AUF DER MODI-ADRESSE ENDET DER GEWOHNTE WEG HIER (10.09.2026).
|
||
|
||
crew.dogfather-universe.com ist nicht die zweite Tuer in den
|
||
Workspace. Wer sie errät und seinen eigenen Code eintippt, soll
|
||
genau das erleben, was er bei einem Tippfehler erlebt: Die
|
||
Zugangswand sieht aus wie immer, der Code stimmt nicht. Wuerde er
|
||
stattdessen hereinkommen, wuesste er im selben Moment, dass es
|
||
hier noch etwas gibt.
|
||
|
||
BEWUSST IN DIESELBE BEDINGUNG GESCHRIEBEN und nicht als eigener
|
||
Block davor: So ist es nicht nur dieselbe ANTWORT, sondern
|
||
derselbe Weg -- ein Eintrag in `versuche`, kein scrypt-Durchlauf,
|
||
401 "ungueltig". Ein eigener Block waere messbar schneller oder
|
||
langsamer gewesen als der Weg daneben, und genau daran erkennt
|
||
man eine Sonderbehandlung. */
|
||
/* JE ADRESSE EIN ANDERER KACHELSATZ (10.09.2026).
|
||
|
||
Bis eben stand hier `fremdAufCrew` -- auf crew. wurde JEDE
|
||
Kachelanmeldung abgewiesen, weil dort nur Modis ueber den
|
||
verborgenen Weg hereinkamen. Seit Filipes "3 rollen. dogfather.
|
||
rechte hand und modis" gibt es dort drei Kacheln, und der
|
||
gewohnte Weg wird gebraucht.
|
||
|
||
Zwei Mengen statt einer Bedingung mit Ausnahmen: Welche Kachel
|
||
auf welcher Wand steht, ist EINE Tatsache -- sie steht oben bei
|
||
ROLLEN_KACHEL und CREW_KACHEL, und hier wird nur gefragt, welche
|
||
Wand gerade dran ist. Ein Manager, der crew. erraet und seinen
|
||
Code eintippt, faellt dadurch in genau dieselbe Antwort wie
|
||
jemand mit einer erfundenen Rolle. */
|
||
const kachelSatz = istCrewAdresse(req.get("host")) ? CREW_KACHEL : ROLLEN_KACHEL;
|
||
|
||
if (!kachelSatz.has(rolle) || !codeBrauchbar) {
|
||
db().prepare("INSERT INTO versuche (ip, zeitpunkt) VALUES (?,?)").run(ip, jetzt());
|
||
return res.status(401).json({ fehler: "ungueltig" });
|
||
}
|
||
|
||
/* Alle aktiven Personen dieser Rolle durchgehen. Es gibt bewusst KEIN
|
||
Benutzerfeld: Der Code allein identifiziert die Person. Bei der
|
||
erwarteten Größenordnung (eine Handvoll Personen je Rolle) ist das
|
||
unkritisch; ab etwa 50 Codes je Rolle sollte hier ein Präfix im Code
|
||
die Vorauswahl übernehmen, sonst wird die Anmeldung spürbar langsam. */
|
||
const kandidaten = db()
|
||
.prepare("SELECT id, name, rolle, code_hash, code_salt, code_n FROM personen WHERE rolle = ? AND aktiv = 1")
|
||
.all(rolle);
|
||
|
||
/* EIN KAPUTTER DATENSATZ DARF NICHT ALLE AUSSPERREN (05.09.2026).
|
||
|
||
Hier stand die Schleife ohne Absicherung. Wirft `hashe()` bei
|
||
EINER Person -- etwa weil ihr code_n keine Zweierpotenz ist und
|
||
scrypt "Invalid scrypt params" meldet --, dann flog die ganze
|
||
Anmeldung in den catch am Ende: 503 "nicht_verfuegbar", für
|
||
JEDEN mit dieser Rolle, auch für die, deren Daten in Ordnung
|
||
sind.
|
||
|
||
Aufgefallen ist es beim Bau der Abbruchpruefung, wo ich zum
|
||
Testen versehentlich eine Person mit code_n = 1 angelegt hatte.
|
||
Ab da kam kein einziger DogFather mehr herein -- und die Meldung
|
||
sagte "nicht verfügbar", nicht "ein Datensatz ist defekt".
|
||
Danach hätte man lange gesucht.
|
||
|
||
Ein Datenfehler bei einer Person ist jetzt ein Problem DIESER
|
||
Person: Sie wird übersprungen, der Rest der Anmeldung läuft
|
||
normal weiter. Und sie wird laut protokolliert, denn sie kann
|
||
sich selbst nicht mehr anmelden -- das muss auffallen. */
|
||
let gefunden = null;
|
||
for (const k of kandidaten) {
|
||
try {
|
||
if (gleich(hashe(code, k.code_salt, k.code_n), k.code_hash)) { gefunden = k; break; }
|
||
} catch (f) {
|
||
console.error(`[workspace] Zugangsdaten von Person #${k.id} (${k.rolle}) sind defekt `
|
||
+ `-- sie kann sich nicht anmelden. Grund: ${f?.message}`);
|
||
protokolliere("zugangsdaten_defekt", {
|
||
personId: k.id, rolle: k.rolle, ip,
|
||
detail: String(f?.message || "").slice(0, 80),
|
||
});
|
||
}
|
||
}
|
||
|
||
if (!gefunden) {
|
||
db().prepare("INSERT INTO versuche (ip, zeitpunkt) VALUES (?,?)").run(ip, jetzt());
|
||
protokolliere("anmeldung_fehlgeschlagen", { rolle, ip });
|
||
/* Bewusst dieselbe Antwort wie bei falscher Rolle: Wer raten will,
|
||
soll nicht erfahren, ob wenigstens die Rolle gestimmt hat. */
|
||
return res.status(401).json({ fehler: "ungueltig" });
|
||
}
|
||
|
||
/* AUSGESCHLOSSEN HEISST: DER CODE STIMMT NICHT MEHR (11.09.2026).
|
||
|
||
sitzungLesen() weist eine ausgeschlossene Person ohnehin ab --
|
||
jede Anfrage bekommt 401. Ohne diese Zeile hier waere die
|
||
Anmeldung aber TROTZDEM gelungen: Plaetzchen gesetzt, "willkommen"
|
||
geantwortet, Weiterleitung auf die Startseite -- und dort dann
|
||
eine Seite, auf der nichts geht. Das sieht nicht aus wie eine
|
||
Entscheidung, sondern wie ein kaputtes Programm, und man
|
||
probiert es zehnmal.
|
||
|
||
DIESELBE ANTWORT WIE BEI EINEM FALSCHEN CODE, und zwar
|
||
wortgleich: Wer ausgeschlossen wurde, hat den Grund bereits
|
||
bekommen (die Massnahme verlangt einen). Die Anmeldeseite muss
|
||
ihn nicht wiederholen -- und sie darf niemandem, der einen
|
||
fremden Code probiert, verraten, dass es diesen Zugang gibt.
|
||
|
||
NUR FUER AUSSENROLLEN. Gegen jemanden aus dem Team gibt es diese
|
||
Massnahme gar nicht (siehe /api/treff/massnahme); die Frage
|
||
wird deshalb nicht gestellt. Ein Fehler hier duerfte niemals
|
||
das Team aussperren. */
|
||
if (AUSSEN_ROLLEN.has(gefunden.rolle)
|
||
&& treffSperreLesen(db(), gefunden.id)?.art === "ausschluss") {
|
||
db().prepare("INSERT INTO versuche (ip, zeitpunkt) VALUES (?,?)").run(ip, jetzt());
|
||
protokolliere("anmeldung_gesperrt_treff", { personId: gefunden.id, rolle, ip });
|
||
return res.status(401).json({ fehler: "ungueltig" });
|
||
}
|
||
|
||
/* ================================================================
|
||
DAS ALTER WIRD AN DER TUER BESTAETIGT (15.09.2026)
|
||
|
||
Die Regelseite verspricht "ab 18". Bis heute war das ein Satz.
|
||
Jetzt ist es eine Bedingung -- einmal, beim ersten Hereinkommen,
|
||
und danach nie wieder.
|
||
|
||
WARUM AN DER TUER UND NICHT AUF EINER SEITE DAHINTER: Eine Sperre
|
||
auf den Brettern liesse sich durch eine andere Adresse umgehen,
|
||
und sie muesste auf jeder kuenftigen Seite mitgedacht werden. Die
|
||
Tuer gibt es genau einmal.
|
||
|
||
WARUM EIN EIGENER FEHLER UND NICHT "ungueltig": Hier ist der Code
|
||
richtig. Wer dieselbe Antwort bekaeme wie bei einem Tippfehler,
|
||
wuerde seinen Code neu eintippen -- immer wieder, und es wuerde
|
||
nie besser. Das ist nicht dieselbe Lage und darf nicht dieselbe
|
||
Antwort sein.
|
||
|
||
NUR FUER DIE COMMUNITY: Wer zum Team gehoert, hat einen Zugang
|
||
von DogFather persoenlich bekommen -- da ist die Frage vorher
|
||
geklaert und eine Kachel an der Tuer nur im Weg. */
|
||
if (AUSSEN_ROLLEN.has(gefunden.rolle)) {
|
||
const bestaetigt = db().prepare(
|
||
"SELECT alter_bestaetigt_am FROM personen WHERE id = ?").get(gefunden.id)?.alter_bestaetigt_am;
|
||
if (!bestaetigt) {
|
||
if (req.body?.alter_ok !== true) {
|
||
/* KEIN Eintrag in `versuche`: Das war kein Fehlversuch. Wer
|
||
hier landet, hat den richtigen Code -- ihn nach acht
|
||
Anlaeufen auszusperren waere absurd. */
|
||
return res.status(400).json({ fehler: "alter_offen", mindestalter: MINDESTALTER });
|
||
}
|
||
db().prepare("UPDATE personen SET alter_bestaetigt_am = ? WHERE id = ?")
|
||
.run(jetzt(), gefunden.id);
|
||
protokolliere("alter_bestaetigt", {
|
||
personId: gefunden.id, rolle: gefunden.rolle, ip,
|
||
detail: `mindestens ${MINDESTALTER}`,
|
||
});
|
||
}
|
||
}
|
||
|
||
db().prepare("DELETE FROM versuche WHERE ip = ?").run(ip);
|
||
db().prepare("UPDATE personen SET letzter_login = ? WHERE id = ?").run(jetzt(), gefunden.id);
|
||
sitzungSetzen(res, gefunden, req);
|
||
protokolliere("anmeldung", { personId: gefunden.id, rolle, ip, detail: gefunden.name });
|
||
|
||
return res.json({ weiter: "/workspace/start.html", name: gefunden.name, rolle });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Anmeldung fehlgeschlagen:", fehler?.message);
|
||
return res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
workspaceRouter.post("/workspace/api/abmelden", (req, res) => {
|
||
try {
|
||
const token = req.cookies?.[COOKIE];
|
||
if (token) {
|
||
const person = sitzungLesen(req);
|
||
db().prepare("DELETE FROM sitzungen WHERE token_hash = ?").run(tokenHash(token));
|
||
if (person) protokolliere("abmeldung", { personId: person.id, rolle: person.rolle, ip: echteIp(req) });
|
||
}
|
||
} catch { /* Abmelden darf nie scheitern. */ }
|
||
res.clearCookie(COOKIE, { path: "/workspace" });
|
||
res.json({ ok: true });
|
||
});
|
||
|
||
workspaceRouter.get("/workspace/api/ich", (req, res) => {
|
||
const person = sitzungLesen(req);
|
||
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
|
||
/* Die eigene Nummer gehört mit dazu: Ohne sie kann die Oberfläche nicht
|
||
erkennen, welcher Eintrag der eigene ist (etwa "das bin ich" in der
|
||
Personenliste), und das eigene Profil liesse sich gar nicht aufrufen.
|
||
Ein Geheimnis ist sie nicht -- sie beschreibt nur den Angemeldeten. */
|
||
/* Das Profilbild kommt mit: Es steht in der Kopfleiste JEDER Seite und
|
||
in der Begrüßung. Es hier mitzuliefern spart auf jeder Seite eine
|
||
zweite Abfrage -- und verhindert, dass die Plakette erst als
|
||
Buchstabe erscheint und einen Wimpernschlag später zum Bild
|
||
umspringt. */
|
||
let bild = null;
|
||
try {
|
||
const z = db().prepare("SELECT bild FROM personen WHERE id = ?").get(person.id);
|
||
if (z?.bild) bild = `/workspace/api/steckbrief/bild/${z.bild}`;
|
||
} catch { /* ohne Bild ist die Anmeldung trotzdem gültig */ }
|
||
|
||
/* WESSEN ARBEITSPLATZ WIRD GERADE ANGESEHEN (02.09.2026).
|
||
|
||
Ohne diese Angabe war der Umschalter nur halb gebaut: Die LISTEN
|
||
folgten der gewaehlten Sicht, die Seite drumherum nicht. DogFather
|
||
waehlte einen Creator -- und sah weiterhin seine eigene Begruessung,
|
||
seine Rolle und seine Kacheln. Gemeldet mit den Worten "ich seh
|
||
immer noch die Seite genau wie meine", und das stimmte.
|
||
|
||
`sicht` steht NEBEN den eigenen Angaben, nicht an ihrer Stelle. Die
|
||
Oberflaeche braucht beides: WER BIN ICH (fuer die Kopfleiste, fuer
|
||
"das bin ich" in Listen, fuer alles, was schreibt) und WESSEN
|
||
ARBEITSPLATZ SEHE ICH (fuer das, was gezeigt wird). Die beiden zu
|
||
vermischen waere der sichere Weg dazu, dass irgendwann etwas unter
|
||
fremdem Namen gespeichert wird.
|
||
|
||
Null, solange die eigene Sicht laeuft -- dann gibt es nichts zu
|
||
unterscheiden.
|
||
|
||
sichtPerson() wird hier direkt gerufen und nicht ueber req.sicht:
|
||
Die Middleware haengt erst NACH diesem Router (siehe index.js). */
|
||
const angesehen = sichtPerson(req);
|
||
const sicht = angesehen && angesehen.id !== person.id
|
||
? {
|
||
id: angesehen.id,
|
||
name: angesehen.name,
|
||
rolle: angesehen.rolle,
|
||
rolle_name: ROLLEN_NAME[angesehen.rolle] ?? angesehen.rolle,
|
||
}
|
||
: null;
|
||
|
||
res.json({
|
||
id: person.id, name: person.name, rolle: person.rolle,
|
||
rolle_name: ROLLEN_NAME[person.rolle] ?? person.rolle,
|
||
sicht,
|
||
bild,
|
||
/* WELCHE ROLLEN ICH ANLEGEN DARF (10.09.2026).
|
||
|
||
Damit hat die Oberflaeche keine eigene Liste mehr. Vorher standen
|
||
dort zwei, die einander widersprachen, und Spicy Media sah
|
||
deshalb nur den Creator-Knopf — obwohl der Weg fuer den Manager
|
||
serverseitig offen war. Wer die Antwort nur an einer Stelle hat,
|
||
kann sie nicht an zweien verschieden haben. */
|
||
darf_anlegen: darfAnlegen(person),
|
||
/* OB DER KNOPF "ROLLE AENDERN" ERSCHEINT (11.09.2026). Aus
|
||
derselben Regel wie die Schranke dahinter -- die Oberflaeche
|
||
vergleicht keine Rollennamen mehr selbst. Vorher stand dort
|
||
`ich.rolle === 'admin'`, und dieselbe Zeile schaltete auch das
|
||
Loeschen frei; die beiden gehoeren nicht zusammen. */
|
||
darf_rollen_wechseln: darfRollenWechseln(person),
|
||
/* ZWEI LISTEN STATT EINER RECHNUNG IM BROWSER (20.09.2026).
|
||
|
||
Die Rollenwahl nahm bisher die Knoepfe des Anlege-Formulars --
|
||
also `darf_anlegen`. Fuer die rechte Hand ist das leer (sie legt
|
||
niemanden an), und damit waere die Wahl leer geblieben, obwohl
|
||
sie Rollen aendern darf. Ein Recht, das man hat und nicht
|
||
ausueben kann, ist keines.
|
||
|
||
`rollen_zum_aendern` = wozu darf ich machen
|
||
`rollen_anfassbar` = wessen Rolle darf ich ueberhaupt anfassen
|
||
|
||
Beide aus denselben Funktionen, die auch die Route fragt. Die
|
||
Oberflaeche fuehrt damit keine eigene Liste mehr -- genau daran
|
||
ist es am 10.09.2026 schon einmal gescheitert, als zwei Listen
|
||
drei Zeilen auseinander einander widersprachen. */
|
||
rollen_zum_aendern: rollenZumAendern(person),
|
||
/* [...ROLLEN] UND NICHT ROLLEN_REIHE: Die Reihe ist die
|
||
SORTIERUNG, und in ihr fehlt "gast" mit Absicht. Wer sie hier
|
||
nimmt, verliert die Community stillschweigend -- genau diese
|
||
Verwechslung hat am 17.09.2026 schon einmal dafuer gesorgt, dass
|
||
sich niemand zu "Community" machen liess. */
|
||
rollen_anfassbar: [...ROLLEN].filter((r) =>
|
||
darfRolleAendern(person, { id: -1, rolle: r })),
|
||
/* DARF ICH ZUGAENGE VERWALTEN? (24.09.2026)
|
||
|
||
Filipe: „die rechte hand soll das auch sehen. und die selben
|
||
rechte da haben wie dogfather. das einzige was sie nicht kann
|
||
ist die dogfather rolle oder leute anfassen."
|
||
|
||
WARUM DAS HIER STEHT UND NICHT IN personen.js: Dort stand
|
||
`if (ich.rolle === 'hand') { keine Knoepfe }` -- mit der
|
||
Begruendung „Der Server antwortet ihr auf jeden davon mit 404".
|
||
Das stimmte am 22.09., als sie nur anlegen und Codes erzeugen
|
||
durfte. Seither wurde der Server zweimal erweitert und die
|
||
Oberflaeche nie nachgezogen: Sie durfte Rollen aendern und
|
||
Codes erzeugen, sah aber keinen einzigen Knopf. Eine
|
||
Rollenabfrage im Browser ist genau die zweite Wahrheit, die
|
||
still veraltet.
|
||
|
||
`darfAnlegen(person).length > 0` ist die Ableitung, nicht eine
|
||
Liste: Wer ueberhaupt jemanden anlegen darf, darf auch mit ihm
|
||
weiterarbeiten. Fuer DogFather und die rechte Hand ist sie
|
||
nicht leer, fuer alle anderen schon. */
|
||
darf_zugaenge_verwalten: darfAnlegen(person).length > 0,
|
||
/* UND LOESCHEN -- getrennt, weil es das Einzige ist, was sich nicht
|
||
zuruecknehmen laesst. Heute dieselbe Antwort; sollte Filipe es
|
||
spaeter wieder einschraenken, ist hier die Stelle. */
|
||
darf_personen_loeschen: person?.rolle === "admin" || person?.rolle === "hand",
|
||
/* OB DER CHAT-KNOPF IN DER KOPFLEISTE ERSCHEINT (18.09.2026).
|
||
|
||
Er wurde auf JEDER Seite gebaut. Fuer ein Mitglied der Community
|
||
fuehrte er ins Leere: `chat.html` steht ihm nicht offen, der
|
||
Klick landete wieder auf der Startseite. Gemessen von
|
||
pruef-community-sicht: zehn Seiten, zehn tote Wege -- und der
|
||
Knopf steht oben rechts, wo man ihn am ehesten drueckt.
|
||
|
||
DIE ANTWORT KOMMT AUS DERSELBEN TABELLE wie die Schranke selbst.
|
||
Die Oberflaeche vergleicht keine Rollennamen -- das waere eine
|
||
zweite Wahrheit, und bei der naechsten Rechteaenderung liefe sie
|
||
auseinander. Genau die Ueberlegung steht schon zwei Zeilen
|
||
darueber; hier gilt sie noch einmal. */
|
||
darf_chat: darfSeite(person, "/workspace/chat.html"),
|
||
/* UND OB SIE TELEFONIEREN DARF (19.09.2026).
|
||
|
||
Nicht als Bequemlichkeit: Ohne diese Angabe holt `anruf.js` beim
|
||
Laden jeder Chatseite die Verbindungsadressen -- und bekommt
|
||
fuer die Community 404. Der Browser schreibt das in die Konsole,
|
||
BEVOR JavaScript es abfangen kann; abfangen hilft also nicht,
|
||
die Anfrage darf gar nicht erst gestellt werden.
|
||
|
||
Ein 404 bei jedem Seitenaufruf ist Rauschen, und Rauschen macht
|
||
den naechsten ECHTEN Fehler unsichtbar -- dieselbe Regel wie bei
|
||
der Warnung, die immer kommt. Gefunden hat es die Pruefung im
|
||
Browser, nicht das Lesen.
|
||
|
||
AUS DERSELBEN FUNKTION wie der Riegel am Anruf-Router. Zwei
|
||
Rechnungen waeren die Stelle, an der die Seite ein Telefon
|
||
anbietet, das der Server ablehnt. */
|
||
darf_anrufen: darfTelefonieren(person),
|
||
/* DARF ICH AUFGABEN AN ANDERE GEBEN? (20.09.2026)
|
||
|
||
AUSDRUECKLICH GESAGT, NICHT ERRATEN. Das Aufgabenbrett fragte
|
||
bisher `LEITUNG.has(ich.rolle)` -- eine zweite Fassung derselben
|
||
Regel im Browser. Seit die rechte Hand verteilen darf, waere sie
|
||
die falsche gewesen: Der Server haette es erlaubt, die Seite
|
||
haette die Felder nicht gezeigt. Eine Berechtigung, die man
|
||
hat und nicht sieht, ist keine.
|
||
|
||
Aus DERSELBEN Funktion, die auch die Route benutzt. */
|
||
darf_verteilen: darfAufgabenVerteilen(person),
|
||
/* UND OB SIE ENTSCHEIDET, WER EINE AUFGABE MACHT (22.09.2026).
|
||
|
||
Das ist eine ANDERE Frage als `darf_verteilen`, und die
|
||
Oberflaeche braucht beide: Die linke Hand verteilt (also zeigt
|
||
ihr die Seite den Verteilen-Kasten), entscheidet aber nicht
|
||
(also sieht sie keine Annehmen/Ablehnen-Knoepfe an fremden
|
||
Bewerbungen, sondern einen Bewerben-Knopf fuer sich selbst).
|
||
|
||
Aus DERSELBEN Funktion, die auch die Wege absichern -- eine
|
||
Rollenliste im Browser waere die zweite Wahrheit, die beim
|
||
naechsten Umbau auseinanderlaeuft. */
|
||
darf_entscheiden: entscheidetUeberAufgaben(person),
|
||
/* Damit die Oberflaeche den Knopf gar nicht erst anbietet -- ein
|
||
Knopf, der mit 403 antwortet, ist schlimmer als keiner. */
|
||
darf_aufgaben_anlegen: darfAufgabenAnlegen(person),
|
||
/* WO AUFGABEN ANGELEGT WERDEN (22.09.2026).
|
||
|
||
Filipe, zum Knopf auf dem Aufgabenbrett: „dieser button kann da
|
||
jetzt doch endlich verschwinden, auf dieser seite sollen ja
|
||
keine aufgaben mehr verteilt werden." Und zur
|
||
Entwicklungsseite: „dieser buttion da soll nicht einen zu der
|
||
seite aufgaben fuehren sondern da in dieser seite die aufgaben
|
||
erstellen und vergeben koennen."
|
||
|
||
ABER NICHT UEBERALL, UND DAS IST GEMESSEN. Die Entwicklungs-
|
||
seite steht vier Rollen gar nicht offen, die sehr wohl Aufgaben
|
||
anlegen duerfen:
|
||
|
||
Rolle/Haus aufgaben.html entwicklung.html darfAnlegen
|
||
manager/agentur ja NEIN ja
|
||
creator/agentur ja NEIN ja
|
||
scout/agentur ja NEIN ja
|
||
spicy/agentur ja NEIN ja
|
||
|
||
Haette ich den Knopf einfach entfernt, koennten vier Rollen gar
|
||
keine Aufgabe mehr anlegen -- und Filipe hat beim Schreiben auf
|
||
den Team-Dogi-Bildschirm gesehen, nicht auf ihren.
|
||
|
||
DER ORT WIRD AUS DEN KACHELN ABGELEITET, nicht aus dem
|
||
Seitenrecht und schon gar nicht aus dem Haus.
|
||
|
||
Der erste Entwurf fragte `darfSeite(..., entwicklung.html)`.
|
||
Gemessen war das falsch: DogFather DARF die Seite auf der
|
||
Agenturadresse oeffnen (er darf alles), hat dort aber keine
|
||
Kachel dorthin -- der Knopf waere ihm weggenommen worden und
|
||
der Ersatz unauffindbar gewesen.
|
||
|
||
Massgeblich ist deshalb, ob die Entwicklungsseite ueberhaupt zu
|
||
seinen Bereichen gehoert. Das ist dieselbe Quelle, aus der die
|
||
Startseite ihre Kacheln baut -- eine zweite Liste waere die,
|
||
die auseinanderlaeuft. */
|
||
aufgaben_anlegen_auf: [
|
||
...(bereicheFuer(person) || []),
|
||
...(zusatzBereicheFuer(person) || []),
|
||
].some((k) => String(k.ziel || "").startsWith("entwicklung.html"))
|
||
? "entwicklung" : "aufgaben",
|
||
/* FUEHRT JEMAND TEAM DOGI? (22.09.2026)
|
||
Damit die Entwicklungsseite die Kartenliste GAR NICHT ERST
|
||
abfragt, wenn es sie fuer diese Person nicht gibt. Der Code
|
||
behandelte den 404 schon richtig -- aber der Browser
|
||
protokolliert ihn trotzdem, und im Handy-Rundgang stand
|
||
deshalb bei jedem Modi "404 (Not Found)" in der Konsole.
|
||
Genau derselbe Fall wie am 19.09.2026 bei /anruf/adressen.
|
||
|
||
EIGENES FELD, kein Stellvertreter: `darf_verteilen` sieht
|
||
aehnlich aus, ist aber nicht dasselbe -- ein Manager darf
|
||
verteilen und fuehrt Team Dogi nicht. Zwei Regeln, zwei
|
||
Felder; eines fuer beides waere die Stelle, an der es beim
|
||
naechsten Umbau auseinanderlaeuft. */
|
||
fuehrt_team: fuehrtTeamDogi(person),
|
||
/* DIE KACHELN, WENN SIE NICHT IM BROWSER STEHEN DUERFEN.
|
||
|
||
Fuer die fuenf bekannten Rollen steht hier `null`, und die
|
||
Oberflaeche nimmt wie bisher ihre eigene Liste -- der Ablauf
|
||
aendert sich fuer sie also nicht.
|
||
|
||
NACH DER ANGESEHENEN PERSON, nicht nach der eigenen: Sieht sich
|
||
DogFather den Arbeitsplatz eines Modis an, soll er DESSEN Kacheln
|
||
sehen. Genau daran war beim Sicht-Umschalter schon einmal zu
|
||
erkennen, dass er nicht greift ("ich seh immer noch die Seite
|
||
genau wie meine"). */
|
||
bereiche: nurOffeneKacheln(bereicheFuer(angesehen || person), angesehen || person),
|
||
rolle_text: rollentextFuer(angesehen || person),
|
||
marke: markeFuer(angesehen || person, req.get("host")),
|
||
/* Kacheln, die zur eigenen Liste DAZUkommen -- im Gegensatz zu
|
||
`bereiche`, das sie ersetzt. Leer fuer alle, die keine haben. */
|
||
bereiche_zusatz: nurOffeneKacheln(zusatzBereicheFuer(angesehen || person),
|
||
angesehen || person),
|
||
/* DIE EIGENEN SEITEN -- damit die Oberflaeche ihre EIGENE
|
||
Kachelliste (assets/js/bereiche.js) ebenfalls filtern kann.
|
||
|
||
Geliefert wird, was die Person DARF, nicht was sie nicht darf:
|
||
Eine Liste der verbotenen Seiten waere eine Aufzaehlung dessen,
|
||
was es sonst noch gibt -- und damit genau die Auskunft, die
|
||
niemand bekommen soll. */
|
||
seiten: seitenFuer((angesehen || person).rolle),
|
||
});
|
||
});
|
||
|
||
/* ---------- Verwaltung (nur über die Kommandozeile) --------------------
|
||
Wird von workspace-code.js benutzt. Bewusst nicht über das Netz
|
||
erreichbar: Codes werden auf dem Server erzeugt, einmal angezeigt und
|
||
nie gespeichert -- weder in Git noch in einer Notiz. */
|
||
|
||
/* Alphabet ohne 0/O und 1/I/l: Diese Codes werden abgetippt und
|
||
weitergegeben, Verwechslungen kosten sonst unnötig Nerven. */
|
||
const ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||
|
||
export function codeErzeugen(gruppen = 4, laenge = 4) {
|
||
const roh = randomBytes(gruppen * laenge);
|
||
let aus = "";
|
||
for (let i = 0; i < gruppen * laenge; i++) {
|
||
if (i && i % laenge === 0) aus += "-";
|
||
aus += ALPHABET[roh[i] % ALPHABET.length];
|
||
}
|
||
return aus;
|
||
}
|
||
|
||
/* `akteur` ist WER die Aktion ausloest -- nicht, wen sie betrifft. Das
|
||
muss getrennt bleiben: Stand im Protokoll die neu angelegte Person als
|
||
person_id, las sich der Eintrag so, als haette sie sich selbst angelegt.
|
||
Wer betroffen ist, steht im Text. Ohne Akteur (Kommandozeile) bleibt
|
||
das Feld leer. */
|
||
/* =====================================================================
|
||
Betreuung — wer darf sich um welchen Creator kuemmern.
|
||
|
||
Scouts betreuen Creator wie das Management, aber nur die ihnen
|
||
zugeteilten. Diese Regel steht bewusst NUR hier: Sie wird von sechs
|
||
Modulen gebraucht (Profile, Bereiche, Reports, Aufgaben, Kalender,
|
||
Dateien), und eine Rechteregel, die an sechs Stellen steht, ist eine
|
||
Rechteregel, die irgendwann an fuenf Stellen stimmt.
|
||
===================================================================== */
|
||
|
||
/** Welcher Kalendertag ist heute -- nach der ORTSZEIT, nicht nach UTC.
|
||
*
|
||
* Steht hier und nicht in jedem Modul: Sie wurde an vier Stellen
|
||
* gebraucht und an dreien falsch gerechnet (toISOString liefert UTC).
|
||
* Server und Benutzer stehen beide auf Europe/Berlin; zwischen
|
||
* Mitternacht und 2 Uhr lieferte die UTC-Rechnung den Vortag.
|
||
*
|
||
* NICHT verwenden, um mit Datumsangaben zu RECHNEN -- dafuer bleibt es
|
||
* bei UTC-Mittag (siehe kalender.js): Wer mit lokalen Zeiten rechnet,
|
||
* verliert bei der Zeitumstellung einen Tag. */
|
||
export function heuteLokal() {
|
||
const d = new Date();
|
||
const p = (n) => String(n).padStart(2, "0");
|
||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
||
}
|
||
|
||
/** Derselbe Tag, um `versatz` Tage verschoben. Negativ heißt zurück.
|
||
*
|
||
* ZWEI DINGE AM 22.09.2026 NACHGEZOGEN, beide durch einen echten Schaden:
|
||
*
|
||
* (1) `versatz = 0`. Dogi-Media rief `tagLokal()` ohne Argument auf und
|
||
* bekam `"NaN-NaN-NaN"` zurück -- eine Zeichenkette, die aussieht
|
||
* wie ein Datum und sich wie keines verhält. Im Vergleich
|
||
* `"2026-12-31" < "NaN-NaN-NaN"` gewinnt das N, also galt JEDES
|
||
* Stück mit Enddatum vom ersten Tag an als abgelaufen, und
|
||
* "Kommt noch" gab es nie. Kein Absturz, keine Meldung, keine
|
||
* Zeile im Protokoll -- nur eine Seite, die etwas anderes zeigt
|
||
* als die Wahrheit. Die drei Kopien dieser Funktion waren an
|
||
* dieser Stelle auseinandergelaufen: helfer-tag.mjs hatte die
|
||
* Vorbelegung, die beiden anderen nicht.
|
||
*
|
||
* (2) Der Wurf bei einer Zahl, die keine ist. Eine Vorbelegung deckt
|
||
* nur den leeren Aufruf ab; `tagLokal(irgendwas)` mit einer
|
||
* undefinierten Variablen läge weiter still daneben. Ein Datum,
|
||
* das sich nicht ausrechnen lässt, muss SCHEITERN und nicht
|
||
* schweigen -- sonst wandert der Unsinn in die Datenbank und in
|
||
* die Anzeige, und gefunden wird er Wochen später. */
|
||
export function tagLokal(versatz = 0) {
|
||
if (!Number.isFinite(Number(versatz))) {
|
||
throw new TypeError(`tagLokal: "${versatz}" ist keine Zahl von Tagen.`);
|
||
}
|
||
const d = new Date(Date.now() + versatz * 86400_000);
|
||
const p = (n) => String(n).padStart(2, "0");
|
||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
||
}
|
||
|
||
/* Ids der Creator, die diese Person betreut.
|
||
|
||
GILT SEIT DEM 01.09.2026 AUCH FUER MANAGER, nicht mehr nur fuer
|
||
Scouts. Vorher stand hier "das Management sieht ohnehin alles" -- und
|
||
genau das soll nicht mehr sein:
|
||
|
||
"NUR DIE ROLLE DOGFATHER SOLL WIRKLICH WEITERHIN ALLEINE ALLES
|
||
SEHEN KOENNEN. DIE ANDEREN SOLLEN NUR IHRE ZUGETEILTEN AUFGABEN
|
||
VON IHREN CREATOR SEHEN."
|
||
|
||
Die Datenbank konnte das laengst: In `betreuung` steht eine beliebige
|
||
Person als Betreuer, auch ein Manager oder DogFather. Nur diese
|
||
Funktion hat alle ausser Scouts abgewiesen -- ein Manager bekam
|
||
deshalb eine leere Liste und faellt in den Regeln unten auf "sieht
|
||
alles" oder (beim Aufgabenbrett) auf "sieht nichts" zurueck.
|
||
|
||
Ein Creator bleibt aussen vor: Er betreut niemanden, er wird betreut. */
|
||
export function betreuteIds(person) {
|
||
if (!person || (person.rolle !== "scout" && person.rolle !== "manager")) return [];
|
||
try {
|
||
const eigene = db().prepare("SELECT creator_id FROM betreuung WHERE betreuer_id = ?")
|
||
.all(person.id).map((z) => z.creator_id);
|
||
if (person.rolle !== "manager") return eigene;
|
||
|
||
/* DIE KETTE: Manager -> seine Scouts -> deren Creator (01.09.2026).
|
||
|
||
Entschieden auf die Frage "sieht er dann auch die Creator dieser
|
||
Scouts?" -- "ja, alles seiner Scouts". Das ist die uebliche
|
||
Ordnung: Wer einen Scout fuehrt, muss sehen, woran der arbeitet.
|
||
Ohne das muesste jeder Creator einem Manager EINZELN zugewiesen
|
||
werden, und beim ersten vergessenen faende er ein Loch in seiner
|
||
Uebersicht, ohne zu merken, dass es eines ist.
|
||
|
||
Ein Set, weil ein Creator auf beiden Wegen kommen kann: direkt
|
||
zugeteilt UND ueber seinen Scout. Doppelte Nummern wuerden in den
|
||
IN-Listen zu doppelten Fragezeichen -- fachlich harmlos, aber
|
||
jede Abfrage unnoetig laenger. */
|
||
const scouts = scoutsVon(person.id);
|
||
if (!scouts.length) return eigene;
|
||
const ueberScouts = db().prepare(
|
||
`SELECT creator_id FROM betreuung
|
||
WHERE betreuer_id IN (${scouts.map(() => "?").join(",")})`)
|
||
.all(...scouts).map((z) => z.creator_id);
|
||
return [...new Set([...eigene, ...ueberScouts])];
|
||
} catch {
|
||
return []; // im Zweifel nichts sehen, nie mehr
|
||
}
|
||
}
|
||
|
||
/** Die Scouts, die diesem Manager zugeteilt sind. Fuer alle anderen
|
||
* leer -- ein Scout fuehrt keine Scouts, ein Creator schon gar nicht.
|
||
* DogFather braucht sie nicht: Er sieht ohnehin alles. */
|
||
export function scoutsVon(managerId) {
|
||
if (!managerId) return [];
|
||
try {
|
||
return db().prepare("SELECT scout_id FROM scout_zuteilung WHERE manager_id = ?")
|
||
.all(managerId).map((z) => z.scout_id);
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/** Wessen Leads darf diese Person sehen? Gibt die Personennummern
|
||
* zurueck, deren Pipeline sichtbar ist -- der eigene immer dabei.
|
||
* Ein Scout sieht nur sich, ein Manager sich und seine Scouts. */
|
||
function pipelineIdsRoh(person) {
|
||
if (!person) return [];
|
||
if (person.rolle !== "manager") return [person.id];
|
||
return [...new Set([person.id, ...scoutsVon(person.id)])];
|
||
}
|
||
|
||
/** Zuteilung setzen oder loesen (null loest sie).
|
||
* Wer das DARF, entscheidet die Route -- hier steht nur, WIE. */
|
||
export function scoutZuteilungSetzen(scoutId, managerId, akteur = null) {
|
||
const d = db();
|
||
if (managerId === null) {
|
||
d.prepare("DELETE FROM scout_zuteilung WHERE scout_id = ?").run(scoutId);
|
||
return;
|
||
}
|
||
d.prepare(`
|
||
INSERT INTO scout_zuteilung (scout_id, manager_id, seit, gesetzt_von)
|
||
VALUES (?,?,?,?)
|
||
ON CONFLICT(scout_id) DO UPDATE SET
|
||
manager_id = excluded.manager_id,
|
||
seit = excluded.seit,
|
||
gesetzt_von = excluded.gesetzt_von`)
|
||
.run(scoutId, managerId, new Date().toISOString(), akteur);
|
||
}
|
||
|
||
/* =====================================================================
|
||
WEN DARF DIESE PERSON ÜBERHAUPT SEHEN? (03.09.2026)
|
||
|
||
Wunsch: "jeder manager soll auch immer nur seine und die seiner
|
||
scouts zugeteilten creator und creator daten sehen. und nicht die der
|
||
anderen."
|
||
|
||
Die Kette Manager -> Scout -> Creator gab es schon (betreuteIds). Was
|
||
fehlte, war ihre ANWENDUNG: Über zwanzig Stellen prüften die ROLLE
|
||
statt der ZUTEILUNG -- "ist Leitung? dann alles". Eine Lecksuche über
|
||
alle Leseschnittstellen (server/pruef-manager-sicht.mjs) fand am
|
||
03.09.2026 dreizehn davon: der Kalender zeigte fremde Fristen, die
|
||
Personenlisten fremde Namen, Report, Steckbrief, Profil, Start-Check,
|
||
Schulung und die Suche jeweils alles.
|
||
|
||
Deshalb stehen die beiden Antworten jetzt HIER, an einer Stelle, und
|
||
werden überall geholt statt jedes Mal neu formuliert.
|
||
|
||
RÜCKGABE null HEISST "ALLE" -- und zwar nur für DogFather. Das ist
|
||
bewusst kein leeres Feld: Eine leere Liste bedeutet "niemand", und
|
||
die Verwechslung der beiden ist genau der Fehler, der aus einer
|
||
Sperre eine Freigabe macht. Wer null bekommt, lässt die Einschränkung
|
||
ganz weg; wer ein Feld bekommt, schränkt darauf ein -- auch wenn es
|
||
leer ist.
|
||
===================================================================== */
|
||
|
||
/** Die Creator, deren Daten diese Person sehen darf.
|
||
* null = alle (nur DogFather). */
|
||
function sichtbareCreatorIdsRoh(person) {
|
||
if (!person) return [];
|
||
if (siehtAlles(person)) return null;
|
||
if (person.rolle === "creator") return [person.id];
|
||
return betreuteIds(person); // Manager: eigene + die seiner Scouts
|
||
}
|
||
|
||
/** Die PERSONEN, die in Listen und Auswahlfeldern auftauchen dürfen --
|
||
* Namen sind auch Daten. null = alle (nur DogFather).
|
||
*
|
||
* Enthält immer die Person selbst: Wer sich in einer Auswahl nicht
|
||
* findet, kann sich nichts selbst zuweisen. Bei einem Manager kommen
|
||
* seine Scouts dazu -- er führt sie, er muss sie eintragen können. */
|
||
/* =====================================================================
|
||
WEN DARF DIESE PERSON NICHT SEHEN? (09.09.2026)
|
||
|
||
Wunsch Filipe, ausdruecklich und dringlich: *"und noch gaaaaaanz
|
||
wichtig keiner soll vanvan sehen ausser ich, ueberall soll keiner
|
||
vanvan sehen ausser dogfather."*
|
||
|
||
Es gab davon bisher nur eine Haelfte: In der Personenliste wurde der
|
||
zweite Admin-Zugang fuer Spicy Media ausgeblendet (Wunsch vom
|
||
31.08.). Ueberall sonst -- Chat, Kalender, Aufgaben, Dateien, Suche
|
||
-- war er sichtbar, und `sichtbarePersonenIds` hat ihn sogar
|
||
ausdruecklich JEDER Rolle gezeigt, weil sie alle Admins einsammelt
|
||
("DogFather ist fuer alle sichtbar").
|
||
|
||
WORAN DIE BEIDEN AUSEINANDERGEHALTEN WERDEN: In der Datenbank tragen
|
||
beide die Rolle `admin`, es gibt kein unterscheidendes Feld. Der
|
||
Unterschied, den es wirklich gibt, ist das Alter -- DogFather ist
|
||
der erste Zugang des Hauses. Deshalb gilt: der Admin mit der
|
||
KLEINSTEN Nummer ist DogFather, alle weiteren Admin-Zugaenge sind
|
||
verborgen.
|
||
|
||
DREI AUSNAHMEN, und nur diese drei:
|
||
* DogFather selbst sieht alle.
|
||
* Ein verborgener Zugang sieht sich selbst (sonst faende er sein
|
||
eigenes Profil nicht).
|
||
* Gibt es nur EINEN Admin, ist nichts zu verbergen.
|
||
|
||
DIE SCHWACHSTELLE STEHT HIER, damit sie niemand suchen muss: Wuerde
|
||
Zugang 1 geloescht, rueckte der naechste Admin nach und waere
|
||
ploetzlich sichtbar. Sollte das eintreten, gehoert ein
|
||
ausdrueckliches Merkmal in die Tabelle (`haupt`-Feld). Solange
|
||
DogFather der erste Zugang ist, ist die Nummer die ehrlichste
|
||
Antwort ohne Datenbankumbau -- und sie haengt nicht am NAMEN, der
|
||
sich aendern kann.
|
||
|
||
WARUM AN DIESER STELLE UND NICHT IN DEN ABFRAGEN: Es gibt 23
|
||
Abfragen allein in workspace-personen.js, die Personen lesen. Eine
|
||
Regel, die man an 23 Stellen wiederholt, ist 23 Gelegenheiten, sie
|
||
zu vergessen -- und beim Vergessen faellt hier niemand auf die Nase,
|
||
sondern es sieht jemand etwas, das er nicht sehen soll. Deshalb
|
||
sitzt sie in den SECHS Listenfunktionen, durch die alles laeuft.
|
||
===================================================================== */
|
||
export function verborgeneIds(person) {
|
||
try {
|
||
const d = db();
|
||
|
||
/* ERSTER TEIL: die verborgenen Admin-Zugaenge (Regel vom 09.09.2026).
|
||
Der Admin mit der kleinsten Nummer ist DogFather, jeder weitere
|
||
Admin-Zugang ist verborgen. */
|
||
const admins = d.prepare(
|
||
"SELECT id FROM personen WHERE rolle = 'admin' ORDER BY id").all().map((z) => z.id);
|
||
const versteckteAdmins = admins.length >= 2 ? admins.slice(1) : [];
|
||
|
||
/* ZWEITER TEIL: das Modi-Team.
|
||
|
||
Hier haengt die Regel an der ROLLE und nicht an einer Nummer --
|
||
und das ist der Unterschied, auf den es ankommt. Beim ersten Teil
|
||
steht die Schwachstelle im Kommentar oben ausdruecklich da: Wuerde
|
||
Zugang 1 geloescht, rueckte der naechste nach und waere ploetzlich
|
||
sichtbar. Eine Rolle kann das nicht passieren. Wer einen Modi
|
||
verbergen will, muss ihm nichts anhaengen und nichts nachhalten --
|
||
er IST verborgen, solange er ein Modi ist.
|
||
|
||
WER DARF SIE SEHEN, und nur diese zwei:
|
||
* die DogFather-Rolle (admin) -- also auch die rechte Hand,
|
||
denn sie traegt dieselbe Rolle. So gewollt (Kapitel 3 des
|
||
Anforderungsdokuments: gleicher Ueberblick).
|
||
* ein Modi selbst -- sie sind untereinander ein Team.
|
||
|
||
Manager, Scouts, Creator und Spicy Media sehen sie nicht. Nicht
|
||
"sehen sie ohne Einzelheiten", sondern gar nicht: kein Name, kein
|
||
Eintrag, keine Zahl, die sie mitzaehlt. */
|
||
const modis = d.prepare(
|
||
`SELECT id FROM personen WHERE rolle IN (${
|
||
[...TEAM_DOGI_ROLLEN].map((r) => `'${r}'`).join(", ")})`).all().map((z) => z.id);
|
||
|
||
/* Ohne bekannte Person wird BEIDES verborgen. Das ist die strengere
|
||
Antwort, und bei einer Sichtbarkeitsregel ist die strengere die
|
||
richtige: Wer nicht weiss, wer fragt, zeigt nichts. */
|
||
const darfAdminsSehen = !!person
|
||
&& (person.id === admins[0] || versteckteAdmins.includes(person.id));
|
||
const darfModisSehen = !!person
|
||
&& (person.rolle === "admin" || TEAM_DOGI_ROLLEN.has(person.rolle));
|
||
|
||
const weg = [];
|
||
if (!darfAdminsSehen) weg.push(...versteckteAdmins);
|
||
if (!darfModisSehen) weg.push(...modis);
|
||
/* Doppelte heraus: Ein Modi-Zugang koennte theoretisch auch in der
|
||
ersten Liste stehen, wenn jemand die Rollen umhaengt. */
|
||
return [...new Set(weg)];
|
||
} catch {
|
||
/* Ohne Datenbank lieber nichts verbergen als abstuerzen -- diese
|
||
Funktion darf keine Seite lahmlegen. Sie laeuft in jedem
|
||
Listenaufruf. */
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/** Dieselbe Regel als Filter auf eine fertige Liste. */
|
||
export function ohneVerborgene(ids, person) {
|
||
const weg = verborgeneIds(person);
|
||
if (!weg.length) return ids;
|
||
if (ids === null) {
|
||
/* `null` heisst bisher "sieht alles". Sobald es etwas zu verbergen
|
||
gibt, darf das nicht mehr gelten -- die Liste wird deshalb
|
||
AUSGESCHRIEBEN. Das ist strenger, nicht lockerer, und die
|
||
Aufrufer bauen daraus ohnehin ein `IN (...)`. */
|
||
try {
|
||
return db().prepare(
|
||
`SELECT id FROM personen WHERE id NOT IN (${weg.map(() => "?").join(",")})`)
|
||
.all(...weg).map((z) => z.id);
|
||
} catch { return null; }
|
||
}
|
||
return ids.filter((i) => !weg.includes(i));
|
||
}
|
||
|
||
function sichtbarePersonenIdsRoh(person) {
|
||
if (!person) return [];
|
||
if (siehtAlles(person)) return null;
|
||
const creator = sichtbareCreatorIds(person) || [];
|
||
const scouts = person.rolle === "manager" ? scoutsVon(person.id) : [];
|
||
/* DOGFATHER IST FUER ALLE SICHTBAR (07.09.2026, Wunsch Filipe:
|
||
"dogfather und agentur sollen die alle sehen").
|
||
|
||
Das ist keine Aufweichung der Regel, sondern ihre Vervollstaendigung:
|
||
Jede Rolle arbeitet mit DogFather -- er gibt frei, er entscheidet,
|
||
er steht in jedem Protokoll. Ihn aus der Personenliste
|
||
herauszuhalten hiess bisher, dass sein Name in Terminen und
|
||
Aufgaben auftaucht, aber nirgends ein Gesicht dazugehoert.
|
||
|
||
Sichtbar heisst hier NUR: Name, Rolle und Bild -- dieselben drei
|
||
Angaben wie bei jedem anderen in dieser Liste. Alles Weitere (seine
|
||
Eintraege, seine Chats, seine Zahlen) haengt weiterhin an den
|
||
eigenen Sichtbarkeitsregeln der jeweiligen Module und ist davon
|
||
nicht beruehrt. */
|
||
const dogi = [];
|
||
try {
|
||
for (const z of db().prepare(
|
||
"SELECT id FROM personen WHERE rolle = 'admin' AND aktiv = 1").all()) dogi.push(z.id);
|
||
} catch { /* ohne Datenbank bleibt die Liste, wie sie war */ }
|
||
|
||
/* DAS MODI-TEAM SIEHT SICH UNTEREINANDER (Entscheidung Filipe,
|
||
09.09.2026: "ja, sie sind untereinander ein Team").
|
||
|
||
Ohne diese Zeilen waere jeder Modi allein: kein Team-Gruppenchat,
|
||
keine Schicht, die man tauschen kann, keine Eskalation, die man
|
||
weiterreicht, ohne dass DogFather sie einzeln durchstellt.
|
||
|
||
Es ist KEIN Loch in der Verborgenheit, sondern ihre andere Seite.
|
||
Was diese Liste zusaetzlich hergibt, wird gleich danach durch
|
||
ohneVerborgene() wieder eingeschraenkt -- und dort steht, dass ein
|
||
Modi nur fuer die DogFather-Rolle und fuer Modis sichtbar ist. Wer
|
||
kein Modi ist, bekommt diese Nummern hier gar nicht erst. */
|
||
const modis = [];
|
||
if (TEAM_DOGI_ROLLEN.has(person.rolle)) {
|
||
try {
|
||
const liste = [...TEAM_DOGI_ROLLEN].map((r) => `'${r}'`).join(", ");
|
||
for (const z of db().prepare(
|
||
`SELECT id FROM personen WHERE rolle IN (${liste}) AND aktiv = 1`).all()) modis.push(z.id);
|
||
} catch { /* ohne Datenbank bleibt die Liste, wie sie war */ }
|
||
}
|
||
return [...new Set([person.id, ...creator, ...scouts, ...dogi, ...modis])];
|
||
}
|
||
|
||
/** WER BETREUT MICH -- die Blickrichtung nach oben.
|
||
*
|
||
* betreuteIds() fragt "wen führe ich?". Diese Funktion fragt das
|
||
* Gegenteil: "wer führt mich?" -- also der eigene Scout, dessen
|
||
* Manager, und bei einem Scout sein Manager.
|
||
*
|
||
* Es gab die Richtung bisher nicht, weil sie für Sichtbarkeit nie
|
||
* gebraucht wurde: Ein Creator soll die Daten seines Scouts nicht
|
||
* sehen. Für eine EINLADUNG ist die Frage aber eine andere -- siehe
|
||
* einladbareIds(). */
|
||
function betreuerIdsRoh(person) {
|
||
if (!person) return [];
|
||
try {
|
||
const d = db();
|
||
if (person.rolle === "creator") {
|
||
const betreuer = d.prepare("SELECT betreuer_id FROM betreuung WHERE creator_id = ?")
|
||
.all(person.id).map((z) => z.betreuer_id);
|
||
if (!betreuer.length) return [];
|
||
/* Über den Scout auch dessen Manager: Wer mit seinem Scout einen
|
||
Call hat, hat ihn oft mit dessen Manager zusammen. */
|
||
const manager = d.prepare(
|
||
`SELECT manager_id FROM scout_zuteilung
|
||
WHERE scout_id IN (${betreuer.map(() => "?").join(",")})`)
|
||
.all(...betreuer).map((z) => z.manager_id);
|
||
return [...new Set([...betreuer, ...manager])];
|
||
}
|
||
if (person.rolle === "scout") {
|
||
return d.prepare("SELECT manager_id FROM scout_zuteilung WHERE scout_id = ?")
|
||
.all(person.id).map((z) => z.manager_id);
|
||
}
|
||
return [];
|
||
} catch {
|
||
return []; // im Zweifel niemand -- nie mehr, als sicher ist
|
||
}
|
||
}
|
||
|
||
/** WEN DARF ICH ZU EINEM TERMIN DAZUSTELLEN?
|
||
*
|
||
* Eingeführt am 05.09.2026 auf den Wunsch, die Teilnehmerwahl solle
|
||
* *"für jeden verfügbar sein"*.
|
||
*
|
||
* WARUM NICHT EINFACH sichtbarePersonenIds: Die Frage ist eine
|
||
* andere. Sichtbarkeit heißt "wessen Daten darf ich sehen" und zeigt
|
||
* bewusst nach unten -- ein Creator sieht dort nur sich selbst.
|
||
* Einladen heißt "mit wem arbeite ich zusammen" und geht in BEIDE
|
||
* Richtungen: Ein Creator lädt seinen Scout ein, ein Scout seinen
|
||
* Manager.
|
||
*
|
||
* Und warum nicht die eine Funktion erweitern: sichtbarePersonenIds
|
||
* hängt auch an der AUFGABENZUWEISUNG. Wer dort einen Betreuer
|
||
* hinzufügt, gibt einem Creator nebenbei die Möglichkeit, seinem
|
||
* Scout Aufgaben zu erteilen. Zwei Fragen, zwei Funktionen.
|
||
*
|
||
* Was ein Einladen bewirkt, ist eng: Die Person sieht diesen einen
|
||
* Termin in ihrem Kalender. Keine Daten, keine Rechte, kein Zugriff.
|
||
* Deshalb ist die weitere Menge hier vertretbar.
|
||
*
|
||
* null = alle (nur DogFather), wie überall in dieser Datei. */
|
||
function einladbareIdsRoh(person) {
|
||
if (!person) return [];
|
||
if (siehtAlles(person)) return null;
|
||
const sichtbar = sichtbarePersonenIds(person) || [];
|
||
/* DogFather gehört immer dazu: Er ist der Einzige, mit dem jede
|
||
Rolle zu tun hat, und ihn nicht einladen zu können wäre der erste
|
||
Fall, der auffällt. */
|
||
const chefs = db().prepare("SELECT id FROM personen WHERE rolle = 'admin' AND aktiv = 1")
|
||
.all().map((z) => z.id);
|
||
return [...new Set([...sichtbar, ...betreuerIds(person), ...chefs])];
|
||
}
|
||
|
||
/** MIT WEM DARF ICH SCHREIBEN? (06.09.2026, für den Chat)
|
||
*
|
||
* Entschieden von Filipe: entlang der Betreuung -- PLUS Manager und
|
||
* Scouts untereinander, auch ohne gemeinsamen Creator, "für Absprachen
|
||
* im Team".
|
||
*
|
||
* WARUM NICHT einladbareIds() WIEDERVERWENDEN, obwohl es fast passt:
|
||
* Dort geht es darum, wen man zu einem TERMIN dazustellen darf. Das
|
||
* ist harmlos -- die Person sieht einen Eintrag im Kalender. Ein Chat
|
||
* ist etwas anderes: Er legt ein dauerhaftes Gespräch an, schickt eine
|
||
* Benachrichtigung und macht den eigenen Namen sichtbar. Zwei Fragen,
|
||
* zwei Funktionen -- sonst verschiebt eine spätere Änderung an der
|
||
* einen stillschweigend die andere.
|
||
*
|
||
* WAS SICH DADURCH ÄNDERT gegenüber der Terminwahl: Ein Manager
|
||
* erreicht jetzt auch Scouts, die nicht seine sind, und umgekehrt.
|
||
* Ein Creator NICHT -- er bleibt bei seinen Betreuern und DogFather.
|
||
* Creator untereinander bleiben getrennt; sie sollen die Namen der
|
||
* anderen Creator weiterhin nicht sehen.
|
||
*
|
||
* null = alle (nur DogFather). */
|
||
function schreibbareIdsRoh(person) {
|
||
if (!person) return [];
|
||
if (siehtAlles(person)) return null;
|
||
|
||
const basis = new Set(einladbareIds(person) || []);
|
||
|
||
/* Das Team untereinander. Ein Scout, der eine Frage zu einem
|
||
fremden Creator hat, muss den zuständigen Manager erreichen können
|
||
-- sonst läuft alles über DogFather, und das ist genau der
|
||
Flaschenhals, den ein Chat auflösen soll. */
|
||
if (person.rolle === "manager" || person.rolle === "scout") {
|
||
try {
|
||
for (const z of db().prepare(
|
||
"SELECT id FROM personen WHERE rolle IN ('manager','scout') AND aktiv = 1").all()) {
|
||
basis.add(z.id);
|
||
}
|
||
} catch { /* im Zweifel nur die Betreuungskette */ }
|
||
}
|
||
|
||
/* DOGFATHER UND SPICY MEDIA SIND FUER JEDEN ERREICHBAR (07.09.2026).
|
||
|
||
Filipe: "im chat muss die spicy rolle und die dogfather rolle fuer
|
||
jeden zugaenglich sein."
|
||
|
||
DogFather kam bisher ueber einladbareIds() mit hinein; Spicy Media
|
||
nicht -- die Rolle steht in keiner Betreuungskette, sie steht
|
||
daneben. Genau deshalb war sie fuer einen Creator im Chat gar nicht
|
||
vorhanden, obwohl sie fuer alle zustaendig ist.
|
||
|
||
Beide werden hier ausdruecklich hinzugefuegt statt sich auf einen
|
||
Nebeneffekt zu verlassen: Eine Zustaendigkeit, die nur zufaellig
|
||
aus einer anderen Regel herausfaellt, faellt beim naechsten Umbau
|
||
genauso zufaellig wieder heraus.
|
||
|
||
Die Gegenrichtung stimmt ohne Zutun: Beide Rollen haben
|
||
`siehtAlles` und damit ohnehin `null` = jeder. */
|
||
try {
|
||
for (const z of db().prepare(
|
||
"SELECT id FROM personen WHERE rolle IN ('admin','spicy') AND aktiv = 1").all()) {
|
||
basis.add(z.id);
|
||
}
|
||
} catch { /* im Zweifel bleibt die Betreuungskette */ }
|
||
|
||
/* Sich selbst nicht -- ein Gespräch mit sich allein ist keines. */
|
||
basis.delete(person.id);
|
||
return [...basis];
|
||
}
|
||
|
||
/** Darf diese Person mit jener schreiben? Eine Frage, eine Antwort --
|
||
* damit die Regel nicht an fünf Stellen einzeln nachgebaut wird. */
|
||
export function darfSchreibenMit(person, andereId) {
|
||
const ids = schreibbareIds(person);
|
||
if (ids === null) return Number(andereId) !== Number(person.id);
|
||
return ids.includes(Number(andereId));
|
||
}
|
||
|
||
/** SQL-Baustein daraus: "diese Spalte ist eine Person, die ich sehen
|
||
* darf". Gibt null zurück, wenn nicht eingeschränkt werden muss. */
|
||
export function personenWo(person, spalte) {
|
||
const ids = sichtbarePersonenIds(person);
|
||
if (ids === null) return null;
|
||
if (!ids.length) return { wo: "0=1", werte: [] };
|
||
return { wo: `${spalte} IN (${ids.map(() => "?").join(",")})`, werte: ids };
|
||
}
|
||
|
||
/* SQL-Baustein "diese Spalte gehoert zu einem meiner Creator".
|
||
Gibt null zurueck, wenn es nichts zu ergaenzen gibt -- eine leere
|
||
IN-Liste waere ungueltiges SQL. */
|
||
export function betreutWo(person, spalte) {
|
||
const ids = betreuteIds(person);
|
||
if (!ids.length) return null;
|
||
return { wo: `${spalte} IN (${ids.map(() => "?").join(",")})`, werte: ids };
|
||
}
|
||
|
||
/* =====================================================================
|
||
FREI EINGETRAGENE NAMEN (02.09.2026)
|
||
|
||
Ein Feld wie "Mit wem" oder "Creator" nimmt entweder eine Person aus
|
||
dem Workspace ODER einen frei getippten Namen. Beides zugleich waere
|
||
eine Aussage, die niemand aufloesen kann.
|
||
|
||
WAS EIN FREIER NAME NICHT KANN, und warum das hier steht:
|
||
Er ist eine Beschriftung, kein Konto. Er sieht nichts, bekommt nichts
|
||
angezeigt, hat kein Aufgabenbrett. Bei "Creator" heisst das: Der
|
||
Eintrag gehoert zu niemandem im System und taucht deshalb in keiner
|
||
personenbezogenen Auswertung auf. Filipe hat das am 02.09.2026
|
||
ausdruecklich so gewaehlt, nachdem der Nachteil benannt war; das
|
||
Formular sagt es an der Stelle noch einmal.
|
||
|
||
Damit daraus kein STILLER Ausfall wird, ist die Gegenmassnahme
|
||
eingebaut: externSql. Jede Abfrage, die bisher den Namen der
|
||
verknuepften Person las, faellt jetzt auf den freien Text zurueck.
|
||
Der Eintrag verschwindet dadurch aus keiner Liste, keiner Suche und
|
||
keiner Uebersicht -- er ist nur eben als "(extern)" gekennzeichnet.
|
||
===================================================================== */
|
||
|
||
export const EXTERN_MAX = 80;
|
||
|
||
/** Prueft einen frei eingetragenen Namen und schreibt ihn nach `aus`.
|
||
* `feld` ist die Spalte ohne Endung, also "creator" oder "teilnehmer".
|
||
*
|
||
* Ist eine Person gewaehlt, wird der freie Name verworfen statt
|
||
* abgelehnt: Wer erst jemanden auswaehlt und dann tippt, meint das
|
||
* Getippte nicht mehr -- eine Fehlermeldung waere hier nur im Weg. */
|
||
export function externPruefen(körper, aus, feld, fehler) {
|
||
const schluessel = `${feld}_extern`;
|
||
if (körper[schluessel] === undefined) return;
|
||
const t = String(körper[schluessel] ?? "").trim();
|
||
if (!t) { aus[schluessel] = null; return; }
|
||
if (t.length > EXTERN_MAX) { fehler.push("Der eingetragene Name ist zu lang."); return; }
|
||
/* Steuerzeichen raus. Sie waeren unsichtbar und koennten eine Zeile
|
||
in Listen und Ausgaben zerreissen. */
|
||
aus[schluessel] = t.replace(/[\u0000-\u001f\u007f]/g, "");
|
||
aus[`${feld}_id`] = null;
|
||
}
|
||
|
||
/** SQL-Baustein: der Name der Person -- und wenn keine verknuepft ist,
|
||
* der frei eingetragene Text, gekennzeichnet.
|
||
*
|
||
* In SQLite ergibt NULL || 'x' wieder NULL. Ist die Spalte leer, faellt
|
||
* COALESCE deshalb korrekt auf NULL durch und nicht auf " (extern)". */
|
||
export const externSql = (personSpalte, externSpalte) =>
|
||
`COALESCE(${personSpalte}, ${externSpalte} || ' (extern)')`;
|
||
|
||
/* Wer sieht welchen Termin -- und damit auch: welche Wiederholung.
|
||
|
||
NUR DogFather sieht alles (01.09.2026). Vorher stand hier istLeitung()
|
||
-- damit sah auch jeder Manager jeden Creator. Ein Manager faellt
|
||
jetzt in dieselbe Regel wie ein Scout: nur die Creator, die ihm
|
||
zugeteilt sind. Ausdruecklicher Wunsch: "NUR DIE ROLLE DOGFATHER SOLL
|
||
WIRKLICH ALLEINE ALLES SEHEN."
|
||
|
||
Geaendert wird ausschliesslich, wer was SIEHT. Was ein Manager DARF
|
||
(freigeben, aendern, Personen verwalten), haengt weiterhin an
|
||
istLeitung und bleibt unveraendert -- sonst haette dieser eine Wunsch
|
||
stillschweigend seine halben Rechte mitgenommen.
|
||
|
||
Der Praefix ist der Tabellenname im jeweiligen SQL: "t" fuer termine,
|
||
"s" fuer termin_serien. Diese Regel steht bewusst nur EINMAL: Eine
|
||
zweite, fast gleiche Fassung fuer die Serien waere frueher oder
|
||
spaeter auseinandergelaufen -- und dann haette eine Wiederholung
|
||
jemandem etwas gezeigt, was der einzelne Termin ihm verbirgt. */
|
||
export function termineSichtbar(person, praefix = "t") {
|
||
const p = praefix;
|
||
|
||
/* =====================================================================
|
||
JEDER SIEHT NUR SEINE EIGENEN TERMINE (07.09.2026)
|
||
|
||
Wunsch Filipe, zuerst: "calls oder termine soll jeder nur sehen
|
||
die er selber macht oder jeden betrifft." Wenige Stunden spaeter,
|
||
nachdem er das Ergebnis gesehen hat, praeziser: "jeder soll und
|
||
darf im kalender immer nur seine eigenen eintraege nur sehen."
|
||
Die zweite Fassung gilt -- Begruendung unten beim entfallenen
|
||
Fall "geht alle an".
|
||
|
||
DAS GILT AUCH FUER DOGFATHER, und das ist die eigentliche
|
||
Aenderung. Bis hierher bekam er `1=1` -- er sah jeden Termin von
|
||
jedem. Bei einer Handvoll Leuten war das ein Ueberblick; bei
|
||
dreissig ist es eine Wand aus fremden Verabredungen, in der die
|
||
eigenen untergehen. Ein Kalender, in dem alles steht, ist kein
|
||
Kalender mehr.
|
||
|
||
"EIGEN" IST HIER GENAU DEFINIERT, sonst wird es Auslegung: Ich habe
|
||
den Termin angelegt (erstellt_von), er ist mir zugeordnet
|
||
(creator_id), ich bin das Gegenueber (teilnehmer_id) -- oder ich
|
||
stehe in der Teilnehmerliste. Sonst nichts. Kein Rollenrecht, keine
|
||
Betreuung, keine Ausnahme fuer teilnehmerlose Termine.
|
||
|
||
Diese Regel ersetzt die Rollenfrage vollstaendig: Es gibt hier
|
||
nichts mehr, was DogFather sieht und ein Creator nicht. Was ein
|
||
Betreuer ueber seine Creator wissen muss, steht in deren Bereichen
|
||
und Aufgaben -- nicht in ihrem Kalender.
|
||
===================================================================== */
|
||
|
||
/* MITGEZAEHLT WIRD AUCH DIE TEILNEHMERLISTE (05.09.2026).
|
||
|
||
Seit ein Termin mehrere Teilnehmer haben kann, reicht
|
||
`teilnehmer_id` nicht mehr: Das ist nur das Haupt-Gegenueber. Wer
|
||
als zweiter oder dritter dabei ist, steht in termin_teilnehmer --
|
||
und ohne diese Zeile saehe er den Termin nicht, an dem er
|
||
teilnimmt.
|
||
|
||
Die beiden Aufrufer arbeiten auf verschiedenen Tabellen: der
|
||
Kalender auf `termine` (praefix t), die Wiederholungen auf
|
||
`termin_serien` (praefix s). Deshalb wird hier die passende
|
||
Nebentabelle gewaehlt statt einer festen -- eine falsche Zuordnung
|
||
waere kein Fehler, den man sieht, sondern eine Liste, in der
|
||
jemandem etwas fehlt. */
|
||
const nebenTabelle = p === "s"
|
||
? { tabelle: "serie_teilnehmer", spalte: "serie_id" }
|
||
: { tabelle: "termin_teilnehmer", spalte: "termin_id" };
|
||
const dabei = `EXISTS (SELECT 1 FROM ${nebenTabelle.tabelle} tn`
|
||
+ ` WHERE tn.${nebenTabelle.spalte} = ${p}.id AND tn.person_id = ?)`;
|
||
|
||
const eigen = `(${p}.creator_id = ? OR ${p}.teilnehmer_id = ? OR ${p}.erstellt_von = ?`
|
||
+ ` OR ${dabei})`;
|
||
const werte = [person.id, person.id, person.id, person.id];
|
||
|
||
/* HIER STAND EIN ZWEITER FALL, UND ER IST AM 07.09.2026 ENTFALLEN.
|
||
|
||
Er hiess "geht alle an": ein Termin ohne Gegenueber und ohne
|
||
Teilnehmer war fuer jeden sichtbar, nach der Ueberlegung "wer
|
||
niemanden eintraegt, meint alle".
|
||
|
||
Die Ueberlegung war falsch, und Filipe hat es an einem echten Fall
|
||
gezeigt: In Cigdems Kalender stand sein "Manager Meeting" -- SEIN
|
||
Termin, den er fuer sich eingetragen hatte. Wer keinen Teilnehmer
|
||
eintraegt, meint eben meistens nicht "alle", sondern "mich". Und
|
||
die Auslegung lag nicht bei dem, der den Termin anlegt, sondern
|
||
hier im Code -- das ist die eigentliche Schwaeche gewesen.
|
||
|
||
Filipe: "jeder soll und darf im kalender immer nur seine eigenen
|
||
eintraege nur sehen."
|
||
|
||
Was alle angeht, geht sie jetzt an, weil jemand sie EINTRAEGT:
|
||
"bei calls und protocoll will ich dass jeder seine termine von
|
||
seinem kalender sieht und die wenn sie markiert werden im termin."
|
||
Die Teilnehmerliste ist damit die einzige Antwort auf die Frage,
|
||
wen ein Termin etwas angeht -- eine sichtbare Entscheidung im
|
||
Formular statt einer unsichtbaren Regel im Server.
|
||
|
||
DIESELBE REGEL GILT FUER CALLS & PROTOKOLLE: Die Call-Liste ruft
|
||
genau diese Funktion auf (workspace-calls.js -> sichtbar). Beide
|
||
Seiten koennen deshalb gar nicht auseinanderlaufen. */
|
||
|
||
/* UND AUF DER ADRESSE VON TEAM DOGI OHNE DIE AGENTUR (10.09.2026).
|
||
|
||
Hier steht die Bedingung IN termineSichtbar und nicht in
|
||
workspace-kalender.js -- sonst haetten der Kalender, die
|
||
Wiederholungen (workspace-serien.js) und der ICS-Abruf
|
||
(workspace-ics.js) sie einzeln gebraucht, und der ICS-Abruf ist
|
||
genau der, den man vergisst: Er laeuft ohne Bildschirm.
|
||
|
||
`teilnehmer_id` gehoert in die Liste. Ein Gespraech mit einem
|
||
Creator haengt oft an gar keiner creator_id -- der Creator IST das
|
||
Gegenueber. Wer nur zwei Spalten prueft, hat hier nichts geprueft. */
|
||
/* EINE AUSNAHME, UND ZWAR GENAU EINE (11.09.2026).
|
||
|
||
Filipe: "mein kalender (dogfather) auf dieser seite hier soll
|
||
komplett verbunden sein mit dem kalender in der workspace seite
|
||
ABER NUR IN DER DOGFATHER ROLLE BEI DOGFATHER: NICHT BEI VANVAN."
|
||
|
||
Ein Kalender ist etwas anderes als eine Liste. Bei den Aufgaben,
|
||
den Bereichen und den Dateien ist die Trennung eine Hilfe -- man
|
||
will das andere Haus gerade NICHT sehen. Beim Kalender waere sie
|
||
eine Falle: Wer auf der Team-Seite einen Termin einträgt und die
|
||
Haelfte seines Tages nicht sieht, legt ihn auf eine Zeit, in der er
|
||
schon woanders sitzt. Ein Kalender, der nur die halbe Wahrheit
|
||
zeigt, ist schlimmer als keiner.
|
||
|
||
NUR FUER DIE DOGFATHER-ROLLE, und das ist sein ausdrueckliches
|
||
Wort. Die rechte Hand behaelt die Trennung: Sie arbeitet auf dieser
|
||
Adresse mit dem Team, und die Termine der Agentur gehen sie dort
|
||
nichts an. Geprueft wird die ROLLE und nicht der Name -- ein Name
|
||
waere die Stelle, an der es beim naechsten Menschen bricht. */
|
||
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;
|
||
}
|
||
}
|
||
|
||
/* WELCHE EINSTELLUNGEN NICHT IM KLARTEXT INS PROTOKOLL GEHOEREN
|
||
---------------------------------------------------------------------
|
||
Gefunden am 23.09.2026, beim Vorbereiten des KLIPY-Zugangs.
|
||
|
||
einstellungSetzen() schrieb bis dahin IMMER Name UND Wert ins
|
||
Protokoll. Fuer "nachtruhe_an = 1" ist das genau richtig -- man
|
||
will sehen, wer wann was umgestellt hat. Fuer einen Schluessel
|
||
ist es das Gegenteil.
|
||
|
||
NACHGEMESSEN in der echten Datenbank, nicht vermutet:
|
||
|
||
kopie_schluessel (31.08.) steht VOLLSTAENDIG im Protokoll
|
||
vapid_paar (05.09.) wurde bei 120 Zeichen abgeschnitten,
|
||
kurz VOR dem privaten Teil
|
||
|
||
Dass der zweite heil blieb, lag an einer Laengengrenze, nicht an
|
||
einer Absicht. Das ist keine offene Tuer -- das Protokoll liegt
|
||
hinter nurAdmin --, aber der Wert wandert in JEDE Sicherung und in
|
||
jeden Export. Und der naechste Schluessel waere denselben Weg
|
||
gegangen.
|
||
|
||
ES IST EINE REGEL UND KEINE LISTE. Eine Liste
|
||
["kopie_schluessel","vapid_paar"] waere beim naechsten Geheimnis
|
||
still unvollstaendig -- derselbe Fehler wie bei den abgeschriebenen
|
||
Spaltennamen am 11.09. Wer eine Einstellung so BENENNT, meint auch
|
||
ein Geheimnis. Faellt ein harmloser Name versehentlich darunter,
|
||
ist der Schaden, dass im Protokoll etwas weniger Text steht.
|
||
|
||
WAS STEHEN BLEIBT: dass sich die Einstellung geaendert hat, wann,
|
||
und durch wen. Nur der Wert fehlt. Ein Protokolleintrag ohne
|
||
Eintrag waere die schlechtere Loesung -- dann saehe niemand mehr,
|
||
dass jemand den Schluessel ausgetauscht hat. */
|
||
const GEHEIME_EINSTELLUNG = /(schluessel|schlüssel|geheim|token|passwort|kennwort|secret|paar|(^|[_-])key([_-]|$))/i;
|
||
|
||
export function einstellungSetzen(schluessel, wert, akteur = null) {
|
||
db().prepare(`
|
||
INSERT INTO einstellungen (schluessel, wert, geaendert, von) VALUES (?,?,?,?)
|
||
ON CONFLICT(schluessel) DO UPDATE SET
|
||
wert = excluded.wert, geaendert = excluded.geaendert, von = excluded.von`).run(
|
||
schluessel, String(wert), new Date().toISOString(), akteur?.id ?? null);
|
||
const roh = String(wert ?? "");
|
||
const detail = GEHEIME_EINSTELLUNG.test(String(schluessel))
|
||
? `${schluessel} = ${roh ? "(gesetzt, " + roh.length + " Zeichen)" : "(geleert)"}`
|
||
: `${schluessel} = ${wert}`;
|
||
protokolliere("einstellung_geaendert", {
|
||
personId: akteur?.id ?? null, rolle: akteur?.rolle ?? null, ip: akteur?.ip ?? null,
|
||
detail: detail.slice(0, 120),
|
||
});
|
||
}
|
||
|
||
export function personAnlegen(name, rolle, akteur = null) {
|
||
if (!ROLLEN.has(rolle)) throw new Error(`Unbekannte Rolle: ${rolle}`);
|
||
const code = codeErzeugen();
|
||
const salt = randomBytes(16).toString("hex");
|
||
const hash = hashe(code, salt, SCRYPT.N);
|
||
const { lastInsertRowid } = db().prepare(
|
||
"INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
|
||
+ " VALUES (?,?,?,?,?,?,1,?)"
|
||
).run(name, rolle, hash, salt, SCRYPT.N, codeKennung(code), jetzt());
|
||
protokolliere("person_angelegt", {
|
||
personId: akteur?.id ?? null, rolle: akteur?.rolle ?? null,
|
||
ip: akteur?.ip ?? null, detail: `${name} (${rolle})`,
|
||
});
|
||
return { id: Number(lastInsertRowid), name, rolle, code };
|
||
}
|
||
|
||
/** Das Sitzungs-Merkmal aus der Anfrage -- roh, wie es im Keks steht.
|
||
* Wird gebraucht, um GENAU DIESE eine Sitzung am Leben zu lassen. */
|
||
export const sitzungToken = (req) => req?.cookies?.[COOKIE] || null;
|
||
|
||
/**
|
||
* Neuen Code setzen.
|
||
*
|
||
* @param behalteToken Die eine Sitzung, die NICHT beendet wird.
|
||
* Gedacht für den Fall "ich erneuere meinen eigenen Code": Wer das tut,
|
||
* ist gerade angemeldet und hält diese Sitzung in der Hand.
|
||
*/
|
||
export function codeNeu(id, akteur = null, behalteToken = null) {
|
||
const person = db().prepare("SELECT id, name, rolle FROM personen WHERE id = ?").get(id);
|
||
if (!person) throw new Error(`Keine Person mit Nummer ${id}`);
|
||
const code = codeErzeugen();
|
||
const salt = randomBytes(16).toString("hex");
|
||
db().prepare(
|
||
"UPDATE personen SET code_hash = ?, code_salt = ?, code_n = ?, code_kennung = ? WHERE id = ?")
|
||
.run(hashe(code, salt, SCRYPT.N), salt, SCRYPT.N, codeKennung(code), id);
|
||
|
||
/* Offene Sitzungen beenden -- ein neuer Code soll den alten Zugang
|
||
wirklich beenden, nicht nur die nächste Anmeldung betreffen.
|
||
|
||
MIT EINER AUSNAHME, seit dem 02.09.2026: die Sitzung, aus der heraus
|
||
der Code gerade erneuert wird.
|
||
|
||
Vorher flog man beim eigenen Code sofort hinaus -- und zwar in
|
||
derselben Sekunde, in der der neue Code auf dem Bildschirm erschien.
|
||
Der nächste Aufruf der Seite lief in ein "nicht angemeldet" und
|
||
leitete zur Anmeldung um; der Code war weg, bevor man ihn kopieren
|
||
konnte. Genau so ist es Filipe am 02.09.2026 passiert, und danach kam
|
||
er nur noch über einen Eingriff auf dem Server wieder hinein.
|
||
|
||
Sicherheitlich kostet die Ausnahme nichts: Wer den Code erneuert, hat
|
||
sich mit genau dieser Sitzung soeben ausgewiesen und hält sie in
|
||
Händen. Alle ANDEREN Geräte fliegen weiterhin hinaus -- das ist der
|
||
Sinn der Übung. */
|
||
const behalten = behalteToken ? tokenHash(behalteToken) : null;
|
||
if (behalten) {
|
||
db().prepare("DELETE FROM sitzungen WHERE person_id = ? AND token_hash <> ?")
|
||
.run(id, behalten);
|
||
} else {
|
||
db().prepare("DELETE FROM sitzungen WHERE person_id = ?").run(id);
|
||
}
|
||
protokolliere("code_erneuert", {
|
||
personId: akteur?.id ?? null, rolle: akteur?.rolle ?? null,
|
||
ip: akteur?.ip ?? null, detail: `für ${person.name}`,
|
||
});
|
||
return { ...person, code };
|
||
}
|
||
|
||
export function personSperren(id, aktiv = 0, akteur = null) {
|
||
const person = db().prepare("SELECT name FROM personen WHERE id = ?").get(id);
|
||
db().prepare("UPDATE personen SET aktiv = ? WHERE id = ?").run(aktiv ? 1 : 0, id);
|
||
if (!aktiv) db().prepare("DELETE FROM sitzungen WHERE person_id = ?").run(id);
|
||
protokolliere(aktiv ? "person_entsperrt" : "person_gesperrt", {
|
||
personId: akteur?.id ?? null, rolle: akteur?.rolle ?? null,
|
||
ip: akteur?.ip ?? null, detail: person?.name ?? `#${id}`,
|
||
});
|
||
}
|
||
|
||
export function personenListe() {
|
||
return db().prepare(`
|
||
SELECT p.id, p.name, p.rolle, p.aktiv, p.erstellt, p.letzter_login,
|
||
(SELECT COUNT(*) FROM sitzungen s WHERE s.person_id = p.id) AS sitzungen
|
||
FROM personen p ORDER BY ${ROLLEN_SORTIERUNG.replace("rolle", "p.rolle")}, p.name`).all();
|
||
}
|
||
|
||
export function protokollLesen(anzahl = 20) {
|
||
return db().prepare(
|
||
"SELECT zeitpunkt, rolle, aktion, detail, ip FROM protokoll ORDER BY id DESC LIMIT ?"
|
||
).all(anzahl);
|
||
}
|
||
|
||
/* =====================================================================
|
||
DER MANTEL UM DIE SECHS LISTENFUNKTIONEN (09.09.2026)
|
||
|
||
Jede von ihnen hat mehrere Rueckgabewege -- `sichtbarePersonenIds`
|
||
allein drei. Die Verbergungsregel an jedem einzelnen anzubringen
|
||
waere achtzehn Gelegenheiten, einen zu vergessen; und wer hier einen
|
||
vergisst, merkt es nicht an einem Fehler, sondern daran, dass jemand
|
||
etwas sieht, das er nicht sehen soll.
|
||
|
||
Deshalb bleiben die Funktionen unveraendert (jetzt mit `Roh` im
|
||
Namen) und werden EINMAL ummantelt. Der Mantel ist die einzige
|
||
Stelle, an der die Regel steht -- und er kann keinen Rueckgabeweg
|
||
uebersehen, weil er hinter allen sitzt.
|
||
|
||
Die Namen nach aussen bleiben gleich: Kein Aufrufer muss geaendert
|
||
werden, und niemand kann versehentlich die ungefilterte Fassung
|
||
benutzen -- die `Roh`-Funktionen werden nicht exportiert.
|
||
===================================================================== */
|
||
/* =====================================================================
|
||
PERSONENLISTEN IM HAUS VON TEAM DOGI (10.09.2026)
|
||
|
||
Die sechs Funktionen darunter beantworten "welche Menschen gehen
|
||
mich etwas an" -- fuer die Chat-Partnerwahl, die Teilnehmerwahl im
|
||
Kalender, die Creator-Auswahl in Formularen und die Pipeline.
|
||
|
||
Auf der Adresse von Team Dogi ist die Antwort eine andere: dort gibt
|
||
es keine Creator, keine Scouts, keine Manager. Nicht "sie sind
|
||
ausgeblendet", sondern: Sie kommen gar nicht erst aus der Datenbank.
|
||
|
||
WARUM ALS ZWEITE HUELLE UM ohneVerborgene UND NICHT DARIN: Die beiden
|
||
beantworten verschiedene Fragen. ohneVerborgene() nimmt heraus, was
|
||
jemand NICHT SEHEN DARF -- eine Rechtefrage, die ueberall gilt.
|
||
nurHaus() nimmt heraus, was HIER NICHT HINGEHOERT -- eine Frage der
|
||
Adresse. Wer beides in eine Funktion legt, kann spaeter nicht mehr
|
||
sagen, welche der beiden gerade zugeschlagen hat.
|
||
|
||
Die Reihenfolge ist egal (beide verkleinern nur), die Trennung nicht.
|
||
===================================================================== */
|
||
/* WER AUF DER TEAM-ADRESSE UEBERHAUPT VORKOMMT.
|
||
*
|
||
* Die Community steht seit dem 11.09.2026 mit drin (Filipe: "das soll
|
||
* keine app fuer sich sein, das soll in der crew seite adaptiert
|
||
* werden"). Der Treff wohnt jetzt hier, also muessen seine Mitglieder
|
||
* hier auch verwaltet werden koennen -- sonst legt DogFather einen
|
||
* Zugang an und er verschwindet im selben Moment aus der Liste.
|
||
*
|
||
* DAS MACHT SIE NICHT ZU KOLLEGEN. Diese Menge beantwortet nur "wer
|
||
* kommt auf dieser Adresse vor". Ob jemand in eine Einladung, eine
|
||
* Aufgabe oder einen Chat darf, beantwortet ohneAussen() weiter unten
|
||
* -- und das sagt bei der Community ueberall Nein. Zwei verschiedene
|
||
* Fragen, zwei verschiedene Funktionen; wer sie in eine legt, kann
|
||
* spaeter nicht mehr sagen, welche gerade zugeschlagen hat. */
|
||
const HAUS_TEAM_ROLLEN = new Set(["admin", "hand", "linke", "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));
|